docs: add design for bx apply withdraw (reimburse + advance)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
wuzilongandCursor committed 2026-09-14 09:55:23 +08:00
1 parent 8c96de2c0c
commit ac713070ab
1 file changed
+147
@@ -0,0 +1,147 @@
# 报销 / 备用金申请「撤回」能力设计
**日期**:2026-09-14
**状态**:已确认待实现
**范围**:Web 管理端报销申请 / 备用金申请 + 小程序报销 / 备用金详情
## 背景
【报销申请】中状态为「审批中」的单据当前不可编辑、不可再次提交。用户希望:**自己作为申请人发起的审批中单据,可自行撤回 → 回草稿 → 修改 → 再提交**。
报销后端已具备撤回接口与发票额度释放逻辑;Web 申请页与小程序详情未暴露入口。备用金无对称的 withdraw API。
## 决策摘要
| 项 | 决策 |
|----|------|
| 端范围 | Web 报销申请 + Web 备用金申请 + 小程序(报销/备用金详情) |
| 权限 | 仅申请人(`applicant_id`)可撤回 |
| 时机 | 只要状态为 `pending`(审批中)即可,不区分已过几级审批 |
| 交互 | 仅单条:Web 工具栏(选中 1 条)+ 详情;小程序主要在详情底栏 |
| 撤回原因 | 不要求 |
| 批量撤回 | 不做 |
| 方案 | 复用现有报销 withdraw,补齐备用金后端 + 各端 UI |
## 行为与状态
### 可撤回条件(同时满足)
1. `status === 'pending'`
2. 当前登录用户 id === 单据 `applicant_id`(报销人 / 借款人)
代申请场景下:填写人若不是申请人,**不能**撤回(与现有报销后端一致)。
### 撤回结果
1. 状态:`pending` → `draft`(待提交)
2. 清空 `current_node`
3. 报销:调用既有 `InvoiceLedgerService.release_reimburse`,退回发票占用额度
4. 备用金:无发票额度逻辑,仅回草稿并清节点
5. **保留**既有审批历史记录(便于审计;再次提交后进入新一轮)
6. 不写入「撤回原因」字段
### 撤回后
按现有草稿逻辑:可编辑、可删除、可再次提交审批。前端沿用 `isDraftEditable`(`draft` / `rejected`)门禁。
## 入口与交互
### Web 管理端
页面:
- `/#/bx/reimburse/apply` → `web-admin/src/views/bx/reimburse/apply/index.vue`
- `/#/bx/advance/apply` → `web-admin/src/views/bx/advance/apply/index.vue`
| 位置 | 行为 |
|------|------|
| 列表工具栏 | 「撤回」按钮;仅当恰好选中 1 条且 `canWithdraw(row)` 时可用 |
| 详情弹窗 | `canWithdraw(viewData)` 时展示「撤回」;与草稿态的编辑/提交互斥 |
| 确认框 | 报销:说明撤回后将退回发票额度;备用金:说明撤回后可修改再提交 |
前端判断:
```ts
function canWithdraw(row?: { status?: string; applicantId?: string }) {
if (!row) return false
return (
row.status === 'pending' &&
String(row.applicantId || '') === currentUserId()
)
}
```
图标建议:`RefreshLeft`(与运费结算/合同变更等撤回一致)。
### 小程序
页面:
- `miniprogram/pages/bx/reimburse/detail.vue`
- `miniprogram/pages/bx/advance/detail.vue`
| 位置 | 行为 |
|------|------|
| 详情底栏 | `pending` 且当前用户为申请人时显示「撤回」;`showActions` 需覆盖 `pending` |
| 确认 | `uni.showModal`;成功后刷新详情 |
| 列表页 | 不加撤回按钮 |
参考已有移动端备份实现:`bak/bx-mobile/src/views/reimburse/ReimburseDetailPage.vue`(确认文案含发票额度退回)。
## 后端与接口
### 报销(已有,本期原则上不改业务规则)
- `POST /api/bx/reimburses/{id}/withdraw`
- Service:`ReimburseService.withdraw`
- 校验:单据存在(数据权限内)+ `applicant_id == user.id` + `status == pending`
- 失败:HTTP 400,`报销单不存在、非本人申请或当前状态不可撤回`
- Web API 已封装:`reimburseApi.withdraw`
### 备用金(本期新增,对齐报销)
- Service:`AdvanceService.withdraw`
- 同上申请人 + `pending` 校验
- `status = draft`,`current_node = None`
- **不**调用发票账本
- 路由:`POST /api/bx/advances/{id}/withdraw`
- 注册于 `backend/app/api/bx/advance.py`(与 submit/approve 同级)
- Web:`advanceApi.withdraw(id)`
- 小程序:`miniprogram/api/bx.js` 中 `advanceApi.withdraw`、`reimburseApi.withdraw`
### 错误与并发
- 前端禁用非可撤单据;后端仍做硬校验(防绕过)
- 若撤回瞬间审批人已通过末级导致状态非 `pending`:返回 400,前端提示失败并刷新列表/详情
## 明确不做(本期)
- 撤回原因录入
- 批量撤回
- 在审批历史中单独插入「撤回」节点(历史原记录保留即可)
- 列表页(小程序)撤回入口
- 修改报销既有 withdraw 的权限模型(保持仅申请人)
## 实现落点(文件级)
| 层 | 文件 |
|----|------|
| Backend | `backend/app/services/bx/advance.py`(新增 withdraw) |
| Backend | `backend/app/api/bx/advance.py`(新增路由) |
| Web API | `web-admin/src/api/bx/index.ts`(advanceApi.withdraw) |
| Web UI | `web-admin/src/views/bx/reimburse/apply/index.vue` |
| Web UI | `web-admin/src/views/bx/advance/apply/index.vue` |
| Mini API | `miniprogram/api/bx.js` |
| Mini UI | `miniprogram/pages/bx/reimburse/detail.vue` |
| Mini UI | `miniprogram/pages/bx/advance/detail.vue` |
可选:为 `AdvanceService.withdraw` 补最小单测(权限/状态门禁);报销若已有覆盖可只测备用金。
## 验收标准
1. 申请人在 Web 报销申请页对「审批中」单条可撤回 → 变为待提交 → 可编辑再提交。
2. 非申请人无法撤回(按钮禁用;直调接口 400)。
3. 撤回后发票额度回升(报销);备用金无额度副作用。
4. Web 备用金申请页行为与报销对称。
5. 小程序报销/备用金详情底栏可撤回,成功后详情变为可提交草稿态。
6. 已审批通过 / 已驳回 / 草稿 单据不可撤回。