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:
@@ -276,6 +276,10 @@ func NewRegistry() *Registry {
|
||||
pollingPriorityDequeued := pollingPriorityAction(constants.AuditActionPollingPriorityDequeued, "卡轮询优先项失败出队", constants.AuditRiskNormal, true)
|
||||
// 人工优先入队的拒绝只可能来自后台账号入口,不登记 Worker 侧来源。
|
||||
pollingPriorityManualDenied := pollingPriorityAction(constants.AuditActionPollingPriorityManualDenied, "拒绝人工优先入队", constants.AuditRiskNormal, false)
|
||||
// 人工关闭与人工重触发只可能来自后台账号入口;超期处理只可能来自计划任务,两者都必须分别声明来源。
|
||||
pollingPriorityClosed := pollingPriorityAction(constants.AuditActionPollingPriorityClosed, "人工关闭卡轮询优先项", constants.AuditRiskNormal, false)
|
||||
pollingPriorityExpired := pollingPriorityAction(constants.AuditActionPollingPriorityExpired, "卡轮询优先项有效期到期出队", constants.AuditRiskNormal, true)
|
||||
pollingPriorityRetriggered := pollingPriorityAction(constants.AuditActionPollingPriorityRetriggered, "人工重触发卡轮询优先项", constants.AuditRiskNormal, false)
|
||||
wecomCredentialsRead := ActionDefinition{
|
||||
Code: constants.AuditActionWeComCredentialsRead, Name: "读取企业微信应用明文凭据",
|
||||
Category: constants.AuditCategorySecurity, Risk: constants.AuditRiskHigh,
|
||||
@@ -359,6 +363,8 @@ func NewRegistry() *Registry {
|
||||
// 渠道原路退款的调用与恢复都由 Worker 触发,与既有的退款系统动作入口一致。
|
||||
refundChannelCalled := refundSystemAction(constants.AuditActionRefundChannelCalled, "发起渠道原路退款")
|
||||
refundChannelRecovered := refundSystemAction(constants.AuditActionRefundChannelRecovered, "恢复渠道原路退款结果")
|
||||
// 线下退款处理流水号补录与更正由后台授权账号发起,与既有退款后台动作入口一致。
|
||||
refundOfflineSettled := refundAction(constants.AuditActionRefundOfflineSettled, "登记线下退款处理流水号", false)
|
||||
approvalRequested := approvalAction(constants.AuditActionApprovalRequested, "提交通用审批申请", []ActionOrigin{
|
||||
{Actor: constants.AuditActorAccount, Source: constants.AuditSourceAdminAPI},
|
||||
})
|
||||
@@ -659,6 +665,9 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionPollingPriorityCompleted: pollingPriorityCompleted,
|
||||
constants.AuditActionPollingPriorityDequeued: pollingPriorityDequeued,
|
||||
constants.AuditActionPollingPriorityManualDenied: pollingPriorityManualDenied,
|
||||
constants.AuditActionPollingPriorityClosed: pollingPriorityClosed,
|
||||
constants.AuditActionPollingPriorityExpired: pollingPriorityExpired,
|
||||
constants.AuditActionPollingPriorityRetriggered: pollingPriorityRetriggered,
|
||||
constants.AuditActionWeComCredentialsRead: wecomCredentialsRead,
|
||||
constants.AuditActionRoleCreated: roleCreated,
|
||||
constants.AuditActionRoleUpdated: roleUpdated,
|
||||
@@ -734,6 +743,7 @@ func NewRegistry() *Registry {
|
||||
constants.AuditActionRefundAnomalyFlagged: refundAnomalyFlagged,
|
||||
constants.AuditActionRefundChannelCalled: refundChannelCalled,
|
||||
constants.AuditActionRefundChannelRecovered: refundChannelRecovered,
|
||||
constants.AuditActionRefundOfflineSettled: refundOfflineSettled,
|
||||
constants.AuditActionApprovalRequested: approvalRequested,
|
||||
constants.AuditActionApprovalSubmissionSynced: approvalSubmissionSynced,
|
||||
constants.AuditActionApprovalSubmissionRecovered: approvalSubmissionRecovered,
|
||||
|
||||
@@ -65,7 +65,7 @@ func (w *PriorityEventWriter) AppendPriorityRequested(ctx context.Context, tx *g
|
||||
if event.ResourceType != constants.AssetTypeIotCard && event.ResourceType != constants.AssetTypeDevice {
|
||||
return errors.New(errors.CodeInvalidParam, "优先轮询事件资源类型不受支持")
|
||||
}
|
||||
// 人工入队不经事件通道:manual_trigger 由 internal/service/polling/priority_enqueue_service.go 直写事实表,
|
||||
// 人工入队不经事件通道:manual_trigger 由 internal/service/polling/priority_manual_service.go 直写事实表,
|
||||
// 事件通道没有操作者,无法满足事实表 CHECK (trigger_type <> 'manual_trigger' OR manual_operator_id > 0)。
|
||||
// 权威约束在事实表 CHECK,这里只是把「DB 23514 → 整个投递事务回滚」提前为可定位的参数错误,
|
||||
// 避免调用方业务事务被数据库层失败牵连回滚。
|
||||
@@ -170,9 +170,14 @@ func (c *PriorityRequestedConsumer) Consume(ctx context.Context, envelope outbox
|
||||
return lookupErr
|
||||
}
|
||||
store := c.store.WithTx(tx)
|
||||
assetSnapshots, assetErr := store.AssetSnapshots(ctx, cardIDs)
|
||||
if assetErr != nil {
|
||||
return assetErr
|
||||
}
|
||||
createdCount, mergedCount = 0, 0
|
||||
for _, cardID := range cardIDs {
|
||||
for _, taskType := range taskTypes {
|
||||
asset := assetSnapshots[cardID]
|
||||
item := &model.PollingPriorityItem{
|
||||
CardID: cardID,
|
||||
TaskType: taskType,
|
||||
@@ -180,6 +185,10 @@ func (c *PriorityRequestedConsumer) Consume(ctx context.Context, envelope outbox
|
||||
SourceOrderID: optionalUintPointer(event.SourceOrderID),
|
||||
SourcePackageUsageID: optionalUintPointer(event.SourcePackageUsageID),
|
||||
ShopIDSnapshot: shopSnapshots[cardID],
|
||||
AssetType: asset.AssetType,
|
||||
AssetID: optionalUintPointer(asset.AssetID),
|
||||
DeviceNoSnapshot: asset.DeviceNo,
|
||||
AgentShopIDSnapshot: asset.AgentShopID,
|
||||
LastTriggeredAt: event.OccurredAt,
|
||||
}
|
||||
created, insertErr := store.InsertOrMerge(ctx, item)
|
||||
|
||||
@@ -34,7 +34,7 @@ func (e *attachmentPreparationError) Unwrap() error {
|
||||
}
|
||||
|
||||
// buildApprovalForm 按后台已校验映射将渠道无关业务快照组装为企微控件值。
|
||||
func buildApprovalForm(ctx context.Context, applicationID, instanceID uint, mappingJSON, templateJSON, snapshotJSON []byte, attachments approvalAttachmentPort) (approvalFormBuildResult, error) {
|
||||
func buildApprovalForm(ctx context.Context, businessType string, applicationID, instanceID uint, mappingJSON, templateJSON, snapshotJSON []byte, attachments approvalAttachmentPort) (approvalFormBuildResult, error) {
|
||||
var mappings []dto.WeComControlMappingItem
|
||||
if err := sonic.Unmarshal(mappingJSON, &mappings); err != nil || len(mappings) == 0 {
|
||||
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "企业微信审批控件映射无效")
|
||||
@@ -51,6 +51,33 @@ func buildApprovalForm(ctx context.Context, applicationID, instanceID uint, mapp
|
||||
for _, control := range template.Controls {
|
||||
requiredControls[control.ID] = control.Required
|
||||
}
|
||||
// 本变更新增字段必须配置控件映射:场景里没有该字段的映射条目时,下面的遍历永远不会看到它,
|
||||
// 该字段会被静默丢弃。因此在组装前先针对本次提交使用的映射逐一确认必须字段有条目,
|
||||
// 缺失即明确失败并指出缺哪个字段/控件;本判定与场景映射校验共用同一份 RequiredSceneFields。
|
||||
// 既有未标注必须映射的可选字段不在其中,缺失映射时保持既有静默跳过行为。
|
||||
mappingFields := make(map[string]struct{}, len(mappings))
|
||||
for _, item := range mappings {
|
||||
mappingFields[strings.TrimSpace(item.BusinessField)] = struct{}{}
|
||||
}
|
||||
for _, field := range wecomapp.RequiredSceneFields(businessType) {
|
||||
if _, exists := mappingFields[field.Code]; !exists {
|
||||
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus,
|
||||
"企业微信审批场景缺少必须配置的控件映射: "+field.Name+"(业务字段 "+field.Code+"),请先配置该控件映射后再提交")
|
||||
}
|
||||
}
|
||||
// 本变更新增字段缺失快照值时同样明确失败:映射已配置却拿不到值,静默跳过会让审批材料
|
||||
// 与本地事实不一致。该校验必须在循环之前完成——循环内的既有文件类控件会经
|
||||
// attachments.Upload 触达外部系统,若把校验留在循环内,失败可能晚于外部调用(ENG-TX-001
|
||||
// 要求事务内不得持有不可回滚的长外部 I/O,同理失败必须发生在任何外呼之前)。
|
||||
// 既有未映射(或映射到既有可选字段)的控件保持既有静默跳过行为。
|
||||
requiredFields := make(map[string]string)
|
||||
for _, field := range wecomapp.RequiredSceneFields(businessType) {
|
||||
requiredFields[field.Code] = field.Name
|
||||
if _, exists := lookupSnapshotValue(snapshot, field.Code); !exists {
|
||||
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus,
|
||||
"审批业务快照缺少必须提交的字段: "+field.Name+"(业务字段 "+field.Code+"),请确认已配置对应控件与业务字段映射")
|
||||
}
|
||||
}
|
||||
contents := make([]map[string]any, 0, len(mappings))
|
||||
attachmentCount := 0
|
||||
for _, mapping := range mappings {
|
||||
@@ -59,6 +86,9 @@ func buildApprovalForm(ctx context.Context, applicationID, instanceID uint, mapp
|
||||
if requiredControls[mapping.ControlID] {
|
||||
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少模板必填字段: "+mapping.BusinessField)
|
||||
}
|
||||
if name, required := requiredFields[strings.TrimSpace(mapping.BusinessField)]; required {
|
||||
return approvalFormBuildResult{}, errors.New(errors.CodeInvalidStatus, "审批业务快照缺少必须提交的字段: "+name+",请确认已配置对应控件与业务字段映射")
|
||||
}
|
||||
continue
|
||||
}
|
||||
value, count, err := buildControlValue(ctx, applicationID, instanceID, mapping, raw, attachments)
|
||||
|
||||
@@ -48,7 +48,7 @@ func (c *ApprovalSubmissionConsumer) Consume(ctx context.Context, envelope outbo
|
||||
return nil
|
||||
}
|
||||
form, err := buildApprovalForm(
|
||||
ctx, record.Context.ApplicationID, event.InstanceID,
|
||||
ctx, record.Context.BusinessType, record.Context.ApplicationID, event.InstanceID,
|
||||
record.Context.ControlMapping, record.Context.TemplateSnapshot, record.Instance.RequestSnapshot, c.attachments,
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user