结论是用 internal/audit + 接口抽象 + 独立 go.mod 模块 + 链式校验器组合,比硬套 ddd 分层更易落地、测试和解耦;核心在于职责分离、依赖隔离与错误结构化透出。

go.mod 文件不是目录结构的装饰,而是模块边界的声明——多级审核模块的核心不在“分几层”,而在“谁负责什么、依赖怎么切、错误怎么透出”。
直接说结论:用 internal/audit + 接口抽象 + 独立 go.mod 模块 + 链式校验器组合,比硬套 DDD 分层更易落地、更易测试、更少耦合。
为什么不能把审核逻辑全塞进 handler 或 service?
常见错误是把“初审→复审→终审”写成三个 if-else 块,或在 service.Approve() 里硬编码状态流转逻辑。后果很实际:
- 无法单独测试某一级审核规则(比如复审只看风控指标,不关心申请人职级)
- 新增审批节点要改主逻辑,违反开闭原则
- 不同业务线(如财务报销 vs 合同签署)共用同一套审核流程,但字段校验和权限判定完全不同,强行复用导致
switch bizType泛滥
真正该复用的是审核器的“执行框架”,不是具体规则。
如何用 interface 解耦审核步骤?
每个审核环节定义一个明确输入输出的接口,不暴露实现细节:
type Reviewer interface {
Name() string
CanReview(ctx context.Context, req *AuditRequest) (bool, error)
Review(ctx context.Context, req *AuditRequest) (*ReviewResult, error)
}
示例:财务复审只检查金额是否超阈值,且必须有 FinanceRole 权限:
func (r *FinanceReviewer) CanReview(ctx context.Context, req *AuditRequest) (bool, error) {
role, ok := auth.RoleFromCtx(ctx)
if !ok || role != "FinanceRole" {
return false, errors.New("insufficient role")
}
return req.Amount > 100000, nil // 仅当金额超10万才触发复审
}
关键点:
-
CanReview()决定是否跳过该环节(不是所有请求都走满三级) -
Review()返回*ReviewResult,含Status(PASS/REJECT/SKIP)、Comment、NextReviewer(支持动态跳转) - 所有实现放在
internal/audit/reviewers/下,外部模块只能 import 接口,不能 import 具体 reviewer
怎么让多级审核可配置、可插拔?
审核链不应写死在代码里,而应通过配置驱动:
// conf/audit.yaml stages: - name: "initial" reviewer: "applicant-manager" required: true - name: "risk" reviewer: "risk-team" condition: "amount > 50000 || category == 'vendor'" - name: "finance" reviewer: "finance-officer" condition: "amount > 100000"
启动时加载配置,按顺序构造审核链:
chain := audit.NewChain().
WithStage("initial", mgrReviewer).
WithStage("risk", riskReviewer).
WithStage("finance", finReviewer)
注意:
-
condition字段用 govaluate 解析,避免手写if判断;表达式里可访问req.Amount、req.Category等字段 - 每个 stage 的
reviewer名称映射到具体实例,用map[string]Reviewer注册,便于单元测试替换 mock - 配置变更无需重启服务,支持热重载(监听文件变化 + atomic swap chain 实例)
审核失败时,错误怎么回传才不丢上下文?
别用 errors.New("审核不通过") —— 审核日志和前端提示都需要知道是哪一级、哪个字段、为什么失败。
推荐返回带路径的结构化错误:
type AuditError struct {
Stage string `json:"stage"` // "risk"
Field string `json:"field"` // "amount"
Code string `json:"code"` // "AMOUNT_OVER_LIMIT"
Message string `json:"message"` // "金额超出风控阈值"
}
链式执行中任一环节返回 AuditError,就终止后续阶段,并透出完整信息。前端可据此高亮对应字段,DB 审计表也能存 stage 和 code 做统计分析。
容易忽略的点:
- 不要在
Review()方法里 panic 或 log.Fatal —— 审核失败是业务常态,不是程序异常 - 如果某级审核依赖外部服务(如调用风控 API),需封装超时和重试逻辑,且错误类型要区分 “服务不可用” 和 “业务拒绝”,前者应标记为
StageStatus=ERROR,后者才是REJECT - 所有
AuditRequest字段必须显式声明 JSON tag,否则 govaluate 解析 condition 时取不到值
审核链的复杂度不在层级数量,而在各环节的独立性与可观测性。越早把每级审核器隔离成可测、可配、可换的单元,后期加新流程、改旧规则、查问题根因就越省力。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











