|
|
@@ -0,0 +1,579 @@
|
|
|
+# 报销 / 备用金申请「撤回」Implementation Plan
|
|
|
+
|
|
|
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
+
|
|
|
+**Goal:** 申请人可将「审批中」的报销/备用金申请单撤回为草稿,再编辑并重新提交;覆盖 Web 申请页与小程序详情。
|
|
|
+
|
|
|
+**Architecture:** 报销复用已有 `POST /reimburses/{id}/withdraw`;备用金按同规则新增 `AdvanceService.withdraw` + 路由。各端仅暴露单条撤回入口(工具栏选中 1 条 / 详情),前端用 `canWithdraw`(`pending` + `applicantId === 当前用户`)门禁,后端仍硬校验。
|
|
|
+
|
|
|
+**Tech Stack:** FastAPI + SQLAlchemy;Vue 3 + Element Plus(web-admin);uni-app(miniprogram)
|
|
|
+
|
|
|
+**Spec:** `docs/superpowers/specs/2026-09-14-bx-apply-withdraw-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## File map
|
|
|
+
|
|
|
+| File | Responsibility |
|
|
|
+|------|----------------|
|
|
|
+| `backend/app/services/bx/advance.py` | 新增 `withdraw`:申请人 + pending → draft,清 `current_node` |
|
|
|
+| `backend/app/api/bx/advance.py` | 新增 `POST /{advance_id}/withdraw` |
|
|
|
+| `backend/tests/test_bx_advance_withdraw.py` | 用 mock 测门禁与状态回写 |
|
|
|
+| `web-admin/src/api/bx/index.ts` | `advanceApi.withdraw` |
|
|
|
+| `web-admin/src/views/bx/reimburse/apply/index.vue` | 工具栏 + 详情撤回 |
|
|
|
+| `web-admin/src/views/bx/advance/apply/index.vue` | 同上(对称) |
|
|
|
+| `miniprogram/api/bx.js` | `reimburseApi.withdraw` / `advanceApi.withdraw` |
|
|
|
+| `miniprogram/pages/bx/reimburse/detail.vue` | 底栏撤回 |
|
|
|
+| `miniprogram/pages/bx/advance/detail.vue` | 底栏撤回 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 备用金 withdraw 后端(TDD)
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Create: `backend/tests/test_bx_advance_withdraw.py`
|
|
|
+- Modify: `backend/app/services/bx/advance.py`(在 `reject` 方法之后、`_approval_record_dict` 之前插入)
|
|
|
+- Modify: `backend/app/api/bx/advance.py`(在 `reject_advance` 与 `delete_advance` 之间插入路由)
|
|
|
+
|
|
|
+- [ ] **Step 1: 写失败用例(mock Service 行为)**
|
|
|
+
|
|
|
+创建 `backend/tests/test_bx_advance_withdraw.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+"""备用金撤回:仅申请人且审批中可回草稿。"""
|
|
|
+from types import SimpleNamespace
|
|
|
+from unittest.mock import MagicMock, patch
|
|
|
+
|
|
|
+from app.services.bx.advance import AdvanceService
|
|
|
+
|
|
|
+
|
|
|
+def _user(uid="u1"):
|
|
|
+ return SimpleNamespace(id=uid, name="张三", dept_id="d1")
|
|
|
+
|
|
|
+
|
|
|
+def _apply(**kwargs):
|
|
|
+ base = {
|
|
|
+ "id": "a1",
|
|
|
+ "applicant_id": "u1",
|
|
|
+ "status": "pending",
|
|
|
+ "current_node": "一级审批",
|
|
|
+ "update_by": None,
|
|
|
+ }
|
|
|
+ base.update(kwargs)
|
|
|
+ return SimpleNamespace(**base)
|
|
|
+
|
|
|
+
|
|
|
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
|
|
|
+@patch.object(AdvanceService, "get_by_id")
|
|
|
+def test_withdraw_pending_by_applicant(mock_get, _tenant):
|
|
|
+ apply_obj = _apply()
|
|
|
+ mock_get.return_value = apply_obj
|
|
|
+ db = MagicMock()
|
|
|
+ out = AdvanceService.withdraw(db, "a1", _user("u1"))
|
|
|
+ assert out is apply_obj
|
|
|
+ assert apply_obj.status == "draft"
|
|
|
+ assert apply_obj.current_node is None
|
|
|
+ assert apply_obj.update_by == "u1"
|
|
|
+ db.flush.assert_called_once()
|
|
|
+
|
|
|
+
|
|
|
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
|
|
|
+@patch.object(AdvanceService, "get_by_id")
|
|
|
+def test_withdraw_rejects_non_applicant(mock_get, _tenant):
|
|
|
+ mock_get.return_value = _apply(applicant_id="u1")
|
|
|
+ assert AdvanceService.withdraw(MagicMock(), "a1", _user("u2")) is None
|
|
|
+
|
|
|
+
|
|
|
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
|
|
|
+@patch.object(AdvanceService, "get_by_id")
|
|
|
+def test_withdraw_rejects_non_pending(mock_get, _tenant):
|
|
|
+ mock_get.return_value = _apply(status="draft")
|
|
|
+ assert AdvanceService.withdraw(MagicMock(), "a1", _user("u1")) is None
|
|
|
+
|
|
|
+
|
|
|
+@patch.object(AdvanceService, "_tenant_no", return_value="T1")
|
|
|
+@patch.object(AdvanceService, "get_by_id")
|
|
|
+def test_withdraw_missing_returns_none(mock_get, _tenant):
|
|
|
+ mock_get.return_value = None
|
|
|
+ assert AdvanceService.withdraw(MagicMock(), "a1", _user("u1")) is None
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 跑测确认失败**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_bx_advance_withdraw.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: FAIL(`AdvanceService` 无 `withdraw` 属性)
|
|
|
+
|
|
|
+- [ ] **Step 3: 实现 `AdvanceService.withdraw`**
|
|
|
+
|
|
|
+在 `backend/app/services/bx/advance.py` 的 `reject` 方法结束后插入(对齐 `ReimburseService.withdraw`,无发票释放):
|
|
|
+
|
|
|
+```python
|
|
|
+ @staticmethod
|
|
|
+ def withdraw(db: Session, apply_id: str, user: BxUserContext) -> Optional[BxAdvanceApply]:
|
|
|
+ tenant_no = AdvanceService._tenant_no(db, user)
|
|
|
+ apply_obj = AdvanceService.get_by_id(db, apply_id, tenant_no, user=user)
|
|
|
+ if not apply_obj:
|
|
|
+ return None
|
|
|
+ if apply_obj.applicant_id != user.id:
|
|
|
+ return None
|
|
|
+ if apply_obj.status != "pending":
|
|
|
+ return None
|
|
|
+ apply_obj.status = "draft"
|
|
|
+ apply_obj.current_node = None
|
|
|
+ apply_obj.update_by = user.id
|
|
|
+ db.flush()
|
|
|
+ return apply_obj
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 注册 API 路由**
|
|
|
+
|
|
|
+在 `backend/app/api/bx/advance.py` 的 `reject_advance` 与 `delete_advance` 之间插入:
|
|
|
+
|
|
|
+```python
|
|
|
+@router.post("/{advance_id}/withdraw", response_model=ApiResponse[AdvanceApplyOut])
|
|
|
+def withdraw_advance(
|
|
|
+ advance_id: str,
|
|
|
+ db: Session = Depends(get_db),
|
|
|
+ user: BxUserContext = Depends(get_bx_current_user),
|
|
|
+):
|
|
|
+ advance = AdvanceService.withdraw(db, advance_id, user)
|
|
|
+ if not advance:
|
|
|
+ raise HTTPException(
|
|
|
+ status_code=status.HTTP_400_BAD_REQUEST,
|
|
|
+ detail="备用金申请不存在、非本人申请或当前状态不可撤回",
|
|
|
+ )
|
|
|
+ db.commit()
|
|
|
+ db.refresh(advance)
|
|
|
+ return ApiResponse(data=AdvanceApplyOut.model_validate(advance))
|
|
|
+```
|
|
|
+
|
|
|
+对照报销路由:`backend/app/api/bx/reimburse.py` 中 `withdraw_reimburse`(约 766–781 行)。
|
|
|
+
|
|
|
+- [ ] **Step 5: 跑测确认通过**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd backend && source venv/bin/activate && pytest tests/test_bx_advance_withdraw.py -v
|
|
|
+```
|
|
|
+
|
|
|
+Expected: 4 passed
|
|
|
+
|
|
|
+- [ ] **Step 6: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/tests/test_bx_advance_withdraw.py backend/app/services/bx/advance.py backend/app/api/bx/advance.py
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(bx): add advance apply withdraw API
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: Web / 小程序 API 客户端
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/api/bx/index.ts`(`advanceApi`,在 `submit` 后)
|
|
|
+- Modify: `miniprogram/api/bx.js`(`advanceApi` 与 `reimburseApi`)
|
|
|
+
|
|
|
+- [ ] **Step 1: Web `advanceApi.withdraw`**
|
|
|
+
|
|
|
+在 `web-admin/src/api/bx/index.ts` 的 `advanceApi.submit` 后增加:
|
|
|
+
|
|
|
+```ts
|
|
|
+ withdraw(id: BxId) {
|
|
|
+ return bxRequest.post(`/advances/${id}/withdraw`)
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+(`reimburseApi.withdraw` 已存在,勿重复。)
|
|
|
+
|
|
|
+- [ ] **Step 2: 小程序 API**
|
|
|
+
|
|
|
+在 `miniprogram/api/bx.js`:
|
|
|
+
|
|
|
+`advanceApi` 增加:
|
|
|
+
|
|
|
+```js
|
|
|
+ withdraw: (id) => bxRequest({ url: `/advances/${id}/withdraw`, method: 'POST' }),
|
|
|
+```
|
|
|
+
|
|
|
+`reimburseApi` 增加:
|
|
|
+
|
|
|
+```js
|
|
|
+ withdraw: (id) => bxRequest({ url: `/reimburses/${id}/withdraw`, method: 'POST' }),
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/api/bx/index.ts miniprogram/api/bx.js
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(bx): wire advance/reimburse withdraw API clients
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: Web 报销申请页撤回入口
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/bx/reimburse/apply/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 导入图标**
|
|
|
+
|
|
|
+在 `@element-plus/icons-vue` 导入列表增加 `RefreshLeft`。
|
|
|
+
|
|
|
+- [ ] **Step 2: 工具栏按钮**
|
|
|
+
|
|
|
+在「提交审批」与「查看审批流」之间插入:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <el-button
|
|
|
+ class="btn-ghost"
|
|
|
+ :disabled="selectedRows.length !== 1 || !canWithdraw(selectedRows[0])"
|
|
|
+ :loading="loadingMap.withdraw"
|
|
|
+ @click="handleToolbarWithdraw"
|
|
|
+ >
|
|
|
+ <el-icon><RefreshLeft /></el-icon>撤回
|
|
|
+ </el-button>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 详情弹窗 hero 操作**
|
|
|
+
|
|
|
+在现有 `v-if="isDraftEditable(viewData)"` 的 `dlg-view-hero-actions` **之后**增加(互斥展示):
|
|
|
+
|
|
|
+```vue
|
|
|
+ <div v-else-if="canWithdraw(viewData)" class="dlg-view-hero-actions">
|
|
|
+ <el-button
|
|
|
+ class="btn-ghost"
|
|
|
+ :loading="loadingMap[`withdraw_${viewData.id}`]"
|
|
|
+ @click="handleViewWithdraw"
|
|
|
+ >
|
|
|
+ <el-icon><RefreshLeft /></el-icon>撤回
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: 脚本逻辑**
|
|
|
+
|
|
|
+在 `isDraftEditable` 附近增加(复用已有 `currentUserId`):
|
|
|
+
|
|
|
+```ts
|
|
|
+function canWithdraw(row?: any) {
|
|
|
+ if (!row) return false
|
|
|
+ return (
|
|
|
+ row.status === 'pending' &&
|
|
|
+ String(row.applicantId || '') === currentUserId()
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+async function handleToolbarWithdraw() {
|
|
|
+ if (selectedRows.value.length !== 1) {
|
|
|
+ ElMessage.warning('请先勾选一条报销单')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const row = selectedRows.value[0]
|
|
|
+ if (!canWithdraw(row)) {
|
|
|
+ ElMessage.warning('仅本人申请且审批中的报销单可撤回')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ await handleWithdraw(row)
|
|
|
+}
|
|
|
+
|
|
|
+async function handleViewWithdraw() {
|
|
|
+ if (!viewData.value?.id) return
|
|
|
+ await handleWithdraw(viewData.value)
|
|
|
+ try {
|
|
|
+ const res = await reimburseApi.getById(viewData.value.id)
|
|
|
+ viewData.value = res.data || res
|
|
|
+ } catch {
|
|
|
+ dialogs.view = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+async function handleWithdraw(row: any) {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ '确定撤回该报销单?撤回后将退回发票额度,单据变为待提交,可修改后再提交。',
|
|
|
+ '撤回确认',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定撤回',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ )
|
|
|
+ loadingMap.withdraw = true
|
|
|
+ loadingMap[`withdraw_${row.id}`] = true
|
|
|
+ await reimburseApi.withdraw(row.id)
|
|
|
+ ElMessage.success('已撤回')
|
|
|
+ await refreshList()
|
|
|
+ } catch (e: any) {
|
|
|
+ if (e !== 'cancel') {
|
|
|
+ ElMessage.error(e?.message || '撤回失败')
|
|
|
+ await refreshList()
|
|
|
+ }
|
|
|
+ } finally {
|
|
|
+ loadingMap.withdraw = false
|
|
|
+ loadingMap[`withdraw_${row.id}`] = false
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+确认 `loadingMap` 为可动态赋值的 reactive 对象(页面已有 `loadingMap.submit` 等用法)。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/bx/reimburse/apply/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): add withdraw on reimburse apply page
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: Web 备用金申请页撤回入口
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `web-admin/src/views/bx/advance/apply/index.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 导入**
|
|
|
+
|
|
|
+增加:
|
|
|
+
|
|
|
+```ts
|
|
|
+import { RefreshLeft } from '@element-plus/icons-vue' // 并入现有 icons 解构
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
+```
|
|
|
+
|
|
|
+增加:
|
|
|
+
|
|
|
+```ts
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+function currentUserId() {
|
|
|
+ return String(userStore.info?.userId || userStore.info?.id || '')
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 工具栏 + 详情**
|
|
|
+
|
|
|
+与 Task 3 对称:工具栏「撤回」按钮(位置同报销);详情 `v-else-if="canWithdraw(viewData)"` 区块。
|
|
|
+
|
|
|
+确认文案改为:
|
|
|
+
|
|
|
+```ts
|
|
|
+'确定撤回该备用金申请?撤回后单据变为待提交,可修改后再提交。'
|
|
|
+```
|
|
|
+
|
|
|
+API 调用:`advanceApi.withdraw(row.id)`。
|
|
|
+
|
|
|
+`canWithdraw` / `handleToolbarWithdraw` / `handleViewWithdraw` / `handleWithdraw` 逻辑同 Task 3(把 `reimburseApi` 换成 `advanceApi`,提示文案用备用金)。
|
|
|
+
|
|
|
+列表行字段为 camelCase `applicantId`(`AdvanceApplyOut` 使用 `to_camel`)。
|
|
|
+
|
|
|
+- [ ] **Step 3: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add web-admin/src/views/bx/advance/apply/index.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(web-admin): add withdraw on advance apply page
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 小程序报销详情撤回
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `miniprogram/pages/bx/reimburse/detail.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1: 用户与可撤回判断**
|
|
|
+
|
|
|
+```js
|
|
|
+import { useUserStore } from '@/stores/user'
|
|
|
+
|
|
|
+const userStore = useUserStore()
|
|
|
+
|
|
|
+function currentUserId() {
|
|
|
+ const u = userStore.userInfo || {}
|
|
|
+ return String(u.id || u.userId || '')
|
|
|
+}
|
|
|
+
|
|
|
+const canWithdraw = computed(() => {
|
|
|
+ const d = detail.value
|
|
|
+ if (!d) return false
|
|
|
+ return d.status === 'pending' && String(d.applicantId || '') === currentUserId()
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 扩展 `showActions`**
|
|
|
+
|
|
|
+```js
|
|
|
+const showActions = computed(() => {
|
|
|
+ if (!detail.value) return false
|
|
|
+ const status = detail.value.status
|
|
|
+ return status === 'draft' || status === 'rejected' || canWithdraw.value
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 3: 底栏模板**
|
|
|
+
|
|
|
+在 `rejected` 模板之后增加:
|
|
|
+
|
|
|
+```vue
|
|
|
+ <template v-if="canWithdraw">
|
|
|
+ <button
|
|
|
+ class="btn btn-ghost"
|
|
|
+ style="flex: 1"
|
|
|
+ :disabled="actionLoading.withdraw"
|
|
|
+ @click="handleWithdraw"
|
|
|
+ >
|
|
|
+ 撤回
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+```
|
|
|
+
|
|
|
+`actionLoading` 增加 `withdraw: false`。
|
|
|
+
|
|
|
+- [ ] **Step 4: `handleWithdraw`**
|
|
|
+
|
|
|
+```js
|
|
|
+async function handleWithdraw() {
|
|
|
+ if (!detail.value?.id) return
|
|
|
+ const { confirm } = await new Promise((resolve) => {
|
|
|
+ uni.showModal({
|
|
|
+ title: '撤回确认',
|
|
|
+ content: '确定撤回该报销单?撤回后将退回发票额度。',
|
|
|
+ success: resolve,
|
|
|
+ fail: () => resolve({ confirm: false })
|
|
|
+ })
|
|
|
+ })
|
|
|
+ if (!confirm) return
|
|
|
+ actionLoading.withdraw = true
|
|
|
+ try {
|
|
|
+ await reimburseApi.withdraw(detail.value.id)
|
|
|
+ uni.showToast({ title: '已撤回', icon: 'success' })
|
|
|
+ await loadDetail()
|
|
|
+ } catch (e) {
|
|
|
+ uni.showToast({ title: e?.message || '撤回失败', icon: 'none' })
|
|
|
+ } finally {
|
|
|
+ actionLoading.withdraw = false
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+(若页面已有封装 `showModal` 工具,优先复用;否则用上式。)
|
|
|
+
|
|
|
+参考:`bak/bx-mobile/src/views/reimburse/ReimburseDetailPage.vue` 的 `handleWithdraw`。
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add miniprogram/pages/bx/reimburse/detail.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(miniprogram): add reimburse detail withdraw
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 6: 小程序备用金详情撤回
|
|
|
+
|
|
|
+**Files:**
|
|
|
+- Modify: `miniprogram/pages/bx/advance/detail.vue`
|
|
|
+
|
|
|
+- [ ] **Step 1–4:** 与 Task 5 对称:
|
|
|
+
|
|
|
+- `useUserStore` + `canWithdraw` + 扩展 `showActions`
|
|
|
+- 底栏「撤回」按钮
|
|
|
+- `handleWithdraw` 调用 `advanceApi.withdraw`
|
|
|
+- 确认文案:`确定撤回该备用金申请?撤回后可修改再提交。`
|
|
|
+
|
|
|
+- [ ] **Step 5: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add miniprogram/pages/bx/advance/detail.vue
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+feat(miniprogram): add advance detail withdraw
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 7: 图谱更新与手工验收核对
|
|
|
+
|
|
|
+**Files:** 无代码逻辑变更(图谱脚本)
|
|
|
+
|
|
|
+- [x] **Step 1: 更新知识图谱**
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh all
|
|
|
+```
|
|
|
+
|
|
|
+若脚本仅支持分端:
|
|
|
+
|
|
|
+```bash
|
|
|
+./scripts/update-graph.sh backend
|
|
|
+./scripts/update-graph.sh frontend
|
|
|
+```
|
|
|
+
|
|
|
+- [x] **Step 2: 对照验收清单(手工)**
|
|
|
+
|
|
|
+按 spec「验收标准」逐条勾选:
|
|
|
+
|
|
|
+1. Web 报销:审批中 → 撤回 → 待提交 → 编辑再提交
|
|
|
+2. 非申请人按钮禁用;直调接口 400
|
|
|
+3. 报销撤回后发票额度回升
|
|
|
+4. Web 备用金对称
|
|
|
+5. 小程序两边详情可撤
|
|
|
+6. draft / rejected / approved 不可撤
|
|
|
+
|
|
|
+- [x] **Step 3: Commit 图谱(若有变更)**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add backend/graphify-out web-admin/graphify-out graphify-out 2>/dev/null || true
|
|
|
+git status
|
|
|
+# 若有图谱文件变更再 commit
|
|
|
+git commit -m "$(cat <<'EOF'
|
|
|
+chore: update graphify after bx apply withdraw
|
|
|
+
|
|
|
+EOF
|
|
|
+)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## Spec coverage self-check
|
|
|
+
|
|
|
+| Spec 要求 | Task |
|
|
|
+|-----------|------|
|
|
|
+| 报销复用既有 withdraw | Task 3、5(API 已有) |
|
|
|
+| 备用金新增 withdraw + 路由 | Task 1 |
|
|
|
+| 仅申请人 + pending | Task 1 测试 + 各端 `canWithdraw` |
|
|
|
+| 回 draft、清 current_node | Task 1 |
|
|
|
+| 报销释放发票额度 | 既有 Service;Task 3/5 文案提示 |
|
|
|
+| Web 工具栏单条 + 详情 | Task 3、4 |
|
|
|
+| 小程序详情底栏 | Task 5、6 |
|
|
|
+| 不做批量/原因/列表撤回 | 未列入任何 Task |
|
|
|
+| API 客户端 | Task 2 |
|
|
|
+
|
|
|
+## 执行说明
|
|
|
+
|
|
|
+- 每 Task 独立可测;先后端再 Web 再小程序。
|
|
|
+- 提交信息用 Conventional Commits;勿提交 `backend/app.pid`。
|
|
|
+- 改完代码后按 AGENTS.md 更新 graphify(Task 7)。
|