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
| 文件 | 职责 |
|---|---|
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) |
Task 1 进度纯函数 + 单测
→ Task 2 CRUD 列表/详情 + API
→ Task 3 前端 API + Tag 注册
→ Task 4 独立页壳 + 路由/菜单 + 协议单去 Tab
→ Task 5 抽出共享缴费确认弹窗(看板接入)
→ Task 6 按样本 Tab(列表 + 详情 + 确认)
→ Task 7 菜单 SQL + 图谱 + 冒烟
Files:
backend/app/services/sample_payment_progress.pyCreate: backend/tests/test_sample_payment_progress.py
[ ] Step 1: Write the failing tests
"""样本缴费进度:状态码与已缴/待缴汇总"""
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()
Run:
cd backend && source venv/bin/activate && pytest tests/test_sample_payment_progress.py -v
Expected: FAIL(模块不存在)
创建 backend/app/services/sample_payment_progress.py:
"""样本缴费方案进度汇总(列表「按样本」维度)"""
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,
}
cd backend && source venv/bin/activate && pytest tests/test_sample_payment_progress.py -v
Expected: PASS
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
)"
Files:
backend/app/crud/protocol_sample_payment.pybackend/app/api/protocol_sample_payment.pyCreate: backend/tests/test_sample_payment_list_helpers.py(若列表拼装可抽 helper 则单测;否则以手动冒烟 + 进度单测为准)
[ ] Step 1: 在 CRUD 增加列表与详情
在 protocol_sample_payment.py 末尾附近新增(签名与字段以设计文档 §4.1 为准):
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)。
"""
...
实现要点:
BizProtocolSamplePayment + join BizCollectionProtocol + outerjoin BizProtocolPartyCustomer,del_flag=0,tenant_no。apply_data_scope(q, BizCollectionProtocol, user_info, db, tenant_no)。keyword:protocol_no / sample_code / mother_name / mother_phone LIKE。pay_mode 精确匹配。due_date 落在区间(YYYYMMDD)。sample_payment 加载其 schedules + transfers(可批量 IN 查询),调用 compute_sample_payment_progress。progress_status 有值则过滤;再 total=len,page 切片。在 protocol_sample_payment.py 增加(注意路由顺序:/sample-payments/{id} 放在不会与其它静态路径冲突的位置;/payment-transfers 已存在,新路径独立):
@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)
若实现时抽出 _filter_and_page_progress(rows, progress_status, page, page_size),为其写测试:输入 5 条不同 progress,筛 bfyj,pageSize=2,断言 total/页内容正确且未先截断。
否则跳过独立测试文件,在 Task 7 冒烟验证。
cd backend && source venv/bin/activate && pytest tests/test_sample_payment_progress.py tests/test_protocol_sample_payment.py -v
Expected: PASS
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
)"
Files:
web-admin/src/api/protocol-sample-payment.tsModify: web-admin/src/utils/status-tag-registry.ts
[ ] Step 1: 增加 API 方法
在 protocol-sample-payment.ts 追加:
export function fetchSamplePaymentList(params?: {
page?: number
pageSize?: number
keyword?: string
payMode?: string
progressStatus?: string
installmentDueDateStart?: string
installmentDueDateEnd?: string
}) {
return request.get<PageList<Record<string, unknown>>>({
url: '/web/admin/sample-payments',
params
})
}
export function fetchSamplePaymentDetail(samplePaymentId: string) {
return request.get<Record<string, unknown>>({
url: `/web/admin/sample-payments/${samplePaymentId}`
})
}
在 status-tag-registry.ts 的 payMode 附近新增:
/** 按样本列表整体进度(勿与转账笔 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)
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
)"
Files:
web-admin/src/views/cell-storage/payment-info/index.vueweb-admin/src/views/cell-storage/protocol-audit/index.vueweb-admin/src/router/modules/sqxp.tsModify: backend/app/api/menu.py
[ ] Step 1: 重写 payment-info/index.vue 为双 Tab 壳
对齐 sample-info/index.vue 结构:
<!-- 缴费信息(流程看板 / 按样本) -->
<template>
<div class="page-container">
<section class="panel">
<nav class="pill-tab-nav" role="tablist" aria-label="视图类型">
<div class="pill-tab-nav__rail">
<div class="pill-tab-nav__progress" :style="pillProgressStyle" />
<button
v-for="(item, index) in entityTabs"
:key="item.key"
type="button"
class="pill-tab-nav__step"
:class="{
'is-active': activeTab === item.key,
'is-done': index < activeTabIndex
}"
role="tab"
:aria-selected="activeTab === item.key"
@click="activeTab = item.key"
>
<span class="pill-tab-nav__index">{{ index + 1 }}</span>
<span class="pill-tab-nav__text">
<span class="pill-tab-nav__title">{{ item.title }}</span>
<span class="pill-tab-nav__desc">{{ item.desc }}</span>
</span>
</button>
</div>
</nav>
<PaymentInfoTab v-if="activeTab === 'flow'" />
<PaymentBySampleTab v-else-if="activeTab === 'by-sample'" />
</section>
</div>
</template>
<script setup lang="ts">
import PaymentInfoTab from '../protocol-audit/PaymentInfoTab.vue'
import PaymentBySampleTab from './PaymentBySampleTab.vue'
defineOptions({ name: 'CellStoragePaymentInfo' })
const VALID_TABS = ['flow', 'by-sample'] as const
type EntityTabKey = (typeof VALID_TABS)[number]
const entityTabs: Array<{ key: EntityTabKey; title: string; desc: string }> = [
{ key: 'flow', title: '流程看板', desc: '按缴费流程状态处理' },
{ key: 'by-sample', title: '按样本', desc: '一协议一样本的缴费总览' }
]
const route = useRoute()
const activeTab = ref<EntityTabKey>('flow')
const activeTabIndex = computed(() =>
Math.max(0, entityTabs.findIndex((item) => item.key === activeTab.value))
)
const pillProgressStyle = computed(() => ({
width: `calc(${100 / entityTabs.length}% - 3px)`,
transform: `translateX(${activeTabIndex.value * 100}%)`
}))
onMounted(() => {
const tab = String(route.query.tab || '')
if (tab === 'payment-info') {
activeTab.value = 'flow'
return
}
if (VALID_TABS.includes(tab as EntityTabKey)) {
activeTab.value = tab as EntityTabKey
}
})
</script>
Task 4 先建一个最小 PaymentBySampleTab.vue 占位(el-empty「开发中」),Task 6 再填满,避免壳页 import 失败。
修改 protocol-audit/index.vue:
VALID_TABS / entityTabs 删除 payment-infoonMounted 增加:若 tab === 'payment-info',router.replace({ name: 'CellStoragePaymentInfo', query: { ...route.query, tab: 'flow' } })
[ ] Step 3: 路由 sqxp.ts
将 CellStoragePaymentInfo 块移到 CellStoragePresignOrder 与 CellStorageSampleInfo 之间,并改:
meta: {
title: '缴费信息',
keepAlive: true,
// 删除 isHide: true
menuCode: 'cellStorage:paymentInfo:list'
}
menu.py 硬编码树在细胞存储 children 的预签约与样本信息之间插入:
{
"id": "904b5173519411f1944300163e3bdd96",
"name": "CellStoragePaymentInfo",
"path": "payment-info",
"component": "/cell-storage/payment-info/index",
"meta": {
"title": "缴费信息",
"keepAlive": True,
"menuCode": "cellStorage:paymentInfo:list",
},
},
Run: pnpm dev(web-admin),打开 /#/cell-storage/payment-info、/#/cell-storage/protocol-audit?tab=payment-info。
Expected:独立页双 Tab;协议单仅 3 Tab;旧深链跳到独立页。
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
)"
Files:
web-admin/src/components/business/payment-receipt-confirm-dialog/index.vueModify: 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 建议:
const props = defineProps<{
modelValue: boolean
previewRow: Record<string, unknown> | null
/** transfer 新模型 vs 旧 detail */
isTransfer: boolean
targetId: string
}>()
const emit = defineEmits<{
'update:modelValue': [boolean]
saved: []
submitted: []
}>()
内部继续调用:
fetchUpdatePaymentTransfer / fetchUpdatePaymentDetailDraftfetchSubmitPaymentTransferFinance / 旧 detail 提交接口fetchUploadAttachment + useAttachmentDropZone字段与校验与看板现网一致,禁止删减。
PaymentInfoTab 删除内联 dialog 大段模板与相关本地 form 状态,改为:
<PaymentReceiptConfirmDialog
v-model="receiptDialogVisible"
:preview-row="receiptPreviewRow"
:is-transfer="receiptIsTransfer"
:target-id="receiptTargetId"
@saved="loadAllColumns()"
@submitted="loadAllColumns()"
/>
在流程看板「已缴费」列点「缴费确认」,确认弹窗可保存/提交。
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
)"
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)已缴清 X/Y 期)、进度状态 Tag、操作列 fixed="right" min-width ≥ 260v-if="row.canConfirm")、协议单list-pager-barfetchSamplePaymentListformatMoneyDisplayTag:payModeTagType / samplePaymentProgressTagType
[ ] Step 2: 详情弹窗
el-dialog.dialog-form-layout(宽约 900px,append-to-body,modal-class="protocol-dialog-mask"):
fetchSamplePaymentDetail(id)schedules 展开:期次、应缴、应缴日、已缴、欠缴、批次状态;内嵌 transfers 列表(金额、方式、时间、凭证、payStatus Tag)jfqrz 的期次显示「缴费确认」:
dfj 转账:用其 id 打开共享确认弹窗(isTransfer=true)fetchCreatePaymentTransfer({ scheduleId, transferAmount: arrears 或 due }),再用返回 id 打开弹窗ProtocolViewDialog(与看板一致)共享弹窗 @saved / @submitted:重新拉详情 + 刷新列表当前页。
/#/cell-storage/payment-info?tab=by-samplegit 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
)"
Files:
docs/sql/migrations/2026-10-05-unhide-payment-info-menu.sql./scripts/update-graph.sh all(或分别 backend/frontend)Update design status if desired
[x] Step 1: 菜单增量 SQL
创建 docs/sql/migrations/2026-10-05-unhide-payment-info-menu.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 可写清注释「前端模式无需执行」,仍保留文件作为文档。
./scripts/update-graph.sh all
[x] Step 3: 冒烟清单(全部勾掉再收工)
[x] 侧栏顺序:预签约 → 缴费信息 → 样本信息
[x] 协议单仅 3 Tab
[x] protocol-audit?tab=payment-info → 独立页流程看板
[x] 流程看板确认仍可用
[x] 按样本列表筛选/分页
[x] 详情确认后两边状态一致
[x] 财务确认页未回归破坏
[x] pytest tests/test_sample_payment_progress.py tests/test_protocol_sample_payment.py -v PASS
[x] Step 4: 设计文档状态
将 docs/superpowers/specs/2026-10-05-payment-info-standalone-menu-design.md 顶部状态改为「已实现」(全部冒烟通过后)。
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 要求 | Task |
|---|---|
| 独立菜单在预签约与样本信息之间 | 4, 7 |
| 双 Tab:流程看板 + 按样本 | 4, 6 |
| 协议单去掉缴费 Tab + 深链 | 4 |
| 一行一样本字段与进度 Tag | 1, 3, 6 |
| 详情可操作确认 | 5, 6 |
| 只读聚合 API + 复用写 API | 2 |
| 权限码复用 | 4, 7 |
| 旧模型不进按样本 | 2(主表仅 sample_payment) |
| 不做退费/改方案/导出 | 全计划未包含 |
payStatus.dfj 文案是「已缴费」;样本进度 dfj 必须用独立 samplePaymentProgress,文案「待缴费」。progressStatus 过滤:先算进度再分页,禁止先 LIMIT。