feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕 - 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败 - 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影 - 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序 - 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列 - 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名 - 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
This commit is contained in:
@@ -152,6 +152,11 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 退款原因必填只在创建与重提用例校验:历史申请与补发历史审批路径不受约束、不追溯。
|
||||
refundReason := strings.TrimSpace(req.RefundReason)
|
||||
if refundReason == "" {
|
||||
return nil, errors.New(errors.CodeInvalidParam, "退款原因不能为空")
|
||||
}
|
||||
if err := validateRefundMethod(decision, req.Method); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -191,8 +196,11 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dt
|
||||
Method: req.Method,
|
||||
CustomerAccountInfo: req.CustomerAccountInfo,
|
||||
RefundVoucherKey: refundVoucherKey,
|
||||
RefundReason: req.RefundReason,
|
||||
Status: model.RefundStatusPending,
|
||||
RefundReason: refundReason,
|
||||
// 来源支付事实在创建时冻结:线下订单没有线上支付记录,两个字段以空值保存且不阻断申请创建。
|
||||
SourcePaymentNo: frozenSourcePaymentNo(decision.Payment),
|
||||
OriginalChannelTradeNo: frozenOriginalChannelTradeNo(decision.Payment),
|
||||
Status: model.RefundStatusPending,
|
||||
}
|
||||
refund.Creator = userID
|
||||
refund.Updater = userID
|
||||
@@ -203,6 +211,8 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateRefundRequest) (*dt
|
||||
result, err := s.refundApprovalCreation.Execute(ctx, refundapprovalapp.CreateCommand{
|
||||
Refund: refund, Order: order, SubmitterAccountID: userID,
|
||||
ChannelRefundRequestNo: buildChannelRefundRequestNo(refund.Method, decision.ChannelConfig),
|
||||
ApplicantRemark: applicantRemark(req.Remark),
|
||||
Material: s.loadRefundMaterial(ctx, refund, decision.Payment),
|
||||
})
|
||||
if err != nil {
|
||||
failedRefund := *refund
|
||||
@@ -290,7 +300,9 @@ func (s *Service) TriggerApproval(ctx context.Context, id uint) (*dto.RefundResp
|
||||
if err != nil {
|
||||
return nil, errors.New(errors.CodeNotFound, "退款申请不存在")
|
||||
}
|
||||
result, err := s.refundApprovalCreation.TriggerHistorical(ctx, refund.ID)
|
||||
// 补发不追溯退款原因必填,但仍按既有展示口径解析审批材料,使新增控件的材料在补发路径同样完整。
|
||||
payment, _ := s.loadPaidPackagePayment(ctx, refund.OrderID)
|
||||
result, err := s.refundApprovalCreation.TriggerHistorical(ctx, refund.ID, s.loadRefundMaterial(ctx, refund, payment))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -832,6 +844,13 @@ func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefund
|
||||
if req.RefundReason != nil {
|
||||
refundReason = *req.RefundReason
|
||||
}
|
||||
// 重提同样强制退款原因必填(去空白后非空);不填时沿用原有原因,原有原因为空的存量单不允许重提。
|
||||
refundReason = strings.TrimSpace(refundReason)
|
||||
if refundReason == "" {
|
||||
s.recordRefundFailure(ctx, constants.AuditActionRefundResubmitted, "重新提交退款申请失败", refund, order,
|
||||
errors.New(errors.CodeInvalidParam, "退款原因不能为空"))
|
||||
return errors.New(errors.CodeInvalidParam, "退款原因不能为空")
|
||||
}
|
||||
customerAccountInfo := refund.CustomerAccountInfo
|
||||
if req.CustomerAccountInfo != nil {
|
||||
customerAccountInfo = *req.CustomerAccountInfo
|
||||
@@ -840,8 +859,14 @@ func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefund
|
||||
s.recordRefundFailure(ctx, constants.AuditActionRefundResubmitted, "重新提交退款申请失败", refund, nil, err)
|
||||
return err
|
||||
}
|
||||
// 申请人备注缺省沿用该退款单最近一次尝试冻结的备注,填写时替换;两者都只影响本次新增的尝试快照。
|
||||
remark := s.latestAttemptRemark(ctx, refund)
|
||||
if req.Remark != nil {
|
||||
remark = applicantRemark(req.Remark)
|
||||
}
|
||||
|
||||
// 重提的实收金额重新派生并冻结;提交人传入的实收金额一律忽略。
|
||||
// 来源支付事实随本次重提一并重新冻结,与重新派生的冻结实收金额保持同一事实来源。
|
||||
updated := *refund
|
||||
updated.Method = method
|
||||
updated.RequestedRefundAmount = requestedRefundAmount
|
||||
@@ -850,6 +875,8 @@ func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefund
|
||||
updated.RefundVoucherKey = refundVoucherKey
|
||||
updated.RefundReason = refundReason
|
||||
updated.CustomerAccountInfo = customerAccountInfo
|
||||
updated.SourcePaymentNo = frozenSourcePaymentNo(decision.Payment)
|
||||
updated.OriginalChannelTradeNo = frozenOriginalChannelTradeNo(decision.Payment)
|
||||
updated.Creator = userID
|
||||
|
||||
attemptChannelRefundRequestNo := buildChannelRefundRequestNo(method, decision.ChannelConfig)
|
||||
@@ -858,6 +885,8 @@ func (s *Service) Resubmit(ctx context.Context, id uint, req *dto.ResubmitRefund
|
||||
}
|
||||
if _, err := s.refundApprovalCreation.Resubmit(ctx, id, refundapprovalapp.ResubmitCommand{
|
||||
Refund: &updated, ChannelRefundRequestNo: attemptChannelRefundRequestNo,
|
||||
ApplicantRemark: remark,
|
||||
Material: s.loadRefundMaterial(ctx, &updated, decision.Payment),
|
||||
}); err != nil {
|
||||
s.recordRefundFailure(ctx, constants.AuditActionRefundResubmitted, "重新提交退款申请失败", refund, order, err)
|
||||
return err
|
||||
@@ -1268,20 +1297,13 @@ func normalizeRefundVoucherKey(keys []string) (model.StringJSONBArray, error) {
|
||||
|
||||
// buildRefundResponse 将退款 Model 转换为 DTO 响应
|
||||
func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
|
||||
assetType := ""
|
||||
if r.OrderType == model.OrderTypeSingleCard {
|
||||
assetType = "card"
|
||||
} else if r.OrderType == model.OrderTypeDevice {
|
||||
assetType = "device"
|
||||
}
|
||||
|
||||
resp := &dto.RefundResponse{
|
||||
ID: r.ID,
|
||||
RefundNo: r.RefundNo,
|
||||
OrderID: r.OrderID,
|
||||
OrderNo: r.OrderNo,
|
||||
AssetIdentifier: r.AssetIdentifier,
|
||||
AssetType: assetType,
|
||||
AssetType: refundAssetType(r.OrderType),
|
||||
IotCardID: r.IotCardID,
|
||||
DeviceID: r.DeviceID,
|
||||
PackageUsageID: r.PackageUsageID,
|
||||
@@ -1303,6 +1325,10 @@ func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
|
||||
ChannelRefundNo: r.ChannelRefundNo,
|
||||
ChannelRefundRequestNo: r.ChannelRefundRequestNo,
|
||||
ChannelRefundAmount: r.ChannelRefundAmount,
|
||||
SourcePaymentNo: r.SourcePaymentNo,
|
||||
OriginalChannelTradeNo: r.OriginalChannelTradeNo,
|
||||
OfflineSettlementNo: r.OfflineSettlementNo,
|
||||
OfflineSettledBy: r.OfflineSettledBy,
|
||||
FailureReason: r.FailureReason,
|
||||
FailureReasonName: constants.RefundFailureReasonName(r.FailureReason),
|
||||
FailureMessage: r.FailureMessage,
|
||||
@@ -1330,6 +1356,9 @@ func buildRefundResponse(r *model.RefundRequest) *dto.RefundResponse {
|
||||
if r.ChannelRefundedAt != nil {
|
||||
resp.ChannelRefundedAt = r.ChannelRefundedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
if r.OfflineSettledAt != nil {
|
||||
resp.OfflineSettledAt = r.OfflineSettledAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user