Просмотр исходного кода

docs: add OA receive account implementation plan.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 месяцев назад
Родитель
Сommit
563da3626b
1 измененных файлов с 763 добавлено и 0 удалено
  1. 763 0
      docs/superpowers/plans/2026-07-30-oa-receive-account.md

+ 763 - 0
docs/superpowers/plans/2026-07-30-oa-receive-account.md

@@ -0,0 +1,763 @@
+# OA 收款账户管理 +「收款账户」控件 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:** 在 OA 审批下交付租户级「收款账户管理」CRUD,并注册 FormCreate `receiveAccount` 多选控件(存完整快照,详情/打印完整展示)。
+
+**Architecture:** 新建表 `oa_receive_account` + 挂在 `/web/admin/oa-approval` 的 CRUD/options API;管理页对齐 `roles/index.vue`;控件四层扩展对齐 `person`/`dept`/`money`(value 工具 → Vue 控件 → DragRule → setup 双侧注册);后端发布白名单与发起校验增加 `receiveAccount`。
+
+**Tech Stack:** FastAPI · SQLAlchemy · Pydantic v2 · pytest · Vue 3 · FormCreate / `@form-create/designer` · Element Plus · pnpm
+
+**Spec:** `docs/superpowers/specs/2026-07-30-oa-receive-account-design.md`  
+**Demo:** `docs/mockups/oa-receive-account-demo.html`
+
+---
+
+## File map
+
+| Path | Responsibility |
+|------|----------------|
+| Create: `docs/sql/oa_receive_account.sql` | 建表 + 菜单 INSERT(幂等) |
+| Modify: `docs/sql/zkxw.sql` | 同步表结构与菜单基线(可选段落或注明以 alter 脚本为准) |
+| Modify: `backend/app/models/models.py` | `OaReceiveAccount` 模型 |
+| Create: `backend/app/crud/oa_receive_account.py` | list/options/get/create/update/soft_delete |
+| Modify: `backend/app/api/oa_approval.py` | REST endpoints + Body schemas |
+| Create: `backend/app/services/oa_receive_account.py` | `validate_receive_account_form_values` |
+| Create: `backend/tests/test_oa_receive_account.py` | 校验单测 + 类型必填校验单测 |
+| Modify: `backend/app/services/oa_publish.py` | `FILLABLE_TYPES` 增加 `receiveAccount` |
+| Modify: `backend/app/services/oa_engine.py` | `start_instance` 挂入校验 |
+| Modify: `web-admin/src/api/oaApproval.ts` | 前端 API |
+| Create: `web-admin/src/utils/oa-receive-account-value.ts` | 快照类型 / normalize / format |
+| Create: `web-admin/src/utils/oa-receive-account-value.test.ts` | Node 单测 |
+| Create: `web-admin/src/utils/oa-receive-account-rule.ts` | DragRule |
+| Create: `web-admin/src/utils/oa-form-create-receive-account.ts` | setup |
+| Create: `web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue` | 控件 |
+| Modify: `web-admin/src/main.ts` | setup 调用 |
+| Modify: `web-admin/src/oa-fc-host/main.ts` | setup 调用 |
+| Modify: `web-admin/src/utils/oa-approval-print.ts` | 打印分支 |
+| Create: `web-admin/src/views/oa-approval/receive-accounts/index.vue` | 管理页 |
+| Modify: `web-admin/src/router/modules/sqxp.ts` | 前端路由子项 |
+| Modify: `docs/superpowers/specs/2026-07-30-oa-receive-account-design.md` | 状态 → 已实现 |
+
+**菜单 ID:** `OA000000000000000000000000000108`(order_num=10)
+
+---
+
+### Task 1: SQL 建表 + 菜单
+
+**Files:**
+- Create: `docs/sql/oa_receive_account.sql`
+
+- [ ] **Step 1: 写迁移 SQL**
+
+```sql
+-- docs/sql/oa_receive_account.sql
+CREATE TABLE IF NOT EXISTS `oa_receive_account` (
+  `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
+  `account_type` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'alipay/personal/corporate',
+  `account_name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '户名',
+  `account_no` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '账号',
+  `bank_name` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '银行',
+  `bank_province` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '省',
+  `bank_city` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '市',
+  `bank_branch` varchar(256) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '支行',
+  `tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
+  `order_num` int(11) DEFAULT NULL COMMENT '排序号',
+  `remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
+  `create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
+  `create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
+  PRIMARY KEY (`id`) USING BTREE,
+  KEY `idx_ora_tenant` (`tenant_no`,`del_flag`) USING BTREE,
+  KEY `idx_ora_type` (`tenant_no`,`account_type`,`del_flag`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='OA收款账户';
+
+INSERT INTO `sys_menu` (
+  `id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`,
+  `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`,
+  `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`
+) SELECT
+  'OA000000000000000000000000000108', '收款账户管理', 'OA000000000000000000000000000001',
+  'C', 'receive-accounts', '/oa-approval/receive-accounts/index', 'oa:receive-accounts:list',
+  1, '1', '1', 'zkxw', 10, 'OA收款账户主数据',
+  NULL, '', NOW(), NULL, NOW(), 0
+FROM DUAL
+WHERE NOT EXISTS (
+  SELECT 1 FROM `sys_menu` WHERE `id` = 'OA000000000000000000000000000108' AND `del_flag` = 0
+);
+```
+
+- [ ] **Step 2: 在本地/测试库执行**
+
+```bash
+# 使用项目现有 DB 连接执行;示例:
+mysql -h... -u... -p... zkxw < docs/sql/oa_receive_account.sql
+```
+
+Expected: 表存在;菜单可查到。
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add docs/sql/oa_receive_account.sql
+git commit -m "docs(sql): add oa_receive_account table and menu."
+```
+
+---
+
+### Task 2: SQLAlchemy 模型
+
+**Files:**
+- Modify: `backend/app/models/models.py`(紧挨 `OaRole` / `OaRoleUser` 之后插入)
+
+- [ ] **Step 1: 增加模型**
+
+```python
+class OaReceiveAccount(Base):
+    """OA收款账户 oa_receive_account"""
+
+    __tablename__ = "oa_receive_account"
+
+    id = Column(String(64), primary_key=True, comment="主键ID")
+    account_type = Column(String(16), nullable=False, comment="alipay/personal/corporate")
+    account_name = Column(String(128), nullable=False, comment="户名")
+    account_no = Column(String(128), nullable=False, comment="账号")
+    bank_name = Column(String(128), nullable=True, comment="银行")
+    bank_province = Column(String(64), nullable=True, comment="省")
+    bank_city = Column(String(64), nullable=True, comment="市")
+    bank_branch = Column(String(256), nullable=True, comment="支行")
+    tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
+    order_num = Column(Integer, nullable=True, comment="排序号")
+    remark = Column(String(512), nullable=True, comment="备注")
+    create_by = Column(String(64), nullable=True, comment="创建人id")
+    create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
+    create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
+    update_by = Column(String(64), nullable=True, comment="更新人id")
+    update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
+    del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
+```
+
+确保文件顶部已有 `datetime`、`Column`、`String`、`Integer`、`DateTime`、`SmallInteger`、`Base` 导入(与现有 OA 模型一致)。
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add backend/app/models/models.py
+git commit -m "feat(backend): add OaReceiveAccount model."
+```
+
+---
+
+### Task 3: CRUD + 类型规范化(TDD 校验逻辑)
+
+**Files:**
+- Create: `backend/app/crud/oa_receive_account.py`
+- Create: `backend/app/services/oa_receive_account.py`(先放 `normalize_account_payload` + 表单校验;也可同文件)
+- Create: `backend/tests/test_oa_receive_account.py`
+
+- [ ] **Step 1: 写失败单测(payload 规范化 + form 校验)**
+
+```python
+# backend/tests/test_oa_receive_account.py
+import unittest
+
+from app.services.oa_receive_account import (
+    normalize_account_fields,
+    validate_receive_account_form_values,
+)
+
+
+class TestNormalizeAccountFields(unittest.TestCase):
+    def test_alipay_clears_bank(self):
+        ok, err, data = normalize_account_fields(
+            {
+                "accountType": "alipay",
+                "accountName": "张三",
+                "accountNo": "13800138000",
+                "bankName": "工行",
+                "bankProvince": "安徽",
+                "bankCity": "合肥",
+                "bankBranch": "xx支行",
+                "remark": "r",
+            }
+        )
+        self.assertTrue(ok)
+        self.assertIsNone(err)
+        self.assertEqual(data["account_type"], "alipay")
+        self.assertEqual(data["account_name"], "张三")
+        self.assertEqual(data["bank_name"], "")
+        self.assertEqual(data["bank_province"], "")
+        self.assertEqual(data["bank_city"], "")
+        self.assertEqual(data["bank_branch"], "")
+
+    def test_personal_requires_bank(self):
+        ok, err, _ = normalize_account_fields(
+            {
+                "accountType": "personal",
+                "accountName": "李四",
+                "accountNo": "6222",
+            }
+        )
+        self.assertFalse(ok)
+        self.assertTrue(err)
+
+    def test_personal_ok(self):
+        ok, err, data = normalize_account_fields(
+            {
+                "accountType": "personal",
+                "accountName": "李四",
+                "accountNo": "6222",
+                "bankName": "工行",
+                "bankProvince": "安徽省",
+                "bankCity": "合肥市",
+                "bankBranch": "长江中路支行",
+            }
+        )
+        self.assertTrue(ok)
+        self.assertEqual(data["bank_name"], "工行")
+
+
+class TestValidateReceiveAccountFormValues(unittest.TestCase):
+    def test_ok_array(self):
+        schema = {
+            "rule": [
+                {
+                    "type": "receiveAccount",
+                    "field": "acc",
+                    "title": "收款账户",
+                    "$required": True,
+                }
+            ]
+        }
+        data = {
+            "acc": [
+                {
+                    "id": "1",
+                    "accountType": "alipay",
+                    "accountName": "张三",
+                    "accountNo": "138",
+                    "bankName": "",
+                    "bankProvince": "",
+                    "bankCity": "",
+                    "bankBranch": "",
+                    "remark": "",
+                }
+            ]
+        }
+        self.assertEqual(validate_receive_account_form_values(schema, data), [])
+
+    def test_required_empty(self):
+        schema = {
+            "rule": [
+                {"type": "receiveAccount", "field": "acc", "title": "收款账户", "$required": True}
+            ]
+        }
+        errs = validate_receive_account_form_values(schema, {"acc": []})
+        self.assertTrue(any("收款账户" in e for e in errs))
+
+    def test_invalid_item(self):
+        schema = {"rule": [{"type": "receiveAccount", "field": "acc"}]}
+        errs = validate_receive_account_form_values(schema, {"acc": [{"id": "1"}]})
+        self.assertTrue(len(errs) >= 1)
+
+
+if __name__ == "__main__":
+    unittest.main()
+```
+
+- [ ] **Step 2: 跑测 — 期望 FAIL**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_oa_receive_account.py -v
+```
+
+Expected: FAIL(模块不存在)
+
+- [ ] **Step 3: 实现 `backend/app/services/oa_receive_account.py`**
+
+实现要点:
+
+- `ACCOUNT_TYPES = frozenset({"alipay", "personal", "corporate"})`
+- `normalize_account_fields(body: dict) -> Tuple[bool, Optional[str], Dict]`:读 camelCase;`alipay` 清空银行四字段;`personal`/`corporate` 校验银行四字段非空;户名账号必填
+- `validate_receive_account_form_values(schema, data)`:遍历 `type==receiveAccount`;值必须是 list;每项须含 `id`、`accountType`、`accountName`、`accountNo`(字符串非空);`alipay` 不强制银行字段;其它类型快照内银行字段也应非空(与存值契约一致);`$required` 时 list 非空
+- `_walk_rules` / `_schema_rules` 复制 `oa_money_field.py` 模式
+
+- [ ] **Step 4: 实现 `backend/app/crud/oa_receive_account.py`**
+
+```python
+# 关键函数签名(实现体对齐 oa_approval._role_to_dict 风格)
+def row_to_dict(r: OaReceiveAccount) -> Dict[str, Any]: ...
+def list_accounts(db, *, tenant_no, keyword=None, account_type=None, page=1, page_size=10) -> Tuple[List[Dict], int]: ...
+def list_options(db, *, tenant_no) -> List[Dict]: ...  # del_flag=0, create_time DESC
+def get_account(db, account_id, tenant_no) -> Optional[Dict]: ...
+def create_account(db, *, tenant_no, user_id, dept_id, fields: Dict) -> Dict: ...
+def update_account(db, account_id, tenant_no, user_id, fields: Dict) -> Optional[Dict]: ...
+def soft_delete_account(db, account_id, tenant_no, user_id) -> bool: ...
+def soft_delete_accounts(db, ids: List[str], tenant_no, user_id) -> int: ...
+```
+
+`row_to_dict` 字段(camelCase):`id, accountType, accountName, accountNo, bankName, bankProvince, bankCity, bankBranch, tenantNo, orderNum, remark, createTime, updateTime`。
+
+列表默认 `create_time DESC`;keyword 模糊匹配 `account_name` / `account_no` / `bank_name`。
+
+- [ ] **Step 5: 跑测 — 期望 PASS**
+
+```bash
+cd backend && source venv/bin/activate && pytest tests/test_oa_receive_account.py -v
+```
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add backend/app/services/oa_receive_account.py backend/app/crud/oa_receive_account.py backend/tests/test_oa_receive_account.py
+git commit -m "feat(backend): add receive account CRUD helpers and validators."
+```
+
+---
+
+### Task 4: REST API + 发布白名单 + 发起校验
+
+**Files:**
+- Modify: `backend/app/api/oa_approval.py`
+- Modify: `backend/app/services/oa_publish.py`
+- Modify: `backend/app/services/oa_engine.py`
+
+- [ ] **Step 1: `oa_publish.FILLABLE_TYPES` 增加 `"receiveAccount"`**
+
+```python
+# FILLABLE_TYPES frozenset 内增加
+"receiveAccount",
+```
+
+- [ ] **Step 2: `oa_engine.start_instance` 挂入校验**
+
+在现有 `validate_person_form_values` 附近:
+
+```python
+from app.services.oa_receive_account import validate_receive_account_form_values
+...
+recv_errs = validate_receive_account_form_values(schema, data)
+if recv_errs:
+    raise OaEngineError(recv_errs[0])
+```
+
+- [ ] **Step 3: 在 `oa_approval.py` 增加 Body 与路由**
+
+```python
+class OaReceiveAccountBody(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+    account_type: str = Field(..., alias="accountType")
+    account_name: str = Field(..., alias="accountName")
+    account_no: str = Field(..., alias="accountNo")
+    bank_name: Optional[str] = Field(None, alias="bankName")
+    bank_province: Optional[str] = Field(None, alias="bankProvince")
+    bank_city: Optional[str] = Field(None, alias="bankCity")
+    bank_branch: Optional[str] = Field(None, alias="bankBranch")
+    order_num: Optional[int] = Field(None, alias="orderNum")
+    remark: Optional[str] = None
+
+
+class OaReceiveAccountBatchDeleteBody(BaseModel):
+    model_config = ConfigDict(populate_by_name=True)
+    ids: List[str] = Field(default_factory=list)
+```
+
+路由(均 `Depends(require_admin_or_super_admin)`):
+
+| 方法 | 路径 | 行为 |
+|------|------|------|
+| GET | `/receive-accounts` | `page,pageSize,keyword,accountType` → `{list,total}` |
+| GET | `/receive-accounts/options` | `{list}` |
+| GET | `/receive-accounts/{account_id}` | 详情 |
+| POST | `/receive-accounts` | normalize → create |
+| PUT | `/receive-accounts/{account_id}` | normalize → update |
+| DELETE | `/receive-accounts/{account_id}` | soft delete |
+| POST | `/receive-accounts/batch-delete` | body.ids 批量软删 |
+
+创建/更新失败返回 `error_response(message=err)`。
+
+- [ ] **Step 4: 手测或 curl(需登录 token)**
+
+```bash
+# 启动后端后:创建支付宝账户 → options 可见 → 软删后 options 不可见
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend/app/api/oa_approval.py backend/app/services/oa_publish.py backend/app/services/oa_engine.py
+git commit -m "feat(backend): expose receive account APIs and form validation."
+```
+
+---
+
+### Task 5: 前端 value 工具(TDD)
+
+**Files:**
+- Create: `web-admin/src/utils/oa-receive-account-value.ts`
+- Create: `web-admin/src/utils/oa-receive-account-value.test.ts`
+
+- [ ] **Step 1: 写失败测试**
+
+```ts
+// web-admin/src/utils/oa-receive-account-value.test.ts
+import { describe, it } from 'node:test'
+import assert from 'node:assert/strict'
+import {
+  ACCOUNT_TYPE_LABEL,
+  formatReceiveAccountDisplay,
+  formatReceiveAccountLine,
+  isReceiveAccountSnapshot,
+  normalizeReceiveAccountModelValue,
+  type ReceiveAccountSnapshot
+} from './oa-receive-account-value'
+
+const alipay: ReceiveAccountSnapshot = {
+  id: '1',
+  accountType: 'alipay',
+  accountName: '张三',
+  accountNo: '13800138000',
+  bankName: '',
+  bankProvince: '',
+  bankCity: '',
+  bankBranch: '',
+  remark: '日常'
+}
+
+const personal: ReceiveAccountSnapshot = {
+  id: '2',
+  accountType: 'personal',
+  accountName: '李四',
+  accountNo: '6222',
+  bankName: '工行',
+  bankProvince: '安徽省',
+  bankCity: '合肥市',
+  bankBranch: '长江中路支行',
+  remark: ''
+}
+
+describe('oa-receive-account-value', () => {
+  it('type labels', () => {
+    assert.equal(ACCOUNT_TYPE_LABEL.alipay, '支付宝')
+    assert.equal(ACCOUNT_TYPE_LABEL.personal, '个人银行卡')
+    assert.equal(ACCOUNT_TYPE_LABEL.corporate, '对公银行账户')
+  })
+  it('isReceiveAccountSnapshot', () => {
+    assert.equal(isReceiveAccountSnapshot(alipay), true)
+    assert.equal(isReceiveAccountSnapshot({ id: '1' }), false)
+  })
+  it('normalize always array', () => {
+    assert.deepEqual(normalizeReceiveAccountModelValue(null), [])
+    assert.deepEqual(normalizeReceiveAccountModelValue(alipay), [alipay])
+    assert.deepEqual(normalizeReceiveAccountModelValue([alipay, personal]), [alipay, personal])
+  })
+  it('format line alipay omits bank', () => {
+    const line = formatReceiveAccountLine(alipay)
+    assert.ok(line.includes('支付宝'))
+    assert.ok(line.includes('张三'))
+    assert.ok(!line.includes('工行'))
+    assert.ok(line.includes('日常'))
+  })
+  it('format line personal full', () => {
+    const line = formatReceiveAccountLine(personal)
+    assert.ok(line.includes('个人银行卡'))
+    assert.ok(line.includes('安徽省 / 合肥市'))
+    assert.ok(line.includes('长江中路支行'))
+  })
+  it('format display multiline', () => {
+    const text = formatReceiveAccountDisplay([alipay, personal])
+    assert.ok(text.includes('\n'))
+  })
+})
+```
+
+- [ ] **Step 2: 跑测 — FAIL**
+
+```bash
+cd web-admin && pnpm exec tsx --test src/utils/oa-receive-account-value.test.ts
+```
+
+- [ ] **Step 3: 实现 `oa-receive-account-value.ts`**
+
+```ts
+export type ReceiveAccountType = 'alipay' | 'personal' | 'corporate'
+
+export type ReceiveAccountSnapshot = {
+  id: string
+  accountType: ReceiveAccountType
+  accountName: string
+  accountNo: string
+  bankName: string
+  bankProvince: string
+  bankCity: string
+  bankBranch: string
+  remark: string
+}
+
+export const ACCOUNT_TYPE_LABEL: Record<ReceiveAccountType, string> = {
+  alipay: '支付宝',
+  personal: '个人银行卡',
+  corporate: '对公银行账户'
+}
+
+export function isReceiveAccountSnapshot(v: unknown): v is ReceiveAccountSnapshot { ... }
+export function normalizeReceiveAccountModelValue(raw: unknown): ReceiveAccountSnapshot[] { ... }
+export function formatReceiveAccountLine(s: ReceiveAccountSnapshot): string {
+  // 有值才 join ' · ';银行类型加 `${province} / ${city}`;支付宝跳过银行字段
+}
+export function formatReceiveAccountDisplay(raw: unknown): string {
+  // normalize 后 map line,join '\n'
+}
+export function optionLabelFromRow(row: {
+  accountType: string
+  accountName: string
+  accountNo: string
+}): string {
+  const t = ACCOUNT_TYPE_LABEL[row.accountType as ReceiveAccountType] || row.accountType
+  return `${t} · ${row.accountName} · ${row.accountNo}`
+}
+export function snapshotFromRow(row: any): ReceiveAccountSnapshot { ... }
+```
+
+- [ ] **Step 4: 跑测 — PASS**
+
+```bash
+cd web-admin && pnpm exec tsx --test src/utils/oa-receive-account-value.test.ts
+```
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add web-admin/src/utils/oa-receive-account-value.ts web-admin/src/utils/oa-receive-account-value.test.ts
+git commit -m "feat(web-admin): add receive account snapshot value helpers."
+```
+
+---
+
+### Task 6: API 模块 + FormCreate 注册链路
+
+**Files:**
+- Modify: `web-admin/src/api/oaApproval.ts`
+- Create: `web-admin/src/utils/oa-receive-account-rule.ts`
+- Create: `web-admin/src/utils/oa-form-create-receive-account.ts`
+- Create: `web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue`
+- Modify: `web-admin/src/main.ts`
+- Modify: `web-admin/src/oa-fc-host/main.ts`
+- Modify: `web-admin/src/utils/oa-approval-print.ts`
+
+- [ ] **Step 1: `oaApproval.ts` 追加**
+
+```ts
+export function fetchOaReceiveAccountList(params?: {
+  keyword?: string
+  accountType?: string
+  page?: number
+  pageSize?: number
+}) {
+  return request.get<{ list: any[]; total: number }>({
+    url: `${BASE}/receive-accounts`,
+    params
+  })
+}
+
+export function fetchOaReceiveAccountOptions() {
+  return request.get<{ list: any[] }>({ url: `${BASE}/receive-accounts/options` })
+}
+
+export function fetchOaReceiveAccountDetail(id: string) {
+  return request.get<any>({ url: `${BASE}/receive-accounts/${id}` })
+}
+
+export function fetchCreateOaReceiveAccount(data: Record<string, unknown>) {
+  return request.post({ url: `${BASE}/receive-accounts`, data, showSuccessMessage: true })
+}
+
+export function fetchUpdateOaReceiveAccount(id: string, data: Record<string, unknown>) {
+  return request.put({ url: `${BASE}/receive-accounts/${id}`, data, showSuccessMessage: true })
+}
+
+export function fetchDeleteOaReceiveAccount(id: string) {
+  return request.del({ url: `${BASE}/receive-accounts/${id}`, showSuccessMessage: true })
+}
+
+export function fetchBatchDeleteOaReceiveAccounts(ids: string[]) {
+  return request.post({
+    url: `${BASE}/receive-accounts/batch-delete`,
+    data: { ids },
+    showSuccessMessage: true
+  })
+}
+```
+
+- [ ] **Step 2: DragRule(固定多选,无 single switch)**
+
+对齐 `oa-person-select-rule.ts`,但:
+
+```ts
+export const receiveAccountDragRule = {
+  menu: 'main' as const,
+  icon: 'icon-input',
+  label: '收款账户',
+  name: 'receiveAccount',
+  input: true,
+  mask: true,
+  validate: ['array', 'object'],
+  event: ['change'],
+  rule() {
+    return {
+      type: 'receiveAccount',
+      field: uniqueId(), // suffix 'r'
+      title: '收款账户',
+      info: '',
+      $required: false,
+      props: { multiple: true }
+    }
+  },
+  props() {
+    return [{ type: 'input', field: 'formCreateInfo', title: '提示信息' }]
+  }
+}
+```
+
+- [ ] **Step 3: `setupOaReceiveAccountSelect`**
+
+复制 `oa-form-create-person.ts` 模式,`component('receiveAccount', OaReceiveAccountSelect)` + `addDragRule`。
+
+- [ ] **Step 4: `OaReceiveAccountSelect.vue`**
+
+对齐 `OaPersonSelect.vue` 结构:
+
+- `defineOptions({ name: 'receiveAccount' })`
+- props: `modelValue`, `disabled`, `formCreateInject`(`multiple` 恒 true)
+- 可编辑:PC `el-select` multiple,options 来自 `fetchOaReceiveAccountOptions`;label 用 `optionLabelFromRow`;`@update` 时用 `snapshotFromRow` 组装快照数组
+- 窄屏:抽屉 + checkbox 列表(同 person)
+- 只读:`formatReceiveAccountDisplay`,用 `white-space: pre-line` 展示多行完整字段
+- 加载失败 `ElMessage.error`
+
+- [ ] **Step 5: `main.ts` / `oa-fc-host/main.ts` 调用 setup**
+
+与 `setupOaPersonSelect` 并列。
+
+- [ ] **Step 6: 打印**
+
+在 `oa-approval-print.ts` 的 `person`/`money` 分支旁:
+
+```ts
+if (type === 'receiveAccount') {
+  const text = formatReceiveAccountDisplay(value)
+  out.push({ label, text: text || '-' })
+  continue
+}
+```
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add web-admin/src/api/oaApproval.ts \
+  web-admin/src/utils/oa-receive-account-rule.ts \
+  web-admin/src/utils/oa-form-create-receive-account.ts \
+  web-admin/src/components/oa-approval/OaReceiveAccountSelect.vue \
+  web-admin/src/main.ts \
+  web-admin/src/oa-fc-host/main.ts \
+  web-admin/src/utils/oa-approval-print.ts
+git commit -m "feat(web-admin): register receiveAccount FormCreate control."
+```
+
+---
+
+### Task 7: 管理页 + 路由
+
+**Files:**
+- Create: `web-admin/src/views/oa-approval/receive-accounts/index.vue`
+- Modify: `web-admin/src/router/modules/sqxp.ts`
+
+- [ ] **Step 1: 路由子项**(插在 `roles` 与 `start` 之间或 OA children 末尾可见菜单区)
+
+```ts
+{
+  id: 'OA000000000000000000000000000108',
+  name: 'OaApprovalReceiveAccounts',
+  path: 'receive-accounts',
+  component: '/oa-approval/receive-accounts/index',
+  meta: {
+    title: '收款账户管理',
+    keepAlive: true,
+    menuCode: 'oa:receive-accounts:list'
+  }
+}
+```
+
+- [ ] **Step 2: 实现管理页**
+
+以 `web-admin/src/views/oa-approval/roles/index.vue` 为骨架,对照 Demo `docs/mockups/oa-receive-account-demo.html`:
+
+- 标题:`收款账户管理`(`h2.title-bar-heading`)
+- 筛选:keyword、accountType(alipay/personal/corporate)
+- 工具栏:已选 N 项 → 新增(primary)→ 批量删除
+- 表格列:类型 Tag、户名、账号、银行、开户地/支行、备注、操作(查看/编辑/删除,fixed right,min-width≥260)
+- Tag:`alipay`→`success`,`personal`→`primary`,`corporate`→`warning`
+- 弹窗:`dialog-form-layout`、`label-position="top"`、`form-grid-2`、`append-to-body`、`modal-class="protocol-dialog-mask"`
+- 类型切换:`alipay` 隐藏银行字段并清空;银行字段必填校验动态 rules
+- 省/市:`el-cascader` 或两个 `el-select` 基于 `fetchChinaRegionTree()`(只取到市,存 **名称** 到 bankProvince/bankCity)
+- 查看弹窗:按类型完整字段(SystemViewDialog 或自建 view grid)
+- 按钮带 `el-icon`(Plus/Search/RefreshLeft/Close/Check/Delete/View/Edit)
+
+- [ ] **Step 3: 浏览器手测**
+
+1. 登录后 OA → 收款账户管理  
+2. 三类各建一条  
+3. 软删一条,options 不再出现  
+4. 设计器拖入「收款账户」,发布含该字段的应用  
+5. 发起审批多选账户,详情/打印完整展示;改主数据后历史快照不变  
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add web-admin/src/views/oa-approval/receive-accounts/index.vue web-admin/src/router/modules/sqxp.ts
+git commit -m "feat(web-admin): add OA receive account management page."
+```
+
+---
+
+### Task 8: 收尾
+
+**Files:**
+- Modify: `docs/superpowers/specs/2026-07-30-oa-receive-account-design.md` 状态改为「已实现」
+- Run: `./scripts/update-graph.sh backend` 与 `./scripts/update-graph.sh frontend`(若脚本存在)
+- Run: `cd backend && python scripts/check_field_consistency.py`(若支持本实体则跑;否则跳过并注明)
+
+- [ ] **Step 1: 更新 spec 状态**
+- [ ] **Step 2: 更新知识图谱**
+- [ ] **Step 3: Commit docs/graph**
+
+```bash
+git add docs/superpowers/specs/2026-07-30-oa-receive-account-design.md backend/graphify-out web-admin/graphify-out
+git commit -m "docs: mark receive account design implemented and refresh graphs."
+```
+
+---
+
+## Spec coverage checklist
+
+| Spec 要求 | Task |
+|-----------|------|
+| 表 `oa_receive_account` + 公共字段 | 1–2 |
+| 三类字段校验 / 支付宝清空银行 | 3 |
+| CRUD + options API | 3–4 |
+| 菜单 + 路由 | 1, 7 |
+| 管理页 P01 | 7 |
+| FormCreate `receiveAccount` 四层扩展 | 5–6 |
+| 快照多选存值 | 5–6 |
+| 完整展示 + 打印 | 5–6 |
+| 发布白名单 + 发起校验 | 4 |
+| 软删无启用停用 | 3–4 |
+| 非目标:条件分支 / 小程序 | 不做 |
+
+## Placeholder / consistency self-review
+
+- 无 TBD;类型名统一 `receiveAccount` / `ReceiveAccountSnapshot` / `accountType`
+- API 路径统一 `/receive-accounts`
+- 菜单 ID 统一 `OA…108`