feat: 后台线下支付订单强制上传支付凭证
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m15s

在后台创建订单接口中新增 payment_voucher_key 字段,线下支付方式
下该字段为必填,存储对象存储 file_key,便于后续追溯与审计。

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-08 16:45:06 +08:00
parent 78d6aded9b
commit cc56e27c01
6 changed files with 31 additions and 5 deletions

View File

@@ -2220,6 +2220,10 @@ components:
payment_method:
description: 支付方式 (wallet:钱包支付, offline:线下支付)
type: string
payment_voucher_key:
description: 线下支付凭证对象存储file_keypayment_method=offline时必填通过/storage/upload-url上传图片后获得
maxLength: 500
type: string
required:
- identifier
- package_ids
@@ -4945,7 +4949,7 @@ components:
nullable: true
type: string
payment_method:
description: 支付方式 (wallet:钱包支付, wechat:微信支付, alipay:支付宝支付)
description: 支付方式 (wallet:钱包支付, wechat:微信支付, alipay:支付宝支付, offline:线下支付)
type: string
payment_status:
description: 支付状态 (1:待支付, 2:已支付, 3:已取消, 4:已退款)
@@ -4953,6 +4957,9 @@ components:
payment_status_text:
description: 支付状态文本
type: string
payment_voucher_key:
description: 线下支付凭证对象存储file_key仅线下支付订单有值
type: string
purchase_remark:
description: 购买备注
type: string

View File

@@ -12,9 +12,10 @@ type CreateOrderRequest struct {
// CreateAdminOrderRequest 后台订单创建请求(仅允许 wallet/offline
type CreateAdminOrderRequest struct {
Identifier string `json:"identifier" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"资产标识符ICCID 或 VirtualNo"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet offline" required:"true" description:"支付方式 (wallet:钱包支付, offline:线下支付)"`
Identifier string `json:"identifier" validate:"required,min=1,max=100" required:"true" minLength:"1" maxLength:"100" description:"资产标识符ICCID 或 VirtualNo"`
PackageIDs []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet offline" required:"true" description:"支付方式 (wallet:钱包支付, offline:线下支付)"`
PaymentVoucherKey string `json:"payment_voucher_key" validate:"omitempty,max=500" maxLength:"500" description:"线下支付凭证对象存储file_keypayment_method=offline时必填通过/storage/upload-url上传图片后获得"`
}
type OrderListRequest struct {
@@ -52,10 +53,11 @@ type OrderResponse struct {
IotCardID *uint `json:"iot_card_id,omitempty" description:"IoT卡ID"`
DeviceID *uint `json:"device_id,omitempty" description:"设备ID"`
TotalAmount int64 `json:"total_amount" description:"订单总金额(分)"`
PaymentMethod string `json:"payment_method,omitempty" description:"支付方式 (wallet:钱包支付, wechat:微信支付, alipay:支付宝支付)"`
PaymentMethod string `json:"payment_method,omitempty" description:"支付方式 (wallet:钱包支付, wechat:微信支付, alipay:支付宝支付, offline:线下支付)"`
PaymentStatus int `json:"payment_status" description:"支付状态 (1:待支付, 2:已支付, 3:已取消, 4:已退款)"`
PaymentStatusText string `json:"payment_status_text" description:"支付状态文本"`
PaidAt *time.Time `json:"paid_at,omitempty" description:"支付时间"`
PaymentVoucherKey string `json:"payment_voucher_key,omitempty" description:"线下支付凭证对象存储file_key仅线下支付订单有值"`
IsPurchaseOnBehalf bool `json:"is_purchase_on_behalf" description:"是否为代购订单"`
CommissionStatus int `json:"commission_status" description:"佣金状态 (1:待计算, 2:已计算)"`
CommissionConfigVersion int `json:"commission_config_version" description:"佣金配置版本"`

View File

@@ -63,6 +63,9 @@ type Order struct {
// 支付配置
PaymentConfigID *uint `gorm:"column:payment_config_id;index;comment:支付配置ID(关联tb_wechat_config.id)" json:"payment_config_id,omitempty"`
// 线下支付凭证对象存储 file_key仅线下支付订单必填
PaymentVoucherKey string `gorm:"column:payment_voucher_key;type:varchar(500);comment:线下支付凭证对象存储file_key" json:"payment_voucher_key,omitempty"`
}
// TableName 指定表名

View File

@@ -351,6 +351,11 @@ func (s *Service) CreateLegacy(ctx context.Context, req *dto.CreateOrderRequest,
// 与 CreateH5Order 的核心区别后台订单不创建待支付状态wallet 立即扣款offline 立即激活
// POST /api/admin/orders
func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
// 线下支付必须上传支付凭证
if req.PaymentMethod == model.PaymentMethodOffline && strings.TrimSpace(req.PaymentVoucherKey) == "" {
return nil, errors.New(errors.CodeInvalidParam, "线下支付必须上传支付凭证")
}
resolvedCard, resolvedDevice, resolveErr := s.resolveAssetByIdentifier(ctx, req.Identifier)
if resolveErr != nil {
return nil, resolveErr
@@ -619,6 +624,11 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
PaymentConfigID: paymentConfigID,
}
// 线下支付订单写入支付凭证 file_key
if req.PaymentMethod == model.PaymentMethodOffline {
order.PaymentVoucherKey = strings.TrimSpace(req.PaymentVoucherKey)
}
items := s.buildOrderItems(userID, validationResult.Packages)
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, orderType, carrierType, carrierID, req.PackageIDs)
@@ -2177,6 +2187,7 @@ func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderIte
PaymentStatus: order.PaymentStatus,
PaymentStatusText: statusText,
PaidAt: order.PaidAt,
PaymentVoucherKey: order.PaymentVoucherKey,
IsPurchaseOnBehalf: order.IsPurchaseOnBehalf,
CommissionStatus: order.CommissionStatus,
CommissionConfigVersion: order.CommissionConfigVersion,

View File

@@ -0,0 +1 @@
ALTER TABLE tb_order DROP COLUMN IF EXISTS payment_voucher_key;

View File

@@ -0,0 +1,2 @@
ALTER TABLE tb_order ADD COLUMN IF NOT EXISTS payment_voucher_key VARCHAR(500);
COMMENT ON COLUMN tb_order.payment_voucher_key IS '线下支付凭证对象存储 file_key仅线下支付订单必填';