2026-05-20-storage-cert-insurance-management.md 46 KB

储存证管理 + 满月待办管理 + 保险公司及项目价格 实施计划

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: 新增 3 个业务模块(保险公司基础信息维护、满月待办保险管理、储存证独立管理),覆盖数据库、后端 API、前端页面全栈实现。

Architecture: 模块 A(保险公司)参照现有体检机构模式新建独立 CRUD 模块;模块 B(满月待办)ALTER 扩展现有表 + 新建 Model/CRUD/列表接口;模块 C(储存证)复用现有表/CRUD,仅新增独立列表接口和前端页面。模块 B 依赖模块 A(引用保险公司数据)。

Tech Stack: MySQL 5.7 / FastAPI + SQLAlchemy / Vue 3 + Element Plus + Tailwind CSS 4 / pnpm

Design Doc: docs/superpowers/specs/2026-05-20-storage-cert-insurance-management-design.md

Reference Pattern Files:

  • 后端 API+CRUD:backend/app/api/pilot_service.py(体检机构 CRUD,L1209-1697)
  • 后端 Model:backend/app/models/models.py(BizCheckupInstitution L1604-1668)
  • 后端 Storage Cert CRUD:backend/app/crud/collection_protocol.py(L512, L1881, L2176)
  • 前端页面:web-admin/src/views/pilot-service/checkup-institution/index.vue(770 行)
  • 前端 API:web-admin/src/api/admin.ts(L1620-1696)
  • 路由配置:web-admin/src/router/modules/sqxp.ts

File Structure Map

New Files

File Responsibility
docs/sql/insurance_and_fullmoon.sql 新表 DDL + ALTER TABLE 语句
backend/app/api/insurance_company.py 保险公司 CRUD 路由(3 组 REST)
web-admin/src/views/basic-info/insurance-company/index.vue 保险公司管理页面
web-admin/src/views/cell-storage/storage-cert/index.vue 储存证管理页面
web-admin/src/views/cell-storage/fullmoon-todo/index.vue 满月待办管理页面

Modified Files

File Change
backend/app/models/models.py +3 Insurance Models, +1 FullmoonTodo Model
backend/app/api/__init__.py +1 import
backend/app/main.py +1 include_router
backend/app/api/collection_protocol.py +2 endpoints (storage-certs list, fullmoon-todos list/update)
backend/app/crud/collection_protocol.py +3 functions (fullmoon_todo CRUD)
web-admin/src/router/modules/sqxp.ts +3 route entries
web-admin/src/api/admin.ts +14 API functions

Task 1: 数据库变更(SQL 脚本)

Files:

  • Create: docs/sql/insurance_and_fullmoon.sql

  • [ ] Step 1: 创建 SQL 迁移脚本

-- =============================================
-- 1. 保险公司主表
-- =============================================
DROP TABLE IF EXISTS `biz_insurance_company`;
CREATE TABLE `biz_insurance_company` (
  `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
  `company_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '保险公司名称',
  `company_address` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '公司地址',
  `contact_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系人',
  `contact_phone` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '联系电话',
  `status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '1正常 0停用',
  `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`),
  KEY `idx_ins_company` (`tenant_no`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保险公司';

-- =============================================
-- 2. 保险公司开票信息
-- =============================================
DROP TABLE IF EXISTS `biz_insurance_company_invoice`;
CREATE TABLE `biz_insurance_company_invoice` (
  `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
  `insurance_company_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '保险公司ID',
  `company_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '单位名称',
  `tax_no` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '税号',
  `company_address` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '单位地址',
  `company_phone` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '电话',
  `bank_name` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '开户银行',
  `bank_account` varchar(64) 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`),
  KEY `idx_ins_inv` (`insurance_company_id`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保险公司开票信息';

-- =============================================
-- 3. 保险项目价格
-- =============================================
DROP TABLE IF EXISTS `biz_insurance_project_price`;
CREATE TABLE `biz_insurance_project_price` (
  `id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
  `insurance_company_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '保险公司ID',
  `project_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '项目名称',
  `project_fee` decimal(18,2) DEFAULT NULL COMMENT '项目费用(元)',
  `status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '1正常 0停用',
  `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`),
  KEY `idx_ins_proj` (`insurance_company_id`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保险项目价格';

-- =============================================
-- 4. 满月待办表扩展(新增 8 个保险字段)
-- =============================================
ALTER TABLE `biz_protocol_fullmoon_todo`
  ADD COLUMN `insurance_company_id` varchar(64) DEFAULT NULL COMMENT '保险公司ID' AFTER `insurance_attachments`,
  ADD COLUMN `insurance_company_name` varchar(200) DEFAULT NULL COMMENT '保险公司名称(冗余快照)' AFTER `insurance_company_id`,
  ADD COLUMN `insurance_type` varchar(200) DEFAULT NULL COMMENT '保险险种' AFTER `insurance_company_name`,
  ADD COLUMN `insurance_policy_no` varchar(100) DEFAULT NULL COMMENT '保险单号' AFTER `insurance_type`,
  ADD COLUMN `insurance_effective_date` char(8) DEFAULT NULL COMMENT '保险单生效时间YYYYMMDD' AFTER `insurance_policy_no`,
  ADD COLUMN `insurance_expiry_date` char(8) DEFAULT NULL COMMENT '保险单失效时间YYYYMMDD' AFTER `insurance_effective_date`,
  ADD COLUMN `insurance_subscribed` char(1) DEFAULT '0' COMMENT '是否已订购保险(1是 0否)' AFTER `insurance_expiry_date`,
  ADD COLUMN `insurance_fee` decimal(18,2) DEFAULT NULL COMMENT '保单费用(元)' AFTER `insurance_subscribed`;
  • Step 2: 在 MySQL 中执行 SQL

Run: mysql -u root -p zkxw < docs/sql/insurance_and_fullmoon.sql Expected: 3 tables created, 1 table altered, no errors

  • Step 3: Commit
git add docs/sql/insurance_and_fullmoon.sql
git commit -m "feat: add insurance company tables and fullmoon todo insurance fields"

Task 2: 后端 Model 定义

Files:

  • Modify: backend/app/models/models.py

在 models.py 文件末尾(BizProtocolStorageCert 类之后),新增 4 个 Model 类。

  • Step 1: 添加 4 个新 Model

在 models.py 文件中找到 BizProtocolStorageCert 类定义结束位置(大约在 L983),在其后追加:

class BizProtocolFullmoonTodo(Base):
    """新生儿满月待办 biz_protocol_fullmoon_todo"""
    __tablename__ = "biz_protocol_fullmoon_todo"

    id = Column(String(64), primary_key=True, comment="主键ID")
    collection_protocol_id = Column(String(64), nullable=False, comment="采集协议单ID")
    todo_status = Column(CHAR(1), nullable=False, default="0", comment="1已办保险 0未办保险")
    handle_date = Column(CHAR(8), nullable=True, comment="办理日期")
    handle_remark = Column(String(512), nullable=True, comment="办理备注")
    insurance_attachments = Column(String(4000), nullable=True, comment="保险附件逗号分隔")
    insurance_company_id = Column(String(64), nullable=True, comment="保险公司ID")
    insurance_company_name = Column(String(200), nullable=True, comment="保险公司名称")
    insurance_type = Column(String(200), nullable=True, comment="保险险种")
    insurance_policy_no = Column(String(100), nullable=True, comment="保险单号")
    insurance_effective_date = Column(CHAR(8), nullable=True, comment="保险单生效时间YYYYMMDD")
    insurance_expiry_date = Column(CHAR(8), nullable=True, comment="保险单失效时间YYYYMMDD")
    insurance_subscribed = Column(CHAR(1), nullable=True, default="0", comment="是否已订购保险")
    insurance_fee = Column(Numeric(18, 2), 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="删除标记")


class BizInsuranceCompany(Base):
    """保险公司 biz_insurance_company"""
    __tablename__ = "biz_insurance_company"

    id = Column(String(64), primary_key=True, comment="主键ID")
    company_name = Column(String(200), nullable=False, comment="保险公司名称")
    company_address = Column(String(512), nullable=True, comment="公司地址")
    contact_name = Column(String(100), nullable=True, comment="联系人")
    contact_phone = Column(String(32), nullable=True, comment="联系电话")
    status = Column(CHAR(1), nullable=False, default="1", comment="1正常 0停用")
    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="删除标记")


class BizInsuranceCompanyInvoice(Base):
    """保险公司开票信息 biz_insurance_company_invoice"""
    __tablename__ = "biz_insurance_company_invoice"

    id = Column(String(64), primary_key=True, comment="主键ID")
    insurance_company_id = Column(String(64), nullable=False, comment="保险公司ID")
    company_name = Column(String(200), nullable=False, comment="单位名称")
    tax_no = Column(String(64), nullable=True, comment="税号")
    company_address = Column(String(512), nullable=True, comment="单位地址")
    company_phone = Column(String(64), nullable=True, comment="电话")
    bank_name = Column(String(200), nullable=True, comment="开户银行")
    bank_account = Column(String(64), 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="删除标记")


class BizInsuranceProjectPrice(Base):
    """保险项目价格 biz_insurance_project_price"""
    __tablename__ = "biz_insurance_project_price"

    id = Column(String(64), primary_key=True, comment="主键ID")
    insurance_company_id = Column(String(64), nullable=False, comment="保险公司ID")
    project_name = Column(String(200), nullable=False, comment="项目名称")
    project_fee = Column(Numeric(18, 2), nullable=True, comment="项目费用(元)")
    status = Column(CHAR(1), nullable=False, default="1", comment="1正常 0停用")
    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="删除标记")
  • Step 2: 验证 import

确认 models.py 顶部已导入 Numeric:from sqlalchemy import ... Numeric。若缺少则添加。

Run: cd backend && source venv/bin/activate && python -c "from app.models.models import BizInsuranceCompany, BizInsuranceCompanyInvoice, BizInsuranceProjectPrice, BizProtocolFullmoonTodo; print('OK')" Expected: OK

  • Step 3: Commit
git add backend/app/models/models.py
git commit -m "feat: add insurance company and fullmoon todo models"

Task 3: 模块 A — 保险公司后端 API

Files:

  • Create: backend/app/api/insurance_company.py
  • Modify: backend/app/api/__init__.py
  • Modify: backend/app/main.py

参照 pilot_service.py 中体检机构的完整 CRUD 模式(L1209-1697),创建保险公司 API 路由文件。包含 3 组 REST 接口:保险公司主表 CRUD、开票信息子表 CRUD、保险项目价格子表 CRUD。

  • Step 1: 创建 insurance_company.py

完整文件参照 pilot_service.py 中 CheckupInstitution 相关代码(L1209-1536),做以下映射替换:

  • checkup_institution → insurance_company
  • CheckupInstitution → InsuranceCompany
  • institution_name → company_name
  • institution_address → company_address
  • ps_svc(CRUD 函数)→ 内联 CRUD 函数(参照项目模式:CRUD 函数直接写在路由文件中)
  • _get_tenant_no / require_admin_or_super_admin / success_response / error_response → 相同 import

路由 URL 结构:

GET    /web/admin/insurance-companies                          → 分页列表
POST   /web/admin/insurance-companies                          → 新增
PUT    /web/admin/insurance-companies/{id}                     → 修改
DELETE /web/admin/insurance-companies/{id}                     → 删除
GET    /web/admin/insurance-companies/{id}/invoices            → 开票信息列表
POST   /web/admin/insurance-companies/{id}/invoices            → 新增开票信息
PUT    /web/admin/insurance-company-invoices/{id}              → 修改开票信息
DELETE /web/admin/insurance-company-invoices/{id}              → 删除开票信息
GET    /web/admin/insurance-companies/{id}/project-prices      → 项目价格列表
POST   /web/admin/insurance-companies/{id}/project-prices      → 新增项目价格
PUT    /web/admin/insurance-project-prices/{id}               → 修改项目价格
DELETE /web/admin/insurance-project-prices/{id}               → 删除项目价格

CRUD 函数模式(内联在路由文件中):

# ---- 内联 CRUD 函数 ----

def _generate_uuid() -> str:
    import uuid
    return uuid.uuid4().hex

def _fmt_dt(dt) -> str:
    return dt.strftime("%Y-%m-%d %H:%M:%S") if dt else ""

def _row_to_dict_company(obj: BizInsuranceCompany) -> dict:
    return {
        "id": obj.id,
        "companyName": obj.company_name,
        "companyAddress": obj.company_address or "",
        "contactName": obj.contact_name or "",
        "contactPhone": obj.contact_phone or "",
        "status": obj.status or "1",
        "tenantNo": obj.tenant_no,
        "orderNum": obj.order_num if obj.order_num is not None else 0,
        "remark": obj.remark or "",
        "createTime": _fmt_dt(obj.create_time),
        "updateTime": _fmt_dt(obj.update_time),
    }

def list_insurance_companies(db, *, page, page_size, keyword, tenant_no):
    q = db.query(BizInsuranceCompany).filter(
        BizInsuranceCompany.tenant_no == tenant_no,
        BizInsuranceCompany.del_flag == 0,
    )
    if keyword:
        q = q.filter(BizInsuranceCompany.company_name.like(f"%{keyword}%"))
    total = q.count()
    items = q.order_by(BizInsuranceCompany.create_time.desc()).offset((page - 1) * page_size).limit(page_size).all()
    return [_row_to_dict_company(r) for r in items], total

def total_pages(total, page_size):
    return (total + page_size - 1) // page_size if page_size else 0

def create_insurance_company(db, *, company_name, tenant_no, user_id, company_address=None, contact_name=None, contact_phone=None, status="1", order_num=0, remark=None):
    obj = BizInsuranceCompany(
        id=_generate_uuid(), company_name=company_name, company_address=company_address,
        contact_name=contact_name, contact_phone=contact_phone, status=status,
        tenant_no=tenant_no, order_num=order_num, remark=remark,
        create_by=user_id, create_dept_id="", update_by=user_id, del_flag=0,
    )
    db.add(obj); db.commit(); db.refresh(obj)
    return obj

def update_insurance_company(db, row_id, *, tenant_no, user_id, **fields):
    obj = db.query(BizInsuranceCompany).filter(BizInsuranceCompany.id == row_id, BizInsuranceCompany.tenant_no == tenant_no, BizInsuranceCompany.del_flag == 0).first()
    if not obj:
        raise ValueError("记录不存在")
    for k, v in fields.items():
        if v is not None:
            setattr(obj, k, v)
    obj.update_by = user_id
    db.add(obj); db.commit(); db.refresh(obj)
    return obj

def soft_delete_insurance_company(db, row_id, *, tenant_no):
    obj = db.query(BizInsuranceCompany).filter(BizInsuranceCompany.id == row_id, BizInsuranceCompany.tenant_no == tenant_no, BizInsuranceCompany.del_flag == 0).first()
    if not obj:
        raise ValueError("记录不存在")
    obj.del_flag = 1
    db.add(obj); db.commit()

开票信息和项目价格的 CRUD 函数遵循完全相同的模式,参照 BizCheckupInstitutionInvoice / BizCheckupInstitutionProject 对应的代码。每个子表的 _row_to_dict 返回 camelCase JSON,list 函数按 insurance_company_id 过滤。

  • Step 2: 注册路由

在 backend/app/api/__init__.py 中:

from app.api.insurance_company import router as insurance_company_router
# 添加到 __all__ 列表

在 backend/app/main.py 中:

from app.api import ... insurance_company_router ...
app.include_router(insurance_company_router, prefix=settings.API_V1_PREFIX)
  • Step 3: 启动后端验证

Run: cd backend && source venv/bin/activate && python -c "from app.api.insurance_company import router; print(f'Routes: {len(router.routes)}')" Expected: Routes: 12

Run: cd backend && ./start-local.sh → 访问 http://localhost:8000/docs → 搜索 insurance-companies → 应能看到所有 12 个接口

  • Step 4: Commit
git add backend/app/api/insurance_company.py backend/app/api/__init__.py backend/app/main.py
git commit -m "feat: add insurance company CRUD API with invoices and project prices"

Task 4: 模块 B+C — 储存证列表 & 满月待办 CRUD + 列表接口

Files:

  • Modify: backend/app/crud/collection_protocol.py
  • Modify: backend/app/api/collection_protocol.py

  • [ ] Step 1: 在 collection_protocol.py (crud) 中添加满月待办 CRUD 函数

在文件末尾(upsert_storage_cert 函数之后)追加:

# ============ Fullmoon Todo ============

def _fullmoon_todo_to_dict(ft: BizProtocolFullmoonTodo) -> Dict[str, Any]:
    return {
        "id": ft.id,
        "collectionProtocolId": ft.collection_protocol_id,
        "todoStatus": ft.todo_status or "0",
        "handleDate": (ft.handle_date or "").strip() if ft.handle_date else "",
        "handleRemark": ft.handle_remark or "",
        "insuranceAttachments": ft.insurance_attachments or "",
        "insuranceCompanyId": ft.insurance_company_id or "",
        "insuranceCompanyName": ft.insurance_company_name or "",
        "insuranceType": ft.insurance_type or "",
        "insurancePolicyNo": ft.insurance_policy_no or "",
        "insuranceEffectiveDate": (ft.insurance_effective_date or "").strip() if ft.insurance_effective_date else "",
        "insuranceExpiryDate": (ft.insurance_expiry_date or "").strip() if ft.insurance_expiry_date else "",
        "insuranceSubscribed": ft.insurance_subscribed or "0",
        "insuranceFee": float(ft.insurance_fee) if ft.insurance_fee else None,
        "tenantNo": ft.tenant_no,
        "orderNum": ft.order_num if ft.order_num is not None else 0,
        "remark": ft.remark or "",
        "createTime": _fmt_dt(ft.create_time),
        "updateTime": _fmt_dt(ft.update_time),
    }

def get_fullmoon_todo(db: Session, collection_protocol_id: str, tenant_no: str) -> Optional[BizProtocolFullmoonTodo]:
    return (
        db.query(BizProtocolFullmoonTodo)
        .filter(
            and_(
                BizProtocolFullmoonTodo.collection_protocol_id == collection_protocol_id,
                BizProtocolFullmoonTodo.tenant_no == tenant_no,
                BizProtocolFullmoonTodo.del_flag == 0,
            )
        )
        .first()
    )

def upsert_fullmoon_todo(
    db: Session,
    collection_protocol_id: str,
    tenant_no: str,
    user_id: Optional[str],
    **fields,
) -> BizProtocolFullmoonTodo:
    fields = _normalize_fields(fields)
    obj = get_fullmoon_todo(db, collection_protocol_id, tenant_no)
    _STR_FIELDS = ["todo_status", "handle_date", "handle_remark", "insurance_attachments",
                    "insurance_company_id", "insurance_company_name", "insurance_type",
                    "insurance_policy_no", "insurance_effective_date", "insurance_expiry_date",
                    "insurance_subscribed"]
    if obj:
        for k in _STR_FIELDS:
            if k in fields:
                setattr(obj, k, (fields[k] or "").strip() or None)
        if "insurance_fee" in fields:
            obj.insurance_fee = fields["insurance_fee"]
        if "remark" in fields:
            obj.remark = (fields["remark"] or "").strip() or None
        obj.update_by = user_id
        obj.update_time = datetime.now()
        db.add(obj); db.commit(); db.refresh(obj)
        return obj
    obj = BizProtocolFullmoonTodo(
        id=generate_uuid(),
        collection_protocol_id=collection_protocol_id,
        tenant_no=tenant_no,
        order_num=0,
        remark=(fields.get("remark") or "").strip() or None,
        create_by=user_id,
        create_dept_id="",
        create_time=datetime.now(),
        update_by=user_id,
        update_time=datetime.now(),
        del_flag=0,
    )
    for k in _STR_FIELDS:
        if k in fields:
            setattr(obj, k, (fields[k] or "").strip() or None)
    if "insurance_fee" in fields:
        obj.insurance_fee = fields["insurance_fee"]
    db.add(obj); db.commit(); db.refresh(obj)
    return obj

同时确保文件顶部 import 了 BizProtocolFullmoonTodo 和 BizInsuranceCompany。

  • Step 2: 在 collection_protocol.py (api) 中添加 3 个新接口

在 API 路由文件末尾追加:

a) 储存证管理列表 GET /web/admin/storage-certs

@router.get("/storage-certs")
async def list_storage_certs(
    page: int = Query(1, ge=1),
    pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
    keyword: Optional[str] = None,
    make_status: Optional[str] = None,
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    """储存证管理列表:筛选已生效 + 样本已入库的协议单"""
    tenant_no = _get_tenant_no(db, user)
    # 1. 查已生效的协议单
    q = db.query(BizCollectionProtocol).filter(
        BizCollectionProtocol.tenant_no == tenant_no,
        BizCollectionProtocol.del_flag == 0,
        BizCollectionProtocol.protocol_status == "sxz",
    )
    if keyword:
        q = q.filter(BizCollectionProtocol.protocol_no.like(f"%{keyword}%"))
    protocol_ids_sub = q.subquery()

    # 2. 过滤有已入库样本的协议单
    sample_sub = (
        db.query(BizProtocolSample.collection_protocol_id)
        .filter(
            BizProtocolSample.sample_status == "yrk",
            BizProtocolSample.del_flag == 0,
            BizProtocolSample.tenant_no == tenant_no,
        )
        .subquery()
    )

    # 3. 主查询
    main_q = (
        db.query(BizCollectionProtocol)
        .filter(
            BizCollectionProtocol.id.in_(db.query(sample_sub.c.collection_protocol_id)),
            BizCollectionProtocol.id.in_(db.query(protocol_ids_sub.c.id)),
        )
    )
    total = main_q.count()
    protocols = main_q.order_by(BizCollectionProtocol.create_time.desc()).offset((page - 1) * pageSize).limit(pageSize).all()

    items = []
    for p in protocols:
        cert = cp_svc.get_storage_cert(db, p.id, tenant_no)
        party = db.query(BizProtocolPartyCustomer).filter(
            BizProtocolPartyCustomer.collection_protocol_id == p.id,
            BizProtocolPartyCustomer.del_flag == 0,
        ).first()
        samples = db.query(BizProtocolSample).filter(
            BizProtocolSample.collection_protocol_id == p.id,
            BizProtocolSample.sample_status == "yrk",
            BizProtocolSample.del_flag == 0,
        ).all()

        cert_dict = cp_svc._storage_cert_to_dict(cert) if cert else {"makeStatus": "0", "certImageUrls": "", "makeDate": ""}

        # 按 make_status 过滤
        if make_status and cert_dict.get("makeStatus") != make_status:
            continue

        sample_info = ", ".join([f"{s.sample_code or ''}" for s in samples[:3]])

        items.append({
            "protocolId": p.id,
            "protocolNo": p.protocol_no,
            "motherName": party.mother_name if party else "",
            "newbornDeliveryTime": _fmt_dt(party.newborn_delivery_time) if party and party.newborn_delivery_time else "",
            "sampleInfo": sample_info,
            "storageInDate": samples[0].storage_in_date if samples else "",
            **cert_dict,
        })

    tp = cp_svc.total_pages(total, pageSize) if hasattr(cp_svc, 'total_pages') else (total + pageSize - 1) // pageSize
    return success_response({"list": items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})

b) 满月待办列表 GET /web/admin/fullmoon-todos

@router.get("/fullmoon-todos")
async def list_fullmoon_todos(
    page: int = Query(1, ge=1),
    pageSize: int = Query(10, ge=1, le=100, alias="pageSize"),
    keyword: Optional[str] = None,
    todo_status: Optional[str] = None,
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    """满月待办列表:筛选已生效 + 新生儿已满月的协议单"""
    tenant_no = _get_tenant_no(db, user)
    from datetime import timedelta
    fullmoon_cutoff = datetime.now() - timedelta(days=30)

    # 查已生效协议单
    q = db.query(BizCollectionProtocol).filter(
        BizCollectionProtocol.tenant_no == tenant_no,
        BizCollectionProtocol.del_flag == 0,
        BizCollectionProtocol.protocol_status == "sxz",
    )
    if keyword:
        q = q.filter(BizCollectionProtocol.protocol_no.like(f"%{keyword}%"))
    protocols = q.order_by(BizCollectionProtocol.create_time.desc()).all()

    items = []
    for p in protocols:
        party = db.query(BizProtocolPartyCustomer).filter(
            BizProtocolPartyCustomer.collection_protocol_id == p.id,
            BizProtocolPartyCustomer.del_flag == 0,
        ).first()
        if not party or not party.newborn_delivery_time:
            continue
        if party.newborn_delivery_time > fullmoon_cutoff:
            continue

        ft = cp_svc.get_fullmoon_todo(db, p.id, tenant_no)
        ft_dict = cp_svc._fullmoon_todo_to_dict(ft) if ft else {
            "todoStatus": "0", "handleDate": "", "handleRemark": "",
            "insuranceAttachments": "", "insuranceCompanyId": "", "insuranceCompanyName": "",
            "insuranceType": "", "insurancePolicyNo": "", "insuranceEffectiveDate": "",
            "insuranceExpiryDate": "", "insuranceSubscribed": "0", "insuranceFee": None,
        }

        if todo_status and ft_dict.get("todoStatus") != todo_status:
            continue

        fullmoon_date = party.newborn_delivery_time + timedelta(days=30)

        items.append({
            "protocolId": p.id,
            "protocolNo": p.protocol_no,
            "motherName": party.mother_name or "",
            "motherPhone": party.mother_phone or "",
            "newbornDeliveryTime": _fmt_dt(party.newborn_delivery_time),
            "fullmoonDate": fullmoon_date.strftime("%Y-%m-%d") if fullmoon_date else "",
            **ft_dict,
        })

    total = len(items)
    start = (page - 1) * pageSize
    page_items = items[start:start + pageSize]
    tp = (total + pageSize - 1) // pageSize if pageSize else 0
    return success_response({"list": page_items, "total": total, "page": page, "pageSize": pageSize, "totalPages": tp})

c) 满月待办保险信息更新 PUT /web/admin/fullmoon-todos/{protocol_id}/insurance

class FullmoonTodoInsuranceUpdateBody(BaseModel):
    model_config = ConfigDict(populate_by_name=True)
    insurance_company_id: Optional[str] = Field(None, alias="insuranceCompanyId")
    insurance_company_name: Optional[str] = Field(None, alias="insuranceCompanyName")
    insurance_type: Optional[str] = Field(None, alias="insuranceType")
    insurance_policy_no: Optional[str] = Field(None, alias="insurancePolicyNo")
    insurance_effective_date: Optional[str] = Field(None, alias="insuranceEffectiveDate")
    insurance_expiry_date: Optional[str] = Field(None, alias="insuranceExpiryDate")
    insurance_subscribed: Optional[str] = Field(None, alias="insuranceSubscribed")
    insurance_fee: Optional[float] = Field(None, alias="insuranceFee")
    insurance_attachments: Optional[str] = Field(None, alias="insuranceAttachments")
    todo_status: Optional[str] = Field(None, alias="todoStatus")
    handle_date: Optional[str] = Field(None, alias="handleDate")
    handle_remark: Optional[str] = Field(None, alias="handleRemark")

@router.put("/fullmoon-todos/{protocol_id}/insurance")
async def update_fullmoon_todo_insurance(
    protocol_id: str,
    body: FullmoonTodoInsuranceUpdateBody,
    user: dict = Depends(require_admin_or_super_admin),
    db: Session = Depends(get_db),
):
    tenant_no = _get_tenant_no(db, user)
    fields = body.model_dump(exclude_unset=True, by_alias=False)
    obj = cp_svc.upsert_fullmoon_todo(db, protocol_id, tenant_no, user.get("user_id"), **fields)
    return success_response(cp_svc._fullmoon_todo_to_dict(obj), message="更新成功")
  • Step 3: 验证接口可启动

Run: cd backend && source venv/bin/activate && python -c "from app.api.collection_protocol import router; print('OK')" Expected: OK

  • Step 4: Commit
git add backend/app/crud/collection_protocol.py backend/app/api/collection_protocol.py
git commit -m "feat: add storage cert list, fullmoon todo list and insurance update endpoints"

Task 5: 前端 API 函数

Files:

  • Modify: web-admin/src/api/admin.ts

在 admin.ts 文件末尾(fetchDeleteCheckupInstitutionPackageItem 函数之后)追加所有新 API 函数。

  • Step 1: 追加 API 函数
// ============ 保险公司管理 ============

export function fetchInsuranceCompanyList(params?: {
  page?: number
  pageSize?: number
  keyword?: string
}) {
  return request.get({ url: '/web/admin/insurance-companies', params })
}

export function fetchCreateInsuranceCompany(data: Record<string, any>) {
  return request.post({ url: '/web/admin/insurance-companies', params: data })
}

export function fetchUpdateInsuranceCompany(id: string, data: Record<string, any>) {
  return request.put({ url: `/web/admin/insurance-companies/${id}`, params: data })
}

export function fetchDeleteInsuranceCompany(id: string) {
  return request.del({ url: `/web/admin/insurance-companies/${id}` })
}

// 保险公司开票信息
export function fetchInsuranceCompanyInvoices(companyId: string) {
  return request.get({ url: `/web/admin/insurance-companies/${companyId}/invoices` })
}

export function fetchCreateInsuranceCompanyInvoice(companyId: string, data: Record<string, any>) {
  return request.post({ url: `/web/admin/insurance-companies/${companyId}/invoices`, params: data })
}

export function fetchUpdateInsuranceCompanyInvoice(id: string, data: Record<string, any>) {
  return request.put({ url: `/web/admin/insurance-company-invoices/${id}`, params: data })
}

export function fetchDeleteInsuranceCompanyInvoice(id: string) {
  return request.del({ url: `/web/admin/insurance-company-invoices/${id}` })
}

// 保险项目价格
export function fetchInsuranceProjectPrices(companyId: string) {
  return request.get({ url: `/web/admin/insurance-companies/${companyId}/project-prices` })
}

export function fetchCreateInsuranceProjectPrice(companyId: string, data: Record<string, any>) {
  return request.post({ url: `/web/admin/insurance-companies/${companyId}/project-prices`, params: data })
}

export function fetchUpdateInsuranceProjectPrice(id: string, data: Record<string, any>) {
  return request.put({ url: `/web/admin/insurance-project-prices/${id}`, params: data })
}

export function fetchDeleteInsuranceProjectPrice(id: string) {
  return request.del({ url: `/web/admin/insurance-project-prices/${id}` })
}

// ============ 储存证管理 ============

export function fetchStorageCertList(params?: {
  page?: number
  pageSize?: number
  keyword?: string
  makeStatus?: string
}) {
  return request.get({ url: '/web/admin/storage-certs', params })
}

// ============ 满月待办管理 ============

export function fetchFullmoonTodoList(params?: {
  page?: number
  pageSize?: number
  keyword?: string
  todoStatus?: string
}) {
  return request.get({ url: '/web/admin/fullmoon-todos', params })
}

export function fetchUpdateFullmoonTodoInsurance(protocolId: string, data: Record<string, any>) {
  return request.put({ url: `/web/admin/fullmoon-todos/${protocolId}/insurance`, params: data })
}
  • Step 2: Commit
git add web-admin/src/api/admin.ts
git commit -m "feat: add insurance company, storage cert and fullmoon todo API functions"

Task 6: 前端路由注册

Files:

  • Modify: web-admin/src/router/modules/sqxp.ts

  • [ ] Step 1: 在细胞存储管理 children 中追加 2 个路由

在 sqxp.ts 中找到细胞存储管理路由块(id 50000000000000000000000000000001),在其 children 数组末尾追加:

{
  id: '50000000000000000000000000000006',
  name: 'CellStorageStorageCert',
  path: 'storage-cert',
  component: '/cell-storage/storage-cert/index',
  meta: { title: '储存证管理', keepAlive: true },
},
{
  id: '50000000000000000000000000000008',
  name: 'CellStorageFullmoonTodo',
  path: 'fullmoon-todo',
  component: '/cell-storage/fullmoon-todo/index',
  meta: { title: '满月待办管理', keepAlive: true },
},
  • Step 2: 在基础信息管理 children 中追加 1 个路由

在基础信息管理路由块(id 40000000000000000000000000000001),在其 children 数组末尾追加:

{
  id: '40000000000000000000000000000007',
  name: 'BasicInfoInsuranceCompany',
  path: 'insurance-company',
  component: '/basic-info/insurance-company/index',
  meta: { title: '保险公司及项目价格', keepAlive: true },
},
  • Step 3: Commit
git add web-admin/src/router/modules/sqxp.ts
git commit -m "feat: register storage cert, fullmoon todo and insurance company routes"

Task 7: 前端页面 — 保险公司及项目价格

Files:

  • Create: web-admin/src/views/basic-info/insurance-company/index.vue

参照模板: web-admin/src/views/pilot-service/checkup-institution/index.vue(770 行,完全同构)

  • Step 1: 创建保险公司管理页面

页面结构完全参照 checkup-institution/index.vue,做以下映射替换:

  • CheckupInstitution → InsuranceCompany
  • fetchCheckupInstitution* → fetchInsuranceCompany*(使用 Task 5 定义的 API 函数)
  • fetchCheckupInstitutionInvoice* → fetchInsuranceCompanyInvoice*
  • fetchCheckupInstitutionProject* → fetchInsuranceProjectPrice*
  • 表单字段映射:institutionName → companyName、institutionAddress → companyAddress
  • Tab 子表:开票信息 + 项目价格(与体检机构的发票+项目完全同构)
  • defineOptions({ name: 'BasicInfoInsuranceCompany' })

关键定制点:

  • 页面标题:「保险公司及项目价格」
  • 列表列:保险公司名称、地址、联系人、联系电话、状态、操作
  • 新增/编辑弹窗:公司名称(必填)、地址、联系人、联系电话、状态、备注
  • 点击行 → 弹出子表 Tab 弹窗:开票信息 Tab + 项目价格 Tab

  • [ ] Step 2: 验证页面可加载

Run: cd web-admin && pnpm dev → 访问 http://localhost:port/#/basic-info/insurance-company Expected: 页面加载,无控制台错误

  • Step 3: Commit
git add web-admin/src/views/basic-info/insurance-company/index.vue
git commit -m "feat: add insurance company management page with invoices and project prices"

Task 8: 前端页面 — 满月待办管理

Files:

  • Create: web-admin/src/views/cell-storage/fullmoon-todo/index.vue

参照模板: web-admin/src/views/system/order/index.vue(复杂列表模式)或 system/unit/index.vue(简单列表模式)

  • Step 1: 创建满月待办管理页面

页面为纯列表 + 编辑弹窗模式(无新增/删除,数据由系统自动生成):

列表列: | 列名 | 字段 | 宽度 | |------|------|------| | 采集协议号 | protocolNo | 160 | | 母亲姓名 | motherName | 100 | | 新生儿分娩时间 | newbornDeliveryTime | 140 | | 满月日期 | fullmoonDate | 120 | | 待办状态 | todoStatus(el-tag: 0=warning"未办保险", 1=success"已办保险")| 100 | | 是否已订购保险 | insuranceSubscribed(el-tag: 0=info"否", 1=success"是")| 120 | | 保险公司 | insuranceCompanyName | 140 | | 保险险种 | insuranceType | 120 | | 操作 | fixed="right", width=160 | 160 |

筛选栏: 关键词(协议号/姓名)、待办状态下拉(全部/未办保险/已办保险)

操作按钮: 「办理保险」→ 弹出编辑弹窗

编辑弹窗字段(label-position="top"):

  • 保险公司(下拉选择,从 fetchInsuranceCompanyList 获取,选择后自动填充 insuranceCompanyName)
  • 保险险种(输入框)
  • 保险单号(输入框)
  • 保险单生效时间(el-date-picker,format YYYYMMDD)
  • 保险单失效时间(el-date-picker,format YYYYMMDD)
  • 是否已订购保险(el-radio-group:是/否)
  • 保单费用(元)(el-input-number)
  • 保险单附件(el-upload,多附件)
  • 办理日期(el-date-picker)
  • 办理备注(el-input textarea)
  • 待办状态(el-radio-group:未办保险/已办保险)

API 调用:fetchFullmoonTodoList(列表)、fetchUpdateFullmoonTodoInsurance(保存)

defineOptions({ name: 'CellStorageFullmoonTodo' })

  • Step 2: 验证页面

Run: 访问 http://localhost:port/#/cell-storage/fullmoon-todo Expected: 列表加载,筛选和编辑弹窗正常

  • Step 3: Commit
git add web-admin/src/views/cell-storage/fullmoon-todo/index.vue
git commit -m "feat: add fullmoon todo management page with insurance info editing"

Task 9: 前端页面 — 储存证管理

Files:

  • Create: web-admin/src/views/cell-storage/storage-cert/index.vue

参照模板: web-admin/src/views/system/unit/index.vue(简单列表模式)

  • Step 1: 创建储存证管理页面

页面为纯列表 + 上传弹窗 + 查看弹窗模式(无新增/删除):

列表列: | 列名 | 字段 | 宽度 | |------|------|------| | 采集协议号 | protocolNo | 160 | | 母亲姓名 | motherName | 100 | | 新生儿分娩时间 | newbornDeliveryTime | 140 | | 样本信息 | sampleInfo | 180 | | 入库日期 | storageInDate | 120 | | 制作状态 | makeStatus(el-tag: 0=warning"未制作", 1=success"已制作")| 100 | | 制作日期 | makeDate | 120 | | 操作 | fixed="right", width=200 | 200 |

筛选栏: 关键词(协议号/姓名)、制作状态下拉(全部/未制作/已制作)

操作按钮:

  • 「上传储存证」→ 弹出上传弹窗
  • 「查看」→ 弹出查看弹窗

上传储存证弹窗:

  • 储存证图片(el-upload,多图上传)
  • 制作日期(el-date-picker,默认当天)
  • 备注(el-input textarea)

保存逻辑:调用现有采集协议单编辑接口中的 storageCert 子资源保存(upsert_storage_cert),或者新增一个专用接口。

defineOptions({ name: 'CellStorageStorageCert' })

  • Step 2: 验证页面

Run: 访问 http://localhost:port/#/cell-storage/storage-cert Expected: 列表加载,上传和查看功能正常

  • Step 3: Commit
git add web-admin/src/views/cell-storage/storage-cert/index.vue
git commit -m "feat: add storage certificate management page"

Task 10: 菜单数据初始化

Files:

  • Modify: 数据库 sys_menu 表

  • [ ] Step 1: 插入菜单数据

在 MySQL 中执行以下 SQL(父菜单 ID 需查询 sys_menu 表确认"细胞存储管理"和"基础信息管理"在 web 端的实际 ID):

-- 查找 web 端"细胞存储管理"的菜单 ID
SELECT id FROM sys_menu WHERE menu_name = '细胞存储管理' AND parent_id IN (SELECT id FROM sys_menu WHERE parent_id = 0 AND menu_name LIKE '%web%');

-- 查找 web 端"基础信息管理"的菜单 ID
SELECT id FROM sys_menu WHERE menu_name = '基础信息管理' AND parent_id IN (SELECT id FROM sys_menu WHERE parent_id = 0 AND menu_name LIKE '%web%');

-- 插入 3 条菜单(将 {cell_storage_menu_id} 和 {basic_info_menu_id} 替换为查询结果)
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark, del_flag)
VALUES
(REPLACE(UUID(),'-',''), '储存证管理', '{cell_storage_menu_id}', 60, 'storage-cert', 'cell-storage/storage-cert/index', 1, 0, 'C', '1', '1', 'cell-storage:storage-cert', '', '1', NOW(), '1', NOW(), '', 0),
(REPLACE(UUID(),'-',''), '满月待办管理', '{cell_storage_menu_id}', 70, 'fullmoon-todo', 'cell-storage/fullmoon-todo/index', 1, 0, 'C', '1', '1', 'cell-storage:fullmoon-todo', '', '1', NOW(), '1', NOW(), '', 0),
(REPLACE(UUID(),'-',''), '保险公司及项目价格', '{basic_info_menu_id}', 60, 'insurance-company', 'basic-info/insurance-company/index', 1, 0, 'C', '1', '1', 'basic-info:insurance-company', '', '1', NOW(), '1', NOW(), '', 0);
  • Step 2: 为管理员角色授权新菜单

在角色管理中,为需要的角色勾选新增的 3 个菜单权限。

  • Step 3: 端到端验证
  1. 登录系统 → 侧边栏应显示 3 个新菜单
  2. 保险公司管理 → 新增保险公司 → 新增开票信息 → 新增项目价格 → 编辑 → 删除
  3. 满月待办管理 → 列表应显示已满月的新生儿协议单 → 点击办理保险 → 填写保险信息 → 保存
  4. 储存证管理 → 列表应显示已入库的协议单 → 上传储存证 → 查看
  • Step 4: Commit
git add docs/sql/insurance_and_fullmoon.sql
git commit -m "feat: add menu initialization SQL for new modules"