diff --git a/internal/model/dto/refund_dto.go b/internal/model/dto/refund_dto.go index ac79fb7..411a48c 100644 --- a/internal/model/dto/refund_dto.go +++ b/internal/model/dto/refund_dto.go @@ -5,6 +5,7 @@ type CreateRefundRequest struct { OrderID uint `json:"order_id" validate:"required" required:"true" description:"关联订单ID"` ActualReceivedAmount int64 `json:"actual_received_amount" validate:"required,min=1" required:"true" minimum:"1" description:"实收金额(分)"` RequestedRefundAmount int64 `json:"requested_refund_amount" validate:"required,min=1" required:"true" minimum:"1" description:"申请退款金额(分)"` + RefundVoucherKey string `json:"refund_voucher_key" validate:"required,min=1,max=500" required:"true" minLength:"1" maxLength:"500" description:"退款凭证对象存储file_key(通过/storage/upload-url上传图片后获得)"` RefundReason string `json:"refund_reason" validate:"omitempty,max=1000" maxLength:"1000" description:"退款原因"` PackageUsageID *uint `json:"package_usage_id" validate:"omitempty" description:"关联套餐使用记录ID(可选)"` } @@ -26,6 +27,7 @@ type ResubmitRefundRequest struct { ID uint `json:"-" params:"id" path:"id" validate:"required" description:"退款申请ID"` ActualReceivedAmount *int64 `json:"actual_received_amount" validate:"omitempty,min=1" minimum:"1" description:"实收金额(分)"` RequestedRefundAmount *int64 `json:"requested_refund_amount" validate:"omitempty,min=1" minimum:"1" description:"申请退款金额(分)"` + RefundVoucherKey *string `json:"refund_voucher_key" validate:"omitempty,max=500" maxLength:"500" description:"退款凭证对象存储file_key(重新提交时可替换;历史记录缺失时必填)"` RefundReason *string `json:"refund_reason" validate:"omitempty,max=1000" maxLength:"1000" description:"退款原因"` } @@ -67,6 +69,7 @@ type RefundResponse struct { ActualReceivedAmount int64 `json:"actual_received_amount" description:"实收金额(分)"` RequestedRefundAmount int64 `json:"requested_refund_amount" description:"申请退款金额(分)"` ApprovedRefundAmount *int64 `json:"approved_refund_amount,omitempty" description:"审批实际退款金额(分)"` + RefundVoucherKey string `json:"refund_voucher_key,omitempty" description:"退款凭证对象存储file_key"` RefundReason string `json:"refund_reason" description:"退款原因"` Status int `json:"status" description:"状态 (1:待审批, 2:已通过, 3:已拒绝, 4:已退回)"` StatusName string `json:"status_name" description:"状态名称(中文)"` diff --git a/internal/model/refund.go b/internal/model/refund.go index 6df65f4..599093c 100644 --- a/internal/model/refund.go +++ b/internal/model/refund.go @@ -30,9 +30,10 @@ type RefundRequest struct { RequestedRefundAmount int64 `gorm:"column:requested_refund_amount;type:bigint;not null;comment:申请退款金额(分)" json:"requested_refund_amount"` ApprovedRefundAmount *int64 `gorm:"column:approved_refund_amount;type:bigint;comment:审批实际退款金额(分)" json:"approved_refund_amount,omitempty"` - // 退款原因和状态 - RefundReason string `gorm:"column:refund_reason;type:text;comment:退款原因" json:"refund_reason"` - Status int `gorm:"column:status;type:int;not null;default:1;index;comment:状态 1-待审批 2-已通过 3-已拒绝 4-已退回" json:"status"` + // 退款凭证、原因和状态 + RefundVoucherKey string `gorm:"column:refund_voucher_key;type:varchar(500);not null;default:'';comment:退款凭证对象存储file_key" json:"refund_voucher_key,omitempty"` + RefundReason string `gorm:"column:refund_reason;type:text;comment:退款原因" json:"refund_reason"` + Status int `gorm:"column:status;type:int;not null;default:1;index;comment:状态 1-待审批 2-已通过 3-已拒绝 4-已退回" json:"status"` // 审批信息 ProcessorID *uint `gorm:"column:processor_id;comment:审批人ID" json:"processor_id,omitempty"` diff --git a/internal/service/refund/service.go b/internal/service/refund/service.go index 234f725..936087b 100644 --- a/internal/service/refund/service.go +++ b/internal/service/refund/service.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "math/rand" + "strings" "time" "go.uber.org/zap" @@ -99,6 +100,10 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dt if err := validateRequestedRefundAmountByOrder(req.RequestedRefundAmount, order); err != nil { return nil, err } + refundVoucherKey, err := normalizeRefundVoucherKey(req.RefundVoucherKey) + if err != nil { + return nil, err + } // 检查是否存在活跃退款申请(待审批、已通过、已退回状态) existing, err := s.refundStore.FindActiveByOrderID(ctx, req.OrderID) @@ -126,6 +131,7 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dt ShopID: shopID, ActualReceivedAmount: req.ActualReceivedAmount, RequestedRefundAmount: req.RequestedRefundAmount, + RefundVoucherKey: refundVoucherKey, RefundReason: req.RefundReason, Status: model.RefundStatusPending, } @@ -606,6 +612,14 @@ func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefund if req.RequestedRefundAmount != nil { requestedRefundAmount = *req.RequestedRefundAmount } + refundVoucherKey := refund.RefundVoucherKey + if req.RefundVoucherKey != nil { + refundVoucherKey = *req.RefundVoucherKey + } + refundVoucherKey, err = normalizeRefundVoucherKey(refundVoucherKey) + if err != nil { + return err + } order, err := s.orderStore.GetByID(ctx, refund.OrderID) if err != nil { @@ -627,6 +641,9 @@ func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefund if req.RequestedRefundAmount != nil { updates["requested_refund_amount"] = *req.RequestedRefundAmount } + if req.RefundVoucherKey != nil { + updates["refund_voucher_key"] = refundVoucherKey + } if req.RefundReason != nil { updates["refund_reason"] = *req.RefundReason } @@ -892,6 +909,17 @@ func validateApprovedRefundAmount(approvedAmount int64, requestedRefundAmount in return validateRequestedRefundAmountByOrder(approvedAmount, order) } +func normalizeRefundVoucherKey(voucherKey string) (string, error) { + normalized := strings.TrimSpace(voucherKey) + if normalized == "" { + return "", errors.New(errors.CodeInvalidParam, "退款申请必须上传退款凭证") + } + if len(normalized) > 500 { + return "", errors.New(errors.CodeInvalidParam, "退款凭证长度不能超过500") + } + return normalized, nil +} + // buildRefundResponse 将退款 Model 转换为 DTO 响应 func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse { assetType := "" @@ -916,6 +944,7 @@ func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse { ActualReceivedAmount: r.ActualReceivedAmount, RequestedRefundAmount: r.RequestedRefundAmount, ApprovedRefundAmount: r.ApprovedRefundAmount, + RefundVoucherKey: r.RefundVoucherKey, RefundReason: r.RefundReason, Status: r.Status, StatusName: constants.GetRefundStatusName(r.Status), diff --git a/migrations/000147_add_gateway_card_imei_to_iot_card.down.sql b/migrations/000147_add_gateway_card_imei_to_iot_card.down.sql new file mode 100644 index 0000000..87c9fd1 --- /dev/null +++ b/migrations/000147_add_gateway_card_imei_to_iot_card.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE tb_iot_card + DROP COLUMN IF EXISTS gateway_card_imei; diff --git a/migrations/000147_add_gateway_card_imei_to_iot_card.up.sql b/migrations/000147_add_gateway_card_imei_to_iot_card.up.sql new file mode 100644 index 0000000..32f4304 --- /dev/null +++ b/migrations/000147_add_gateway_card_imei_to_iot_card.up.sql @@ -0,0 +1,6 @@ +-- 为 tb_iot_card 新增 gateway_card_imei 字段 +-- 该 IMEI 来自 Gateway 卡状态接口,属于插拔卡业务,与设备自身 IMEI 无关,仅作数据同步落库 +ALTER TABLE tb_iot_card + ADD COLUMN IF NOT EXISTS gateway_card_imei VARCHAR(50) NOT NULL DEFAULT ''; + +COMMENT ON COLUMN tb_iot_card.gateway_card_imei IS '插拔卡业务 IMEI,网关卡状态接口同步,非设备 IMEI'; diff --git a/migrations/000148_add_refund_voucher_key_to_refund_request.down.sql b/migrations/000148_add_refund_voucher_key_to_refund_request.down.sql new file mode 100644 index 0000000..6a7d461 --- /dev/null +++ b/migrations/000148_add_refund_voucher_key_to_refund_request.down.sql @@ -0,0 +1,2 @@ +ALTER TABLE tb_refund_request + DROP COLUMN IF EXISTS refund_voucher_key; diff --git a/migrations/000148_add_refund_voucher_key_to_refund_request.up.sql b/migrations/000148_add_refund_voucher_key_to_refund_request.up.sql new file mode 100644 index 0000000..73db234 --- /dev/null +++ b/migrations/000148_add_refund_voucher_key_to_refund_request.up.sql @@ -0,0 +1,4 @@ +ALTER TABLE tb_refund_request + ADD COLUMN IF NOT EXISTS refund_voucher_key VARCHAR(500) NOT NULL DEFAULT ''; + +COMMENT ON COLUMN tb_refund_request.refund_voucher_key IS '退款凭证对象存储file_key';