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

@@ -63,7 +63,8 @@ func (s *Service) Unbind(ctx context.Context, request *dto.UnbindPhoneAssetAssoc
return err
}
affected, err := s.associationStore.WithTx(tx).InvalidateByIDs(ctx, tx, []uint{locked.ID},
constants.PhoneAssetAssociationInvalidateMethodBackendSingle, request.Reason, operatorID, now)
constants.PhoneAssetAssociationInvalidateMethodBackendSingle, request.Reason, operatorID,
middleware.GetUsernameFromContext(ctx), now)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解除手机号资产关联失败")
}
@@ -145,7 +146,8 @@ func (s *Service) unbindAsset(ctx context.Context, asset dto.BatchUnbindAssetIte
ids = append(ids, row.ID)
}
affected, err := s.associationStore.WithTx(tx).InvalidateByIDs(ctx, tx, ids,
constants.PhoneAssetAssociationInvalidateMethodBackendBatch, reason, operatorID, now)
constants.PhoneAssetAssociationInvalidateMethodBackendBatch, reason, operatorID,
middleware.GetUsernameFromContext(ctx), now)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "解除手机号资产关联失败")
}
@@ -320,6 +322,11 @@ func (s *Service) List(ctx context.Context, request *dto.ListPhoneAssetAssociati
if err != nil {
return nil, err
}
// 当前有效关联数量必须与「最多关联十项」判定同口径,且按当页手机号集合一次聚合,禁止逐行查询。
validCounts, err := s.loadValidAssociationCounts(ctx, rows)
if err != nil {
return nil, err
}
items := make([]*dto.PhoneAssetAssociationResponse, 0, len(rows))
for _, row := range rows {
item := &dto.PhoneAssetAssociationResponse{
@@ -327,18 +334,40 @@ func (s *Service) List(ctx context.Context, request *dto.ListPhoneAssetAssociati
AssetIdentifier: identifiers[assetIdentifierKey(row.AssetType, row.AssetID)],
Status: row.Status, StatusName: constants.GetPhoneAssetAssociationStatusName(row.Status),
Source: row.Source, EstablishedAt: row.EstablishedAt.Format(time.RFC3339),
ValidAssociationCount: validCounts[row.Phone],
}
if row.InvalidatedAt != nil {
item.InvalidatedAt = row.InvalidatedAt.Format(time.RFC3339)
item.InvalidationMethod = row.InvalidationMethod
item.InvalidationMethodName = constants.PhoneAssetAssociationInvalidateMethodName(row.InvalidationMethod)
item.InvalidationReason = row.InvalidationReason
// 最近解绑人只取解绑当时的名称快照与账号标识:有效关系保持为空,不以创建人或更新时间填充。
item.InvalidatorID = row.Invalidator
item.InvalidatorName = row.InvalidatorNameSnapshot
}
items = append(items, item)
}
return &dto.PhoneAssetAssociationPageResult{Items: items, Total: total, Page: page, Size: pageSize}, nil
}
// loadValidAssociationCounts 按当页涉及的手机号集合一次聚合当前有效关联数量,禁止逐手机号查询。
func (s *Service) loadValidAssociationCounts(ctx context.Context, rows []*model.PhoneAssetAssociation) (map[string]int64, error) {
phones := make([]string, 0, len(rows))
seen := make(map[string]struct{}, len(rows))
for _, row := range rows {
if _, ok := seen[row.Phone]; ok {
continue
}
seen[row.Phone] = struct{}{}
phones = append(phones, row.Phone)
}
counts, err := s.associationStore.CountValidByPhones(ctx, phones)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "批量统计手机号有效关联数量失败")
}
return counts, nil
}
// loadAssetIdentifiers 按资产类型各一次 IN 批量读取资产当前标识,禁止逐行查询。
func (s *Service) loadAssetIdentifiers(ctx context.Context, rows []*model.PhoneAssetAssociation) (map[string]string, error) {
result := make(map[string]string, len(rows))