# 基础设施:通用审批流引擎 > 被依赖:需求 18(多人审批)、需求 20(退款审批)、需求 21(充值审核) > 设计:DDD 结构,`internal/domain/approval/` --- ## 一、业务规则 - 审批顺序:**提交人部门领导 → 财务**(固定两级) - 每级只能有一个审批人(通过角色确定,不是指定个人) - 上一级审批通过后,下一级审批人收到站内消息 - **驳回**:永久拒绝,流程终止,通知申请人含驳回原因 - **退回**:退回给提交人修改,流程终止,提交人可修改后重新提交(新建审批流) - **通过**:最后一级通过后,通知申请人,触发业务回调 --- ## 二、数据库 ```sql -- 审批流实例表 CREATE TABLE tb_approval_flow ( id BIGSERIAL PRIMARY KEY, flow_no VARCHAR(30) NOT NULL UNIQUE, biz_type VARCHAR(50) NOT NULL, -- 业务类型:recharge | refund biz_id BIGINT NOT NULL, biz_no VARCHAR(50) NOT NULL DEFAULT '', -- 关联业务单号(快照) submitter_id BIGINT NOT NULL, submitter_name VARCHAR(50) NOT NULL DEFAULT '', current_step INT NOT NULL DEFAULT 1, -- 当前步骤(1=部门领导, 2=财务) total_steps INT NOT NULL DEFAULT 2, status INT NOT NULL DEFAULT 1, -- 1=审批中 2=已通过 3=已驳回 4=已退回 reject_reason TEXT, -- 驳回/退回原因 completed_at TIMESTAMPTZ, creator BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 审批步骤记录表 CREATE TABLE tb_approval_step_record ( id BIGSERIAL PRIMARY KEY, flow_id BIGINT NOT NULL, step INT NOT NULL, step_name VARCHAR(50) NOT NULL, approver_id BIGINT, approver_name VARCHAR(50), result INT NOT NULL DEFAULT 0, -- 0=待审批 1=通过 2=驳回 3=退回 remark TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_approval_flow_biz ON tb_approval_flow (biz_type, biz_id); CREATE INDEX idx_approval_step_flow ON tb_approval_step_record (flow_id, step); ``` --- ## 三、Model ```go // internal/model/approval.go const ( ApprovalBizTypeRecharge = "recharge" ApprovalBizTypeRefund = "refund" ApprovalStatusPending = 1 // 审批中 ApprovalStatusApproved = 2 // 已通过 ApprovalStatusRejected = 3 // 已驳回(永久拒绝,流程终止) ApprovalStatusReturned = 4 // 已退回(退回给提交人修改,可重新提交) ApprovalStepResultPending = 0 // 待审批 ApprovalStepResultApproved = 1 // 通过 ApprovalStepResultRejected = 2 // 驳回 ApprovalStepResultReturned = 3 // 退回 ) ``` --- ## 四、领域层(DDD) ```go // internal/domain/approval/approval_flow.go type ApprovalFlowAggregate struct { model.ApprovalFlow Steps []*model.ApprovalStepRecord domainEvents []DomainEvent `gorm:"-"` } // CanAdvance 检查当前用户是否可以审批当前步骤 func (f *ApprovalFlowAggregate) CanAdvance(approverRoles []string) bool { if f.Status != model.ApprovalStatusPending { return false } if f.CurrentStep == 1 { return contains(approverRoles, "department_leader") } if f.CurrentStep == 2 { return contains(approverRoles, "finance") } return false } // Approve 通过当前步骤 func (f *ApprovalFlowAggregate) Approve(approverID uint, approverName string, remark string) error { if f.Status != model.ApprovalStatusPending { return ErrFlowNotPending } now := time.Now() step := f.currentStepRecord() step.ApproverID = &approverID step.ApproverName = &approverName step.Result = model.ApprovalStepResultApproved step.Remark = &remark step.ApprovedAt = &now if f.CurrentStep >= f.TotalSteps { f.Status = model.ApprovalStatusApproved f.CompletedAt = &now f.recordEvent(ApprovalCompletedEvent{FlowID: f.ID, BizType: f.BizType, BizID: f.BizID}) } else { f.CurrentStep++ f.recordEvent(ApprovalStepAdvancedEvent{ FlowID: f.ID, NextStep: f.CurrentStep, BizType: f.BizType, SubmitterID: f.SubmitterID, }) } return nil } // Reject 驳回(永久拒绝,流程终止) func (f *ApprovalFlowAggregate) Reject(approverID uint, approverName string, reason string) error { if f.Status != model.ApprovalStatusPending { return ErrFlowNotPending } now := time.Now() step := f.currentStepRecord() step.ApproverID = &approverID step.ApproverName = &approverName step.Result = model.ApprovalStepResultRejected step.Remark = &reason step.ApprovedAt = &now f.Status = model.ApprovalStatusRejected f.RejectReason = &reason f.CompletedAt = &now f.recordEvent(ApprovalRejectedEvent{ FlowID: f.ID, BizType: f.BizType, BizID: f.BizID, SubmitterID: f.SubmitterID, Reason: reason, }) return nil } // Return 退回给提交人修改(流程终止,提交人可重新提交) func (f *ApprovalFlowAggregate) Return(approverID uint, approverName string, reason string) error { if f.Status != model.ApprovalStatusPending { return ErrFlowNotPending } now := time.Now() step := f.currentStepRecord() step.ApproverID = &approverID step.ApproverName = &approverName step.Result = model.ApprovalStepResultReturned step.Remark = &reason step.ApprovedAt = &now f.Status = model.ApprovalStatusReturned f.RejectReason = &reason f.CompletedAt = &now f.recordEvent(ApprovalReturnedEvent{ FlowID: f.ID, BizType: f.BizType, BizID: f.BizID, SubmitterID: f.SubmitterID, Reason: reason, }) return nil } ``` --- ## 五、领域事件 ```go // internal/domain/approval/events.go // ApprovalStepAdvancedEvent 步骤推进(通知下一级审批人) type ApprovalStepAdvancedEvent struct { FlowID uint NextStep int BizType string SubmitterID uint } // ApprovalCompletedEvent 审批完成(触发业务回调 + 通知申请人) type ApprovalCompletedEvent struct { FlowID uint BizType string BizID uint } // ApprovalRejectedEvent 驳回(通知申请人,流程永久终止) type ApprovalRejectedEvent struct { FlowID uint BizType string BizID uint SubmitterID uint Reason string } // ApprovalReturnedEvent 退回给提交人修改(通知申请人,可重新提交) type ApprovalReturnedEvent struct { FlowID uint BizType string BizID uint SubmitterID uint Reason string } ``` --- ## 六、接入协议(业务层如何接入审批流) 任何业务接入审批流需做以下四件事: ### 6.1 业务表新增字段 ```sql ALTER TABLE tb_xxx ADD COLUMN approval_flow_id BIGINT DEFAULT NULL; ``` ### 6.2 提交时创建审批流 业务提交接口(如 `POST /admin/refund-requests`): ```go // 1. 创建业务单(status=待审批) bizRecord, _ := s.bizStore.Create(ctx, req) // 2. 创建审批流 flow, _ := submitApprovalUseCase.Execute(ctx, SubmitApprovalCommand{ BizType: model.ApprovalBizTypeRefund, BizID: bizRecord.ID, BizNo: bizRecord.RefundNo, SubmitterID: middleware.GetUserID(ctx), SubmitterName: middleware.GetUsername(ctx), }) // 3. 把 flow_id 写回业务单 s.bizStore.UpdateApprovalFlowID(ctx, bizRecord.ID, flow.ID) ``` ### 6.3 监听审批事件,更新业务状态 每个接入方在 `internal/task/` 注册对应的 Asynq 事件处理器: ```go // internal/task/refund_approval_handler.go // OnApprovalCompleted 审批通过 → 退款单状态变"已通过",触发退款 func (h *RefundApprovalHandler) OnApprovalCompleted(ctx context.Context, event ApprovalCompletedEvent) error { // 按 biz_type 过滤,只处理退款类型 if event.BizType != model.ApprovalBizTypeRefund { return nil } return h.refundService.ApproveRefund(ctx, event.BizID) } // OnApprovalRejected 审批驳回 → 退款单状态变"已拒绝" func (h *RefundApprovalHandler) OnApprovalRejected(ctx context.Context, event ApprovalRejectedEvent) error { if event.BizType != model.ApprovalBizTypeRefund { return nil } return h.refundService.RejectRefund(ctx, event.BizID, event.Reason) } // OnApprovalReturned 审批退回 → 退款单状态变"已退回",提交人可修改重提 func (h *RefundApprovalHandler) OnApprovalReturned(ctx context.Context, event ApprovalReturnedEvent) error { if event.BizType != model.ApprovalBizTypeRefund { return nil } return h.refundService.ReturnRefund(ctx, event.BizID, event.Reason) } ``` 充值单同理,注册 `RechargeApprovalHandler`。 ### 6.4 退回后重新提交 业务层新增重新提交接口(如 `POST /admin/refund-requests/{id}/resubmit`): ```go // 1. 校验业务单 status=已退回 if bizRecord.Status != model.RefundStatusReturned { return errors.New(errors.CodeForbidden, "当前状态不允许重新提交") } // 2. 新建审批流(biz_type+biz_id 相同,旧的流程作为历史保留) flow, _ := submitApprovalUseCase.Execute(ctx, SubmitApprovalCommand{...}) // 3. 更新业务单:approval_flow_id = 新 flow.ID,status 回到"待审批" s.bizStore.Resubmit(ctx, bizRecord.ID, flow.ID) ``` --- ## 七、应用层(用例) ```go // internal/application/approval/submit_approval.go type SubmitApprovalCommand struct { BizType string BizID uint BizNo string SubmitterID uint SubmitterName string } type SubmitApprovalUseCase struct { approvalRepo approval.ApprovalRepository notifyPublisher *notification.NotificationPublisher userStore UserStore } func (uc *SubmitApprovalUseCase) Execute(ctx context.Context, cmd SubmitApprovalCommand) (*model.ApprovalFlow, error) { flow := &domain_approval.ApprovalFlowAggregate{...} // 初始化两个步骤记录(均为待审批) if err := uc.approvalRepo.Create(ctx, flow); err != nil { return nil, err } // 通知部门领导 leaderIDs := uc.userStore.GetUsersByRole(ctx, "department_leader") uc.notifyPublisher.Publish(ctx, notification.SendPayload{ RecipientIDs: leaderIDs, Type: constants.NotifyTypeApprovalPending, Title: "您有一条待审批记录", Body: fmt.Sprintf("「%s」提交了%s申请", cmd.SubmitterName, bizTypeLabel(cmd.BizType)), RefType: "approval", RefID: flow.ID, }) return &flow.ApprovalFlow, nil } ``` --- ## 八、API 设计 ### 8.1 待我审批列表 ``` GET /admin/approvals?status=1&biz_type=recharge&page=1&page_size=20 ``` ### 8.2 审批通过 ``` POST /admin/approvals/{flow_id}/approve { "remark": "审批通过" } ``` ### 8.3 审批驳回(永久拒绝) ``` POST /admin/approvals/{flow_id}/reject { "reason": "金额不符,请重新提交" } ``` ### 8.4 退回给提交人修改 ``` POST /admin/approvals/{flow_id}/return { "reason": "请补充支付凭证后重新提交" } ``` 退回后,提交人修改业务单,再通过业务接口重新提交(如 `POST /admin/refund-requests/{id}/resubmit`)。 ### 8.5 审批详情 ``` GET /admin/approvals/{flow_id} ``` 响应包含:flow 基本信息 + steps 列表(每步审批人、结果【通过/驳回/退回】、时间、备注) --- ## 九、前端对接 ### 审批详情页操作区 当前登录用户是当前步骤审批人时,显示三个按钮: ``` [审批通过] [退回修改] [驳回] 备注/原因输入框 ``` - **审批通过** → `POST /admin/approvals/{id}/approve` - **退回修改** → `POST /admin/approvals/{id}/return`(需填退回原因) - **驳回** → `POST /admin/approvals/{id}/reject`(需填驳回原因) ### 权限控制 - `department_leader` 角色:步骤1为"待审批"时显示操作按钮 - `finance` 角色:步骤2为"待审批"时显示操作按钮 - 其他角色:只读