feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
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:
2026-09-18 15:34:29 +08:00
parent 5e78809b93
commit 5ed6b39deb
142 changed files with 7878 additions and 964 deletions

View File

@@ -288,9 +288,62 @@ func validateSceneMapping(businessType string, mapping []dto.WeComControlMapping
}
}
}
// 本变更新增字段必须配置映射:缺失即明确失败,避免审批材料被静默丢弃后审批人看不到关键依据。
// 既有未映射的可选控件不受影响,保持既有静默跳过行为。
wanted, ok := sceneBusinessFields(businessType)
if !ok {
return errors.New(errors.CodeInvalidParam, "企业微信审批业务类型无效")
}
for _, field := range wanted {
if !field.RequiredMapping {
continue
}
if _, exists := businessFields[field.Code]; !exists {
return errors.New(errors.CodeInvalidParam, "企业微信场景缺少必须映射的业务字段: "+field.Name)
}
}
return nil
}
// RequiredSceneField 描述本变更新增、必须配置控件映射的业务字段。
type RequiredSceneField struct {
// Code 是 control_mapping.business_field 应填写的稳定字段编码。
Code string
// Name 是字段中文名称,用于在缺失时指出具体缺哪个字段/控件。
Name string
}
// RequiredSceneFields 返回指定业务类型下必须配置控件映射的业务字段,顺序与场景白名单一致。
//
// 这是「必须映射字段」的唯一判定来源:场景映射校验(保存时)与表单构建(提交时)都从
// 这里取集合MUST NOT 再维护第二份清单。既有未标注必须映射的可选字段不在返回结果中,
// 因此其缺失映射仍保持既有静默跳过行为。
func RequiredSceneFields(businessType string) []RequiredSceneField {
fields, ok := sceneBusinessFields(businessType)
if !ok {
return nil
}
result := make([]RequiredSceneField, 0, len(fields))
for _, field := range fields {
if field.RequiredMapping {
result = append(result, RequiredSceneField{Code: field.Code, Name: field.Name})
}
}
return result
}
// approvalBusinessTypes 返回全部已登记审批业务场景,供只读字段汇总复用。
func approvalBusinessTypes() []string {
return []string{
constants.ApprovalBusinessTypeRefund,
constants.ApprovalBusinessTypeOfflineRecharge,
constants.ApprovalBusinessTypeEmployeeCollection,
constants.ApprovalBusinessTypeAgentDistribution,
constants.ApprovalBusinessTypeWithdrawalQualification,
constants.ApprovalBusinessTypeCommissionWithdrawal,
}
}
func allowedSceneBusinessField(businessType, businessField string) bool {
fields, ok := sceneBusinessFields(businessType)
if !ok {
@@ -334,6 +387,13 @@ func sceneBusinessFields(businessType string) ([]dto.WeComBusinessFieldResponse,
{Code: constants.ApprovalFieldRefundVoucherKey, Name: "退款凭证", ValueType: constants.ApprovalFieldValueTypeFileList, Description: "提交时上传到企微文件控件的退款凭证列表"},
{Code: constants.ApprovalFieldRefundReason, Name: "退款原因", ValueType: constants.ApprovalFieldValueTypeString, Description: "业务提交人填写的退款原因"},
{Code: constants.ApprovalFieldPackageUsageID, Name: "套餐使用记录 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "退款关联套餐使用记录 ID无关联记录时可能为空"},
{Code: constants.ApprovalFieldRefundAssetType, Name: "资产类型", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款关联资产类型card 单卡、device 设备;沿用退款申请的下单快照推导", RequiredMapping: true},
{Code: constants.ApprovalFieldRefundDeviceType, Name: "设备类型", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款申请关联设备的类型;资产不是设备或无关联设备时为空字符串", RequiredMapping: true},
{Code: constants.ApprovalFieldRefundDeviceModel, Name: "设备型号", ValueType: constants.ApprovalFieldValueTypeString, Description: "退款申请关联设备的型号;资产不是设备或无关联设备时为空字符串", RequiredMapping: true},
{Code: constants.ApprovalFieldRefundPackageUsedMB, Name: "当前退款套餐已用量MB", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "当前退款套餐已用量MB真实流量解析不到套餐时为 0", RequiredMapping: true},
{Code: constants.ApprovalFieldRefundPackageTotalMB, Name: "当前退款套餐总量MB", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "当前退款套餐总量MB真实流量解析不到套餐时为 0", RequiredMapping: true},
{Code: constants.ApprovalFieldRefundChannelTradeNo, Name: "原支付渠道交易流水号", ValueType: constants.ApprovalFieldValueTypeString, Description: "创建申请时冻结的原成功支付记录渠道交易流水号;线下订单为空字符串", RequiredMapping: true},
{Code: constants.ApprovalFieldRefundApplicantRemark, Name: "申请人备注", ValueType: constants.ApprovalFieldValueTypeString, Description: "本次提交填写的申请人备注快照;未填写时为空字符串。与审批备注不是同一字段", RequiredMapping: true},
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
}, true
@@ -364,6 +424,8 @@ func sceneBusinessFields(businessType string) ([]dto.WeComBusinessFieldResponse,
{Code: constants.ApprovalFieldDistributionPhoneMasked, Name: "注册手机号", ValueType: constants.ApprovalFieldValueTypeString, Description: "脱敏后的注册手机号,禁止写入完整手机号"},
{Code: constants.ApprovalFieldDistributionContactName, Name: "联系人姓名", ValueType: constants.ApprovalFieldValueTypeString, Description: "扫码注册填写的联系人姓名"},
{Code: constants.ApprovalFieldDistributionRegion, Name: "注册地址摘要", ValueType: constants.ApprovalFieldValueTypeString, Description: "省市区与详细地址拼接的注册地址摘要"},
{Code: constants.ApprovalFieldDistributionBusinessOwner, Name: "业务员", ValueType: constants.ApprovalFieldValueTypeString, Description: "上级代理店铺当前启用业务员的名称快照,用于确认将同步给新代理的初始业务员;上级无业务员时为空字符串", RequiredMapping: true},
{Code: constants.ApprovalFieldDistributionBusinessOwnerAbsence, Name: "无业务员标记", ValueType: constants.ApprovalFieldValueTypeString, Description: "上级店铺当前无业务员时提交「无」有业务员时为空字符串MUST NOT 用提交人或上级主账号填充", RequiredMapping: true},
{Code: constants.ApprovalFieldSubmitterID, Name: "提交人账号 ID", ValueType: constants.ApprovalFieldValueTypeInteger, Description: "本系统真实业务提交人账号 ID"},
{Code: constants.ApprovalFieldSubmitterName, Name: "提交人名称", ValueType: constants.ApprovalFieldValueTypeString, Description: "本系统真实业务提交人名称快照"},
}, true
@@ -421,17 +483,12 @@ func normalizeSceneMapping(mapping []dto.WeComControlMappingItem) []dto.WeComCon
}
func validApprovalBusinessType(businessType string) bool {
switch businessType {
case constants.ApprovalBusinessTypeRefund,
constants.ApprovalBusinessTypeOfflineRecharge,
constants.ApprovalBusinessTypeEmployeeCollection,
constants.ApprovalBusinessTypeAgentDistribution,
constants.ApprovalBusinessTypeWithdrawalQualification,
constants.ApprovalBusinessTypeCommissionWithdrawal:
return true
default:
return false
for _, candidate := range approvalBusinessTypes() {
if candidate == businessType {
return true
}
}
return false
}
func approvalBusinessTypeName(businessType string) string {