fix: 补上第1批 M8 提交失败决策写入(6298a906 遗漏)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m35s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 10m35s
- 企微提交被明确拒绝时,在同一事务内写 submission_failed 决策投递记录与终态事件
- Worker 两处 ApprovalContextRepository 注入决策写入器;未注入时提交失败直接报错,不再静默只改实例状态
- 终态决策消费入口单独放行 submission_failed,渠道回调仍不能推进到提交失败
- 6298a906 的编辑脚本在本文件锚点不唯一处中断,三处改动未落盘;本次补齐
- 第1批 ⑤ 5.4、① 5.2/5.4/5.5 测试环境验证完成
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -423,7 +423,9 @@ func registerWeComApprovalOutboxConsumer(runtime *workerRuntime, cfg *config.Con
|
||||
cfg.WeCom.BaseURL, cfg.WeCom.Timeout, appLogger,
|
||||
)
|
||||
consumer := wecomInfra.NewApprovalSubmissionConsumer(
|
||||
wecomInfra.NewApprovalContextRepository(runtime.db, auditWriter),
|
||||
wecomInfra.NewApprovalContextRepository(runtime.db, auditWriter).SetDecisionWriters(
|
||||
approvalInfra.NewTerminalEventWriter(outbox.NewRepository()), approvalInfra.NewDecisionDeliveryStore(runtime.db),
|
||||
),
|
||||
wecomInfra.NewApprovalSubmissionClient(tokenProvider, integrationRepository, cfg.WeCom.BaseURL, cfg.WeCom.Timeout),
|
||||
wecomInfra.NewApprovalAttachmentUploader(tokenProvider, integrationRepository, runtime.storageSvc, cfg.WeCom.BaseURL, cfg.WeCom.Timeout),
|
||||
)
|
||||
@@ -542,7 +544,9 @@ func registerWeComApprovalTasks(mux *asynq.ServeMux, runtime *workerRuntime, cfg
|
||||
approvalInfra.NewDecisionDeliveryStore(runtime.db), nil,
|
||||
)
|
||||
decisionSync.SetAuditWriter(auditWriter)
|
||||
contexts := wecomInfra.NewApprovalContextRepository(runtime.db, auditWriter)
|
||||
contexts := wecomInfra.NewApprovalContextRepository(runtime.db, auditWriter).SetDecisionWriters(
|
||||
approvalInfra.NewTerminalEventWriter(outbox.NewRepository()), approvalInfra.NewDecisionDeliveryStore(runtime.db),
|
||||
)
|
||||
detailHandler := wecomInfra.NewApprovalDetailTaskHandler(
|
||||
wecomInfra.NewApprovalDetailClient(tokenProvider, integrationRepository, cfg.WeCom.BaseURL, cfg.WeCom.Timeout),
|
||||
contexts, decisionSync, integrationRepository,
|
||||
|
||||
@@ -66,8 +66,11 @@ func (c *TerminalDecisionConsumer) Consume(ctx context.Context, envelope outbox.
|
||||
event.BusinessType == "" || event.BusinessID == 0 || event.CorrelationID == "" {
|
||||
return errors.New(errors.CodeInvalidParam, "审批标准决策事件载荷不完整")
|
||||
}
|
||||
// submission_failed 只由审批核心在提交失败事务内发出,仅在此放行;StatusForDecision 不认该值,渠道回调仍不能推进到提交失败。
|
||||
if event.Decision != constants.ApprovalDecisionSubmissionFailed {
|
||||
if _, err := approvaldomain.StatusForDecision(event.Decision); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.dispatcher.Consume(ctx, event)
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ type ApprovalRecoveryRecord struct {
|
||||
type ApprovalContextRepository struct {
|
||||
db *gorm.DB
|
||||
audit approvalapp.AuditWriter
|
||||
eventWriter approvalapp.TerminalEventWriter
|
||||
deliveryWriter approvalapp.DecisionDeliveryWriter
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
@@ -50,6 +52,14 @@ func NewApprovalContextRepository(db *gorm.DB, audits ...approvalapp.AuditWriter
|
||||
return &ApprovalContextRepository{db: db, audit: audit, now: time.Now}
|
||||
}
|
||||
|
||||
// SetDecisionWriters 注入提交失败决策所需的投递记录与终态事件写入器。
|
||||
// 提交被明确拒绝时必须在同一事务内写两者,缺任何一个都会让业务单永远停在审批中。
|
||||
func (r *ApprovalContextRepository) SetDecisionWriters(eventWriter approvalapp.TerminalEventWriter, deliveryWriter approvalapp.DecisionDeliveryWriter) *ApprovalContextRepository {
|
||||
r.eventWriter = eventWriter
|
||||
r.deliveryWriter = deliveryWriter
|
||||
return r
|
||||
}
|
||||
|
||||
// ClaimSubmission 将待提交上下文原子置为请求处理中,阻止并发或重投重复提单。
|
||||
func (r *ApprovalContextRepository) ClaimSubmission(ctx context.Context, instanceID uint) (*ApprovalSubmissionRecord, bool, error) {
|
||||
if r == nil || r.db == nil || instanceID == 0 {
|
||||
@@ -467,6 +477,10 @@ func (r *ApprovalContextRepository) markSubmissionState(ctx context.Context, ins
|
||||
if r.audit == nil {
|
||||
return errors.New(errors.CodeServiceUnavailable, "通用审批审计 Writer 未配置")
|
||||
}
|
||||
failed := approvalStatus == constants.ApprovalStatusSubmissionFailed
|
||||
if failed && (r.eventWriter == nil || r.deliveryWriter == nil) {
|
||||
return errors.New(errors.CodeServiceUnavailable, "审批提交失败决策写入器未配置")
|
||||
}
|
||||
now := r.now().UTC()
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
contextResult := tx.Model(&model.WeComApprovalContext{}).
|
||||
@@ -494,6 +508,12 @@ func (r *ApprovalContextRepository) markSubmissionState(ctx context.Context, ins
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outboxEventID := ""
|
||||
if failed {
|
||||
if outboxEventID, err = r.appendSubmissionFailedDecision(ctx, tx, instance); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
beforeStatus := constants.ApprovalStatusSubmitting
|
||||
return r.audit.WriteApproval(ctx, tx, approvalapp.AuditChange{
|
||||
EventID: approvalSubmissionAuditEventID(instanceID, approvalStatus),
|
||||
@@ -504,11 +524,30 @@ func (r *ApprovalContextRepository) markSubmissionState(ctx context.Context, ins
|
||||
CorrelationID: instance.CorrelationID, BeforeStatus: &beforeStatus, AfterStatus: &approvalStatus,
|
||||
ActorKind: constants.AuditActorSystemTask, ActorID: constants.ApprovalAuditActorSubmissionWorker,
|
||||
Source: constants.AuditSourceWorker, Result: auditResult, ErrorSummary: message,
|
||||
IntegrationIDs: []string{integrationID},
|
||||
IntegrationIDs: []string{integrationID}, OutboxEventID: outboxEventID,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// appendSubmissionFailedDecision 在提交失败事务内写决策投递记录与终态事件,通知业务模块终止业务单。
|
||||
// 事件号沿用终态决策格式 approval:{实例ID}:submission_failed;失败原因由业务消费者按实例读取企微上下文。
|
||||
func (r *ApprovalContextRepository) appendSubmissionFailedDecision(ctx context.Context, tx *gorm.DB, instance *model.ApprovalInstance) (string, error) {
|
||||
event := approvalapp.TerminalDecisionEvent{
|
||||
EventID: "approval:" + strconv.FormatUint(uint64(instance.ID), 10) + ":" + constants.ApprovalDecisionSubmissionFailed,
|
||||
InstanceID: instance.ID, BusinessType: instance.BusinessType, BusinessID: instance.BusinessID,
|
||||
SubmitterAccountID: instance.SubmitterAccountID, Decision: constants.ApprovalDecisionSubmissionFailed,
|
||||
Source: constants.ApprovalSyncSourceSubmission, CorrelationID: instance.CorrelationID,
|
||||
OccurredAt: instance.StatusChangedAt,
|
||||
}
|
||||
if err := r.deliveryWriter.Create(ctx, tx, event); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := r.eventWriter.Append(ctx, tx, event); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return event.EventID, nil
|
||||
}
|
||||
|
||||
func loadApprovalAuditInstance(ctx context.Context, tx *gorm.DB, instanceID uint) (*model.ApprovalInstance, error) {
|
||||
var instance model.ApprovalInstance
|
||||
if err := tx.WithContext(ctx).First(&instance, instanceID).Error; err != nil {
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
- [x] 5.1 `gofmt`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`。
|
||||
- [x] 5.2 按 ENG-TEST-001(测试库 `junhong_cmp_test`、Redis DB 6):修改密码接口验证四种提示且失败不改密码;七个入口各用 `Abc12345` 成功、`abcd1234` 返回 `1016`、7 位与 13 位返回 `1001`、含空格返回 `1016`;用规则上线前的 6 位旧密码账号登录照常成功。只使用本 Change 自建的测试账号、企业与店铺,验证后清理。
|
||||
- [ ] 5.3 通知测试环境前端同步七个入口的密码提示与前端校验(8~12 位、四类至少三类)。
|
||||
- [ ] 5.4 测试环境为订单、IoT 卡、设备各建一个导出任务,下载核对新表头、值来源、MB/GB 换算与列数;再用变更前创建的任务验证旧表头与行宽不变;只清理本 Change 自建的任务与对象。
|
||||
- [x] 5.4 测试环境为订单、IoT 卡、设备各建一个导出任务,下载核对新表头、值来源、MB/GB 换算与列数;再用变更前创建的任务验证旧表头与行宽不变;只清理本 Change 自建的任务与对象。
|
||||
- [ ] 5.5 运行 `openspec validate --all` 与 `./scripts/context-health.sh`。
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
## 5. 文档与测试环境验证
|
||||
|
||||
- [x] 5.1 对变更 Go 文件执行 `gofmt`,运行 `go build ./cmd/api ./cmd/worker` 与 `go run cmd/gendocs/main.go`,核对新字段与说明。
|
||||
- [ ] 5.2 按 ENG-TEST-001 在 `junhong_cmp_test` 创建并清理专用账号与店铺:用新表头 CSV 验证启用平台账号手机号换绑成功且审计前后值正确;格式不合法、用户名、不存在、非平台、已禁用分别按 delta spec 失败且原负责人不变;旧表头整批失败。
|
||||
- [x] 5.2 按 ENG-TEST-001 在 `junhong_cmp_test` 创建并清理专用账号与店铺:用新表头 CSV 验证启用平台账号手机号换绑成功且审计前后值正确;格式不合法、用户名、不存在、非平台、已禁用分别按 delta spec 失败且原负责人不变;旧表头整批失败。
|
||||
- [x] 5.3 订单:C 端微信、支付宝已支付及已退款订单三个字段与 `tb_payment` 一致;代理、钱包、线下、未支付为空串;构造“先微信未付、再支付宝已付”订单核对 `payments`;套餐订单与同 ID 充值单不串;列表无逐行查询。
|
||||
- [ ] 5.4 预警:规则列表与详情不含 `real_data_mb`,无额度套餐仍被拒;列表与详情 `threshold_mb` 计算正确;新导出任务最后一列为阈值流量,旧任务列序不变。
|
||||
- [ ] 5.5 设备导入:混合文件、整份空 IMEI、仅空格、虚拟号与 IMEI 都为空四种情形按 delta spec 核对。
|
||||
- [x] 5.4 预警:规则列表与详情不含 `real_data_mb`,无额度套餐仍被拒;列表与详情 `threshold_mb` 计算正确;新导出任务最后一列为阈值流量,旧任务列序不变。
|
||||
- [x] 5.5 设备导入:混合文件、整份空 IMEI、仅空格、虚拟号与 IMEI 都为空四种情形按 delta spec 核对。
|
||||
- [ ] 5.6 通知测试环境前端去掉 `real_data_mb` 的使用、接入订单新字段;通知运营换新换绑模板、导入须填 IMEI。
|
||||
- [ ] 5.7 运行 `openspec validate --all` 与 `./scripts/context-health.sh`。
|
||||
|
||||
Reference in New Issue
Block a user