From 4849b405946c86e2488da7e1ccc3b9304af160bd Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Mon, 5 Oct 2026 12:53:47 +0800 Subject: [PATCH] docs: add implementation plan for standalone payment-info menu Break the approved design into TDD-friendly tasks for APIs, dual tabs, and per-sample UI. Co-authored-by: Cursor --- ...2026-10-05-payment-info-standalone-menu.md | 842 ++++++++++++++++++ 1 file changed, 842 insertions(+) create mode 100644 docs/superpowers/plans/2026-10-05-payment-info-standalone-menu.md diff --git a/docs/superpowers/plans/2026-10-05-payment-info-standalone-menu.md b/docs/superpowers/plans/2026-10-05-payment-info-standalone-menu.md new file mode 100644 index 00000000..3fedd96d --- /dev/null +++ b/docs/superpowers/plans/2026-10-05-payment-info-standalone-menu.md @@ -0,0 +1,842 @@ +# 缴费信息独立菜单 + 按样本维度 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:** 将缴费信息从采集协议单拆成独立菜单,并新增「按样本」列表/详情(可缴费确认),流程看板能力原样迁入。 + +**Architecture:** 后端新增只读聚合 `GET /sample-payments` 与详情;写操作继续走 `payment-transfers`。前端把 `payment-info` 从重定向壳改为双 Pill Tab 页(流程看板 + 按样本),协议单页去掉缴费 Tab 并做深链兼容。进度状态用纯函数计算并单测,列表过滤在查询层完成。 + +**Tech Stack:** MySQL 5.7 · FastAPI · SQLAlchemy · Vue 3 · Element Plus · pnpm · pytest/unittest + +**Design spec:** `docs/superpowers/specs/2026-10-05-payment-info-standalone-menu-design.md` + +--- + +## File Map + +| 文件 | 职责 | +|------|------| +| `backend/app/services/sample_payment_progress.py` | **新建** 进度状态/金额汇总纯函数 | +| `backend/tests/test_sample_payment_progress.py` | **新建** 进度纯函数单测 | +| `backend/app/crud/protocol_sample_payment.py` | 增加 `list_sample_payments_page` / `get_sample_payment_detail` | +| `backend/app/api/protocol_sample_payment.py` | 注册两个只读路由 | +| `backend/tests/test_sample_payment_list.py` | **新建** 列表聚合与过滤单测(尽量无 DB;或轻量 mock) | +| `web-admin/src/api/protocol-sample-payment.ts` | `fetchSamplePaymentList` / `fetchSamplePaymentDetail` | +| `web-admin/src/utils/status-tag-registry.ts` | `samplePaymentProgress` 注册表(勿与 `payStatus.dfj=已缴费` 混用) | +| `web-admin/src/views/cell-storage/payment-info/index.vue` | 独立页双 Tab 壳 | +| `web-admin/src/views/cell-storage/payment-info/PaymentBySampleTab.vue` | **新建** 按样本列表 + 详情 | +| `web-admin/src/components/business/payment-receipt-confirm-dialog/index.vue` | **新建** 从看板抽出的缴费确认弹窗 | +| `web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue` | 改用共享确认弹窗;仍作流程看板组件 | +| `web-admin/src/views/cell-storage/protocol-audit/index.vue` | 去掉缴费 Tab;深链重定向 | +| `web-admin/src/router/modules/sqxp.ts` | 取消 `isHide`,路由顺序:预签约 → 缴费信息 → 样本信息 | +| `backend/app/api/menu.py` | 细胞存储 children 插入缴费信息菜单项 | +| `docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql` | **新建** DB 菜单显示/排序(若库中已有该 menu_code) | + +--- + +## 依赖顺序 + +```text +Task 1 进度纯函数 + 单测 + → Task 2 CRUD 列表/详情 + API + → Task 3 前端 API + Tag 注册 + → Task 4 独立页壳 + 路由/菜单 + 协议单去 Tab + → Task 5 抽出共享缴费确认弹窗(看板接入) + → Task 6 按样本 Tab(列表 + 详情 + 确认) + → Task 7 菜单 SQL + 图谱 + 冒烟 +``` + +--- + +### Task 1: 样本缴费进度纯函数 + +**Files:** +- Create: `backend/app/services/sample_payment_progress.py` +- Create: `backend/tests/test_sample_payment_progress.py` + +- [ ] **Step 1: Write the failing tests** + +```python +"""样本缴费进度:状态码与已缴/待缴汇总""" + +from decimal import Decimal +import unittest + +from app.services.sample_payment_progress import ( + compute_sample_payment_progress, +) + + +class TestSamplePaymentProgress(unittest.TestCase): + def test_pending_when_no_transfers(self): + r = compute_sample_payment_progress( + schedules=[ + {"scheduleStatus": "open", "dueAmount": Decimal("1000")}, + ], + transfers=[], + ) + self.assertEqual(r["progressStatus"], "dfj") + self.assertEqual(r["paidAmount"], Decimal("0")) + self.assertEqual(r["dueRemainAmount"], Decimal("1000")) + self.assertEqual(r["clearedScheduleCount"], 0) + self.assertEqual(r["activeScheduleCount"], 1) + self.assertTrue(r["canConfirm"]) + + def test_confirming_wins_over_partial(self): + r = compute_sample_payment_progress( + schedules=[ + {"id": "s1", "scheduleStatus": "open", "dueAmount": Decimal("1000")}, + ], + transfers=[ + {"scheduleId": "s1", "payStatus": "yjf", "transferAmount": Decimal("200")}, + {"scheduleId": "s1", "payStatus": "jfqrz", "transferAmount": Decimal("100")}, + ], + ) + self.assertEqual(r["progressStatus"], "jfqrz") + self.assertEqual(r["paidAmount"], Decimal("200")) + self.assertTrue(r["canConfirm"] is False or r["canConfirm"] is True) + # 有确认中时 canConfirm=False(避免重复提交) + self.assertFalse(r["canConfirm"]) + + def test_partial_paid(self): + r = compute_sample_payment_progress( + schedules=[ + {"id": "s1", "scheduleStatus": "cleared", "dueAmount": Decimal("500")}, + {"id": "s2", "scheduleStatus": "open", "dueAmount": Decimal("500")}, + ], + transfers=[ + {"scheduleId": "s1", "payStatus": "yjf", "transferAmount": Decimal("500")}, + ], + ) + self.assertEqual(r["progressStatus"], "bfyj") + self.assertEqual(r["clearedScheduleCount"], 1) + self.assertEqual(r["activeScheduleCount"], 2) + self.assertTrue(r["canConfirm"]) + + def test_all_cleared(self): + r = compute_sample_payment_progress( + schedules=[ + {"id": "s1", "scheduleStatus": "cleared", "dueAmount": Decimal("500")}, + {"id": "s2", "scheduleStatus": "void", "dueAmount": Decimal("500")}, + ], + transfers=[ + {"scheduleId": "s1", "payStatus": "yjf", "transferAmount": Decimal("500")}, + ], + ) + self.assertEqual(r["progressStatus"], "yjq") + self.assertEqual(r["activeScheduleCount"], 1) + self.assertEqual(r["clearedScheduleCount"], 1) + self.assertFalse(r["canConfirm"]) + self.assertEqual(r["dueRemainAmount"], Decimal("0")) + + def test_void_schedules_ignored(self): + r = compute_sample_payment_progress( + schedules=[ + {"scheduleStatus": "void", "dueAmount": Decimal("9999")}, + {"scheduleStatus": "open", "dueAmount": Decimal("100")}, + ], + transfers=[], + ) + self.assertEqual(r["activeScheduleCount"], 1) + self.assertEqual(r["dueRemainAmount"], Decimal("100")) + + +if __name__ == "__main__": + unittest.main() +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: + +```bash +cd backend && source venv/bin/activate && pytest tests/test_sample_payment_progress.py -v +``` + +Expected: FAIL(模块不存在) + +- [ ] **Step 3: Implement pure function** + +创建 `backend/app/services/sample_payment_progress.py`: + +```python +"""样本缴费方案进度汇总(列表「按样本」维度)""" + +from __future__ import annotations + +from decimal import Decimal +from typing import Any, Dict, Iterable, List, Mapping, Optional + + +def _dec(v: Any) -> Decimal: + if v is None or v == "": + return Decimal("0") + return Decimal(str(v)) + + +def _status(row: Mapping[str, Any], *keys: str) -> str: + for k in keys: + if k in row and row[k] is not None: + return str(row[k]).strip() + return "" + + +def compute_sample_payment_progress( + *, + schedules: Iterable[Mapping[str, Any]], + transfers: Iterable[Mapping[str, Any]], +) -> Dict[str, Any]: + """ + progressStatus 优先级:jfqrz > yjq > bfyj > dfj + 已缴仅计 pay_status=yjf;有效期排除 schedule_status=void + canConfirm:无 jfqrz,且存在有效未清期(欠缴>0)或可编辑的 dfj 草稿 + """ + sch_list: List[Mapping[str, Any]] = list(schedules or []) + tr_list: List[Mapping[str, Any]] = list(transfers or []) + + active = [ + s + for s in sch_list + if _status(s, "scheduleStatus", "schedule_status") != "void" + ] + active_schedule_count = len(active) + cleared_schedule_count = sum( + 1 + for s in active + if _status(s, "scheduleStatus", "schedule_status") == "cleared" + ) + + paid = sum( + ( + _dec(t.get("transferAmount", t.get("transfer_amount"))) + for t in tr_list + if _status(t, "payStatus", "pay_status") == "yjf" + ), + Decimal("0"), + ) + due_total = sum((_dec(s.get("dueAmount", s.get("due_amount"))) for s in active), Decimal("0")) + due_remain = due_total - paid + if due_remain < 0: + due_remain = Decimal("0") + + has_jfqrz = any(_status(t, "payStatus", "pay_status") == "jfqrz" for t in tr_list) + + if has_jfqrz: + progress = "jfqrz" + elif active_schedule_count > 0 and cleared_schedule_count == active_schedule_count: + progress = "yjq" + elif paid > 0: + progress = "bfyj" + else: + progress = "dfj" + + can_confirm = (not has_jfqrz) and any( + _status(s, "scheduleStatus", "schedule_status") != "cleared" for s in active + ) + + return { + "progressStatus": progress, + "paidAmount": paid, + "dueRemainAmount": due_remain, + "clearedScheduleCount": cleared_schedule_count, + "activeScheduleCount": active_schedule_count, + "canConfirm": can_confirm, + } +``` + +- [ ] **Step 4: Run tests to verify they pass** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_sample_payment_progress.py -v +``` + +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/services/sample_payment_progress.py backend/tests/test_sample_payment_progress.py +git commit -m "$(cat <<'EOF' +feat(backend): add sample payment progress aggregation helper + +Pure function for per-sample list progressStatus, paid/due totals, and canConfirm. +EOF +)" +``` + +--- + +### Task 2: 列表/详情 CRUD + API + +**Files:** +- Modify: `backend/app/crud/protocol_sample_payment.py` +- Modify: `backend/app/api/protocol_sample_payment.py` +- Create: `backend/tests/test_sample_payment_list_helpers.py`(若列表拼装可抽 helper 则单测;否则以手动冒烟 + 进度单测为准) + +- [ ] **Step 1: 在 CRUD 增加列表与详情** + +在 `protocol_sample_payment.py` 末尾附近新增(签名与字段以设计文档 §4.1 为准): + +```python +def list_sample_payments_page( + db: Session, + *, + tenant_no: str, + keyword: Optional[str] = None, + pay_mode: Optional[str] = None, + progress_status: Optional[str] = None, + due_date_start: Optional[str] = None, + due_date_end: Optional[str] = None, + page: int = 1, + page_size: int = 10, + user_info=None, +) -> Tuple[List[Dict[str, Any]], int]: + """ + 主表 biz_protocol_sample_payment JOIN 协议 + 当事人。 + 数据权限:apply_data_scope 挂在 BizCollectionProtocol 上。 + progress_status 过滤:先按基础条件取出候选 id(可分批),对每个方案加载 schedules/transfers, + 用 compute_sample_payment_progress 判定后过滤,再切片分页。 + 注意:候选集过大时改为 SQL EXISTS 优化;P0 可接受「基础过滤后内存算进度再分页」, + 但 MUST 先过滤进度再 slice,禁止「先 LIMIT 再筛进度」。 + """ + ... + + +def get_sample_payment_detail( + db: Session, + *, + sample_payment_id: str, + tenant_no: str, + user_info=None, +) -> Optional[Dict[str, Any]]: + """ + 复用 get bundle 单条 / _sample_payment_to_dict,并补: + protocolNo, protocolStatus, voidType, motherName, motherPhone, 以及 progress 汇总字段。 + 协议不在 data_scope → 返回 None(API 层 403)。 + """ + ... +``` + +实现要点: + +1. 基础 query:`BizProtocolSamplePayment` + join `BizCollectionProtocol` + outerjoin `BizProtocolPartyCustomer`,`del_flag=0`,`tenant_no`。 +2. `apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no)`。 +3. `keyword`:`protocol_no` / `sample_code` / `mother_name` / `mother_phone` LIKE。 +4. `pay_mode` 精确匹配。 +5. 应缴日:EXISTS 未作废 schedule 的 `due_date` 落在区间(YYYYMMDD)。 +6. 对每个候选 `sample_payment` 加载其 schedules + transfers(可批量 IN 查询),调用 `compute_sample_payment_progress`。 +7. 若 `progress_status` 有值则过滤;再 `total=len`,`page` 切片。 +8. 列表行字段:设计文档 §4.1 摘要字段,**不含**完整 schedules 树。 + +- [ ] **Step 2: 注册 API** + +在 `protocol_sample_payment.py` 增加(注意路由顺序:`/sample-payments/{id}` 放在不会与其它静态路径冲突的位置;`/payment-transfers` 已存在,新路径独立): + +```python +@router.get("/sample-payments") +async def list_sample_payments( + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100, alias="pageSize"), + keyword: Optional[str] = None, + payMode: Optional[str] = Query(None, alias="payMode"), + progressStatus: Optional[str] = Query(None, alias="progressStatus"), + installmentDueDateStart: Optional[str] = Query(None, alias="installmentDueDateStart"), + installmentDueDateEnd: Optional[str] = Query(None, alias="installmentDueDateEnd"), + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _get_tenant_no(db, user) + items, total = psp_svc.list_sample_payments_page( + db, + tenant_no=tenant_no, + keyword=keyword, + pay_mode=payMode, + progress_status=progressStatus, + due_date_start=installmentDueDateStart, + due_date_end=installmentDueDateEnd, + page=page, + page_size=page_size, + user_info=user, + ) + return success_response( + {"list": items, "total": total, "page": page, "pageSize": page_size} + ) + + +@router.get("/sample-payments/{sample_payment_id}") +async def get_sample_payment( + sample_payment_id: str, + user: dict = Depends(require_admin_or_super_admin), + db: Session = Depends(get_db), +): + tenant_no = _get_tenant_no(db, user) + data = psp_svc.get_sample_payment_detail( + db, + sample_payment_id=sample_payment_id, + tenant_no=tenant_no, + user_info=user, + ) + if not data: + return error_response(403, "无权查看或记录不存在") + return success_response(data) +``` + +- [ ] **Step 3: 补一条进度过滤顺序的单元测试(helper)** + +若实现时抽出 `_filter_and_page_progress(rows, progress_status, page, page_size)`,为其写测试:输入 5 条不同 progress,筛 `bfyj`,pageSize=2,断言 total/页内容正确且未先截断。 + +否则跳过独立测试文件,在 Task 7 冒烟验证。 + +- [ ] **Step 4: Run progress tests still pass** + +```bash +cd backend && source venv/bin/activate && pytest tests/test_sample_payment_progress.py tests/test_protocol_sample_payment.py -v +``` + +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/crud/protocol_sample_payment.py backend/app/api/protocol_sample_payment.py +git add backend/tests/test_sample_payment_list_helpers.py # if created +git commit -m "$(cat <<'EOF' +feat(backend): add sample-payments list and detail read APIs + +Support per-sample payment overview for the standalone payment-info menu. +EOF +)" +``` + +--- + +### Task 3: 前端 API 与 Tag 注册 + +**Files:** +- Modify: `web-admin/src/api/protocol-sample-payment.ts` +- Modify: `web-admin/src/utils/status-tag-registry.ts` + +- [ ] **Step 1: 增加 API 方法** + +在 `protocol-sample-payment.ts` 追加: + +```typescript +export function fetchSamplePaymentList(params?: { + page?: number + pageSize?: number + keyword?: string + payMode?: string + progressStatus?: string + installmentDueDateStart?: string + installmentDueDateEnd?: string +}) { + return request.get>>({ + url: '/web/admin/sample-payments', + params + }) +} + +export function fetchSamplePaymentDetail(samplePaymentId: string) { + return request.get>({ + url: `/web/admin/sample-payments/${samplePaymentId}` + }) +} +``` + +- [ ] **Step 2: 注册进度 Tag(独立于 payStatus)** + +在 `status-tag-registry.ts` 的 `payMode` 附近新增: + +```typescript +/** 按样本列表整体进度(勿与转账笔 payStatus 混用:payStatus.dfj=已缴费) */ +export const samplePaymentProgress: StatusRegistry = { + dfj: { label: '待缴费', tagType: 'info' }, + jfqrz: { label: '缴费确认中', tagType: 'warning' }, + bfyj: { label: '部分已缴', tagType: 'primary' }, + yjq: { label: '已缴清', tagType: 'success' } +} +export const samplePaymentProgressLabel = createLabelGetter(samplePaymentProgress) +export const samplePaymentProgressTagType = createTagTypeGetter(samplePaymentProgress) +``` + +- [ ] **Step 3: Commit** + +```bash +git add web-admin/src/api/protocol-sample-payment.ts web-admin/src/utils/status-tag-registry.ts +git commit -m "$(cat <<'EOF' +feat(web-admin): add sample-payments API clients and progress tags +EOF +)" +``` + +--- + +### Task 4: 独立页壳、路由/菜单、协议单去 Tab + +**Files:** +- Modify: `web-admin/src/views/cell-storage/payment-info/index.vue` +- Modify: `web-admin/src/views/cell-storage/protocol-audit/index.vue` +- Modify: `web-admin/src/router/modules/sqxp.ts` +- Modify: `backend/app/api/menu.py` + +- [ ] **Step 1: 重写 `payment-info/index.vue` 为双 Tab 壳** + +对齐 `sample-info/index.vue` 结构: + +```vue + + + + +``` + +Task 4 先建一个最小 `PaymentBySampleTab.vue` 占位(`el-empty`「开发中」),Task 6 再填满,避免壳页 import 失败。 + +- [ ] **Step 2: 协议单去掉缴费 Tab + 深链重定向** + +修改 `protocol-audit/index.vue`: + +- `VALID_TABS` / `entityTabs` 删除 `payment-info` +- `onMounted` 增加:若 `tab === 'payment-info'`,`router.replace({ name: 'CellStoragePaymentInfo', query: { ...route.query, tab: 'flow' } })` + +- [ ] **Step 3: 路由 `sqxp.ts`** + +将 `CellStoragePaymentInfo` 块移到 `CellStoragePresignOrder` 与 `CellStorageSampleInfo` 之间,并改: + +```ts +meta: { + title: '缴费信息', + keepAlive: true, + // 删除 isHide: true + menuCode: 'cellStorage:paymentInfo:list' +} +``` + +- [ ] **Step 4: `menu.py` 硬编码树** + +在细胞存储 children 的预签约与样本信息之间插入: + +```python +{ + "id": "904b5173519411f1944300163e3bdd96", + "name": "CellStoragePaymentInfo", + "path": "payment-info", + "component": "/cell-storage/payment-info/index", + "meta": { + "title": "缴费信息", + "keepAlive": True, + "menuCode": "cellStorage:paymentInfo:list", + }, +}, +``` + +- [ ] **Step 5: 手动冒烟壳页** + +Run: `pnpm dev`(web-admin),打开 `/#/cell-storage/payment-info`、`/#/cell-storage/protocol-audit?tab=payment-info`。 + +Expected:独立页双 Tab;协议单仅 3 Tab;旧深链跳到独立页。 + +- [ ] **Step 6: Commit** + +```bash +git add web-admin/src/views/cell-storage/payment-info/index.vue \ + web-admin/src/views/cell-storage/payment-info/PaymentBySampleTab.vue \ + web-admin/src/views/cell-storage/protocol-audit/index.vue \ + web-admin/src/router/modules/sqxp.ts \ + backend/app/api/menu.py +git commit -m "$(cat <<'EOF' +feat: split payment-info into standalone menu with dual tabs + +Remove payment tab from protocol-audit and keep deep-link redirect. +EOF +)" +``` + +--- + +### Task 5: 抽出共享缴费确认弹窗 + +**Files:** +- Create: `web-admin/src/components/business/payment-receipt-confirm-dialog/index.vue` +- Modify: `web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue` + +- [ ] **Step 1: 抽取弹窗** + +把 `PaymentInfoTab.vue` 中「缴费确认」`el-dialog`(含表单、附件上传、仅保存、提交缴费确认)抽到: + +`web-admin/src/components/business/payment-receipt-confirm-dialog/index.vue` + +Props / Emits 建议: + +```ts +const props = defineProps<{ + modelValue: boolean + previewRow: Record | null + /** transfer 新模型 vs 旧 detail */ + isTransfer: boolean + targetId: string +}>() +const emit = defineEmits<{ + 'update:modelValue': [boolean] + saved: [] + submitted: [] +}>() +``` + +内部继续调用: + +- `fetchUpdatePaymentTransfer` / `fetchUpdatePaymentDetailDraft` +- `fetchSubmitPaymentTransferFinance` / 旧 detail 提交接口 +- `fetchUploadAttachment` + `useAttachmentDropZone` + +字段与校验与看板现网一致,禁止删减。 + +- [ ] **Step 2: 看板改用共享组件** + +`PaymentInfoTab` 删除内联 dialog 大段模板与相关本地 form 状态,改为: + +```vue + +``` + +- [ ] **Step 3: 看板回归** + +在流程看板「已缴费」列点「缴费确认」,确认弹窗可保存/提交。 + +- [ ] **Step 4: Commit** + +```bash +git add web-admin/src/components/business/payment-receipt-confirm-dialog/index.vue \ + web-admin/src/views/cell-storage/protocol-audit/PaymentInfoTab.vue +git commit -m "$(cat <<'EOF' +refactor(web-admin): extract shared payment receipt confirm dialog + +Reuse the same confirm form for flow kanban and per-sample views. +EOF +)" +``` + +--- + +### Task 6: 按样本 Tab(列表 + 详情 + 确认) + +**Files:** +- Modify: `web-admin/src/views/cell-storage/payment-info/PaymentBySampleTab.vue` + +- [ ] **Step 1: 实现筛选 + 表格列表** + +结构对齐 `SampleCollectionInfoTab.vue`(P02): + +- `filter-block`:关键词、缴费方式(`dj`/`fq`)、进度状态(`samplePaymentProgress` 四码)、应缴日期区间 +- `list-body.list-wrap` + `el-table.list-table` + `stripe`(不加 border) +- 列:协议号、客户(姓名+电话次要)、样本编码、缴费方式 Tag、合同额、已缴、待缴、进度文案(`已缴清 X/Y 期`)、进度状态 Tag、操作列 `fixed="right"` `min-width` ≥ 260 +- 操作:查看、缴费确认(`v-if="row.canConfirm"`)、协议单 +- 分页:`list-pager-bar` +- 数据:`fetchSamplePaymentList` +- 金额:`formatMoneyDisplay` +- Tag:`payModeTagType` / `samplePaymentProgressTagType` + +- [ ] **Step 2: 详情弹窗** + +`el-dialog.dialog-form-layout`(宽约 900px,`append-to-body`,`modal-class="protocol-dialog-mask"`): + +1. 打开时 `fetchSamplePaymentDetail(id)` +2. 上半摘要:协议号、客户、样本、模式、合同额、已缴/待缴、进度、协议状态 Tag +3. 下半按 `schedules` 展开:期次、应缴、应缴日、已缴、欠缴、批次状态;内嵌 `transfers` 列表(金额、方式、时间、凭证、`payStatus` Tag) +4. 对欠缴 > 0 且无该期 `jfqrz` 的期次显示「缴费确认」: + - 若该期已有 `dfj` 转账:用其 `id` 打开共享确认弹窗(`isTransfer=true`) + - 若无转账:先 `fetchCreatePaymentTransfer({ scheduleId, transferAmount: arrears 或 due })`,再用返回 id 打开弹窗 +5. 列表行「缴费确认」:打开详情后定位最早可确认期,再走同上逻辑 +6. 「协议单」:`ProtocolViewDialog`(与看板一致) + +- [ ] **Step 3: 确认后刷新** + +共享弹窗 `@saved` / `@submitted`:重新拉详情 + 刷新列表当前页。 + +- [ ] **Step 4: 手动验证** + +1. 打开 `/#/cell-storage/payment-info?tab=by-sample` +2. 能搜到已配置按样本缴费的数据 +3. 点查看看各期/转账 +4. 待缴费样本可提交财务确认;提交后进度变为「缴费确认中」 +5. 流程看板同笔记录也出现在「缴费确认中」列 + +- [ ] **Step 5: Commit** + +```bash +git add web-admin/src/views/cell-storage/payment-info/PaymentBySampleTab.vue +git commit -m "$(cat <<'EOF' +feat(web-admin): add per-sample payment list and operable detail + +Show one row per sample with schedules/transfers and shared receipt confirm. +EOF +)" +``` + +--- + +### Task 7: 菜单 SQL、图谱、冒烟收尾 + +**Files:** +- Create: `docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql` +- Run: `./scripts/update-graph.sh all`(或分别 backend/frontend) +- Update design status if desired + +- [ ] **Step 1: 菜单增量 SQL** + +创建 `docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql`: + +```sql +-- 缴费信息:取消隐藏,排序夹在预签约订单与样本信息管理之间 +-- 说明:若环境使用前端固定菜单(menu.py / sqxp),本脚本用于 DB 菜单模式环境 + +-- 1) 若已存在该权限码菜单:显示并调整排序 +UPDATE sys_menu +SET del_flag = 0, + visible = '0', -- 按本库「显示」约定调整;若字段为 is_hide/status 则改为对应值 + order_num = ( + SELECT mid_order FROM ( + SELECT COALESCE(MAX(m2.order_num), 0) AS mid_order + FROM sys_menu m2 + WHERE m2.perms IN ('cell-storage:presign-order:list', 'cell-storage:presign-order:list') + OR m2.path LIKE '%presign-order%' + ) t + ) + 1 +WHERE perms = 'cellStorage:paymentInfo:list' + OR path LIKE '%payment-info%'; + +-- 2) 若库中无此菜单:由实施时对照 menu.py 插入完整行(id 用 904b5173519411f1944300163e3bdd96) +-- INSERT ... 仅在确认表结构后由实施填写,避免错误列名上线 +``` + +实施时打开真实 `sys_menu` 表结构(`docs/sql/zkxw.sql` 中 `CREATE TABLE sys_menu`)核对列名(`visible`/`status`/`is_frame`/`perms`/`path`/`component`/`order_num`/`parent_id`),把 UPDATE/INSERT 写成可直接执行的最终版;**禁止带着未核对的占位列上生产**。 + +若当前环境完全走 `menu.py` 前端模式、库中无该菜单,SQL 可写清注释「前端模式无需执行」,仍保留文件作为文档。 + +- [ ] **Step 2: 更新图谱** + +```bash +./scripts/update-graph.sh all +``` + +- [ ] **Step 3: 冒烟清单(全部勾掉再收工)** + +- [ ] 侧栏顺序:预签约 → 缴费信息 → 样本信息 +- [ ] 协议单仅 3 Tab +- [ ] `protocol-audit?tab=payment-info` → 独立页流程看板 +- [ ] 流程看板确认仍可用 +- [ ] 按样本列表筛选/分页 +- [ ] 详情确认后两边状态一致 +- [ ] 财务确认页未回归破坏 +- [ ] `pytest tests/test_sample_payment_progress.py tests/test_protocol_sample_payment.py -v` PASS + +- [ ] **Step 4: 设计文档状态** + +将 `docs/superpowers/specs/2026-10-05-payment-info-standalone-menu-design.md` 顶部状态改为「已实现」(全部冒烟通过后)。 + +- [ ] **Step 5: Commit** + +```bash +git add docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql \ + backend/graphify-out web-admin/graphify-out \ + docs/superpowers/specs/2026-10-05-payment-info-standalone-menu-design.md +git commit -m "$(cat <<'EOF' +chore: menu SQL, graph sync, and mark payment-info split done +EOF +)" +``` + +--- + +## Spec Coverage Checklist + +| Spec 要求 | Task | +|-----------|------| +| 独立菜单在预签约与样本信息之间 | 4, 7 | +| 双 Tab:流程看板 + 按样本 | 4, 6 | +| 协议单去掉缴费 Tab + 深链 | 4 | +| 一行一样本字段与进度 Tag | 1, 3, 6 | +| 详情可操作确认 | 5, 6 | +| 只读聚合 API + 复用写 API | 2 | +| 权限码复用 | 4, 7 | +| 旧模型不进按样本 | 2(主表仅 sample_payment) | +| 不做退费/改方案/导出 | 全计划未包含 | + +## Placeholder / Ambiguity Notes(已锁定) + +- `payStatus.dfj` 文案是「已缴费」;样本进度 `dfj` 必须用独立 `samplePaymentProgress`,文案「待缴费」。 +- 列表 `progressStatus` 过滤:**先算进度再分页**,禁止先 LIMIT。 +- 共享确认弹窗优先抽取(Task 5),避免两套表单漂移。