From cc56e27c01f26d681912ab5cc155e9af3b93975d Mon Sep 17 00:00:00 2001 From: huang Date: Wed, 8 Apr 2026 16:45:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=8E=E5=8F=B0=E7=BA=BF=E4=B8=8B?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E8=AE=A2=E5=8D=95=E5=BC=BA=E5=88=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=94=AF=E4=BB=98=E5=87=AD=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在后台创建订单接口中新增 payment_voucher_key 字段,线下支付方式 下该字段为必填,存储对象存储 file_key,便于后续追溯与审计。 Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/admin-openapi.yaml | 9 ++++++++- internal/model/dto/order_dto.go | 10 ++++++---- internal/model/order.go | 3 +++ internal/service/order/service.go | 11 +++++++++++ .../000109_add_payment_voucher_to_order.down.sql | 1 + migrations/000109_add_payment_voucher_to_order.up.sql | 2 ++ 6 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 migrations/000109_add_payment_voucher_to_order.down.sql create mode 100644 migrations/000109_add_payment_voucher_to_order.up.sql diff --git a/docs/admin-openapi.yaml b/docs/admin-openapi.yaml index 673b3d9..1c0a7d3 100644 --- a/docs/admin-openapi.yaml +++ b/docs/admin-openapi.yaml @@ -2220,6 +2220,10 @@ components: payment_method: description: 支付方式 (wallet:钱包支付, offline:线下支付) type: string + payment_voucher_key: + description: 线下支付凭证对象存储file_key(payment_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 diff --git a/internal/model/dto/order_dto.go b/internal/model/dto/order_dto.go index cf88d12..3efd337 100644 --- a/internal/model/dto/order_dto.go +++ b/internal/model/dto/order_dto.go @@ -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_key(payment_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:"佣金配置版本"` diff --git a/internal/model/order.go b/internal/model/order.go index eba3d30..c496635 100644 --- a/internal/model/order.go +++ b/internal/model/order.go @@ -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 指定表名 diff --git a/internal/service/order/service.go b/internal/service/order/service.go index a6abcfb..f8d1980 100644 --- a/internal/service/order/service.go +++ b/internal/service/order/service.go @@ -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, diff --git a/migrations/000109_add_payment_voucher_to_order.down.sql b/migrations/000109_add_payment_voucher_to_order.down.sql new file mode 100644 index 0000000..aff7521 --- /dev/null +++ b/migrations/000109_add_payment_voucher_to_order.down.sql @@ -0,0 +1 @@ +ALTER TABLE tb_order DROP COLUMN IF EXISTS payment_voucher_key; diff --git a/migrations/000109_add_payment_voucher_to_order.up.sql b/migrations/000109_add_payment_voucher_to_order.up.sql new file mode 100644 index 0000000..5a0d255 --- /dev/null +++ b/migrations/000109_add_payment_voucher_to_order.up.sql @@ -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(仅线下支付订单必填)';