diff --git a/internal/query/employeecollection/application.go b/internal/query/employeecollection/application.go index 982ba7c..751ed0c 100644 --- a/internal/query/employeecollection/application.go +++ b/internal/query/employeecollection/application.go @@ -196,7 +196,7 @@ func (q *ApplicationQuery) applicationAttemptList( Order("attempt_no ASC, id ASC").Find(&attempts).Error; err != nil { return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询核销审批尝试记录失败") } - approvalStatus, approvalOpinions, err := approvalStatusOfAttempts(ctx, q.db, attempts, true) + approvalStatus, approvalOpinions, err := approvalStatusOfAttempts(ctx, q.db, attempts) if err != nil { return nil, err } diff --git a/internal/query/employeecollection/bill.go b/internal/query/employeecollection/bill.go index 2f322f6..e4a9f80 100644 --- a/internal/query/employeecollection/bill.go +++ b/internal/query/employeecollection/bill.go @@ -365,7 +365,7 @@ func (q *BillQuery) applicationAttempts( Order("application_id ASC, attempt_no ASC").Find(&attempts).Error; err != nil { return nil, nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "查询审批尝试记录失败") } - approvalStatus, approvalOpinions, err := approvalStatusOfAttempts(ctx, q.db, attempts, true) + approvalStatus, approvalOpinions, err := approvalStatusOfAttempts(ctx, q.db, attempts) if err != nil { return nil, nil, nil, err } @@ -376,13 +376,11 @@ func (q *BillQuery) applicationAttempts( return grouped, approvalStatus, approvalOpinions, nil } -// approvalStatusOfAttempts 批量读取审批尝试记录关联的通用审批实例状态。 -// withOpinion 为真时同时读取终态决策快照并提取审批意见;列表路径传 false,避免批量加载渠道快照。 +// approvalStatusOfAttempts 批量读取审批尝试记录关联的通用审批实例状态与终态审批意见。 func approvalStatusOfAttempts( ctx context.Context, db *gorm.DB, attempts []model.EmployeeCollectionApplicationAttempt, - withOpinion bool, ) (map[uint]int, map[uint]string, error) { instanceIDs := make([]uint, 0, len(attempts)) for index := range attempts { @@ -395,10 +393,7 @@ func approvalStatusOfAttempts( if len(instanceIDs) == 0 { return approvalStatus, approvalOpinions, nil } - columns := []string{"id", "status"} - if withOpinion { - columns = append(columns, "decision_snapshot") - } + columns := []string{"id", "status", "decision_snapshot"} var instances []model.ApprovalInstance if err := db.WithContext(ctx).Select(columns). Where("id IN ?", instanceIDs).Find(&instances).Error; err != nil { diff --git a/openspec/changes/add-employee-collection-bills/design.md b/openspec/changes/add-employee-collection-bills/design.md index b967e7c..55a7d56 100644 --- a/openspec/changes/add-employee-collection-bills/design.md +++ b/openspec/changes/add-employee-collection-bills/design.md @@ -101,7 +101,7 @@ - 企业微信回调和既有状态恢复任务均按审批实例 ID 进入同一应用用例,不提供后台“通过/驳回”接口。 - 最终通过:锁定申请及按 ID 升序的全部账单;仅当申请仍为审批中时,将每笔分摊从预占转入已核销,重新计算账单 `待核销/部分核销/已核销` 状态,标记申请已通过,并记录审批结果。重复或乱序的同一终态不重复增加已核销金额。 - 最终驳回:仅当申请仍为审批中时释放全部预占,标记已驳回并保存审批意见;重复回调不重复释放。提交失败、回调延迟和未知结果维持在途,由既有查询恢复任务确认,不得人工改写终态。 -- 通过后撤销:已通过后收到企业微信撤销时**不回滚已核销金额**;申请转异常终态、保留审计、账单详情提示来源申请异常,并禁止自动重提。 +- 通过后撤销:已通过后收到企业微信撤销时**不回滚已核销金额**;申请转异常终态、保留审计、账单详情提示来源申请异常,并禁止自动重提。若该撤销结果到达时本地申请仍为审批中(回调乱序或通过结果未被消费),同样按异常终态处理并释放本次尝试的全部审批中预占——否则申请会永久停在审批中、预占永久占用账单,既不可关闭也不可重提。 ### 来源建账与退款冲销 diff --git a/openspec/changes/add-employee-collection-bills/specs/employee-collection-bill/spec.md b/openspec/changes/add-employee-collection-bills/specs/employee-collection-bill/spec.md index 01987f3..105c8cd 100644 --- a/openspec/changes/add-employee-collection-bills/specs/employee-collection-bill/spec.md +++ b/openspec/changes/add-employee-collection-bills/specs/employee-collection-bill/spec.md @@ -83,7 +83,7 @@ ### Requirement: 核销申请审批、重提与幂等 核销申请状态 SHALL 为 `审批中`、`已通过`、`已驳回`、`已撤销/已关闭`,且不得以申请状态覆盖账单核销状态。提交或重提时系统 MUST 新增一条不可变审批尝试记录(冻结当次收款方式、外部付款、附件、备注、账单分摊及审批材料快照)并为其创建新的企业微信审批实例;申请只保存最新审批实例 ID 用于展示。企业微信业务类型 MUST 为 `employee_collection_approval`,其业务标识 MUST 取审批尝试记录主键,使同一申请的多次提交各自持有独立审批实例,且 MUST NOT 修改既有审批实例的共享唯一约束或既有 `refund_approval`、`offline_recharge_approval` 场景语义。 -企业微信最终通过时,系统 MUST 幂等地将申请标记为已通过、将各分摊写入账单已核销金额并释放其预占;最终驳回时 MUST 标记申请已驳回、释放全部预占且保留审批意见。已驳回申请可修改全部申请内容后重提,历史审批实例、材料和结果不得覆盖;已通过分摊不可修改。企业微信提交失败、回调延迟或结果未知时申请保持在途,系统 MUST 使用既有查询/恢复机制确认渠道结果,且不得由本地人工通过或拒绝绕过企业微信。企业微信对已通过申请撤销时,系统 MUST NOT 回滚已核销金额,MUST 将申请转为异常终态、保留审计与账单详情提示,并禁止自动重提。 +企业微信最终通过时,系统 MUST 幂等地将申请标记为已通过、将各分摊写入账单已核销金额并释放其预占;最终驳回时 MUST 标记申请已驳回、释放全部预占且保留审批意见。已驳回申请可修改全部申请内容后重提,历史审批实例、材料和结果不得覆盖;已通过分摊不可修改。企业微信提交失败、回调延迟或结果未知时申请保持在途,系统 MUST 使用既有查询/恢复机制确认渠道结果,且不得由本地人工通过或拒绝绕过企业微信。企业微信对已通过申请撤销时,系统 MUST NOT 回滚已核销金额,MUST 将申请转为异常终态、保留审计与账单详情提示,并禁止自动重提。同一撤销结果在本地申请仍为审批中时(回调乱序或通过结果未被消费),系统 MUST 释放该次审批尝试的全部审批中预占、将申请转为异常终态并记录终态原因、保留审计,MUST NOT 自动重提。 #### Scenario: 企业微信通过核销申请 - **WHEN** 企业微信对含多笔分摊的核销申请返回最终通过,且该结果首次被消费 @@ -97,6 +97,10 @@ - **WHEN** 已通过的核销申请收到企业微信撤销结果 - **THEN** 系统不回滚已核销金额,将申请标记为异常终态、保留审计并在账单详情提示,且不允许自动重提 +#### Scenario: 审批中收到通过后撤销 +- **WHEN** 企业微信对仍处于审批中的核销申请返回“通过后撤销”(回调乱序或本地未消费到通过结果) +- **THEN** 系统释放该申请全部审批中预占、将申请转为异常终态并记录终态原因、保留审计,且不回滚任何已核销金额;该申请禁止自动重提,账单可重新发起新的核销申请 + ### Requirement: 收款方式字典、退款联动与可追溯性 系统 SHALL 提供唯一固定分类的线下收款方式字典,其对外契约 MUST 包含 ID、稳定编码、名称和启用状态,引用方 MUST 保存名称快照。超级管理员可维护名称、稳定编码、排序、启停和备注;已被业务引用的字典项 MUST NOT 被物理删除,只能停用,且历史申请继续显示冻结名称。该字典分类由本能力独占维护,MUST NOT 被定义为“核销专用”,本能力也 MUST NOT 修改企业微信 `offline_recharge_approval` 场景的业务字段。