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:
backend/app/api/pilot_service.py(体检机构 CRUD,L1209-1697)backend/app/models/models.py(BizCheckupInstitution L1604-1668)backend/app/crud/collection_protocol.py(L512, L1881, L2176)web-admin/src/views/pilot-service/checkup-institution/index.vue(770 行)web-admin/src/api/admin.ts(L1620-1696)web-admin/src/router/modules/sqxp.ts| 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 |
满月待办管理页面 |
| 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 |
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`;
Run: mysql -u root -p zkxw < docs/sql/insurance_and_fullmoon.sql
Expected: 3 tables created, 1 table altered, no errors
git add docs/sql/insurance_and_fullmoon.sql
git commit -m "feat: add insurance company tables and fullmoon todo insurance fields"
Files:
backend/app/models/models.py在 models.py 文件末尾(BizProtocolStorageCert 类之后),新增 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="删除标记")
确认 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
git add backend/app/models/models.py
git commit -m "feat: add insurance company and fullmoon todo models"
Files:
backend/app/api/insurance_company.pybackend/app/api/__init__.pybackend/app/main.py参照 pilot_service.py 中体检机构的完整 CRUD 模式(L1209-1697),创建保险公司 API 路由文件。包含 3 组 REST 接口:保险公司主表 CRUD、开票信息子表 CRUD、保险项目价格子表 CRUD。
insurance_company.py完整文件参照 pilot_service.py 中 CheckupInstitution 相关代码(L1209-1536),做以下映射替换:
checkup_institution → insurance_companyCheckupInstitution → InsuranceCompanyinstitution_name → company_nameinstitution_address → company_addressps_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 过滤。
在 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)
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 个接口
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"
Files:
backend/app/crud/collection_protocol.pyModify: 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。
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="更新成功")
Run: cd backend && source venv/bin/activate && python -c "from app.api.collection_protocol import router; print('OK')"
Expected: OK
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"
Files:
web-admin/src/api/admin.ts在 admin.ts 文件末尾(fetchDeleteCheckupInstitutionPackageItem 函数之后)追加所有新 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 })
}
git add web-admin/src/api/admin.ts
git commit -m "feat: add insurance company, storage cert and fullmoon todo API functions"
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 },
},
在基础信息管理路由块(id 40000000000000000000000000000001),在其 children 数组末尾追加:
{
id: '40000000000000000000000000000007',
name: 'BasicInfoInsuranceCompany',
path: 'insurance-company',
component: '/basic-info/insurance-company/index',
meta: { title: '保险公司及项目价格', keepAlive: true },
},
git add web-admin/src/router/modules/sqxp.ts
git commit -m "feat: register storage cert, fullmoon todo and insurance company routes"
Files:
web-admin/src/views/basic-info/insurance-company/index.vue参照模板: web-admin/src/views/pilot-service/checkup-institution/index.vue(770 行,完全同构)
页面结构完全参照 checkup-institution/index.vue,做以下映射替换:
CheckupInstitution → InsuranceCompanyfetchCheckupInstitution* → fetchInsuranceCompany*(使用 Task 5 定义的 API 函数)fetchCheckupInstitutionInvoice* → fetchInsuranceCompanyInvoice*fetchCheckupInstitutionProject* → fetchInsuranceProjectPrice*institutionName → companyName、institutionAddress → companyAddressdefineOptions({ name: 'BasicInfoInsuranceCompany' })关键定制点:
点击行 → 弹出子表 Tab 弹窗:开票信息 Tab + 项目价格 Tab
[ ] Step 2: 验证页面可加载
Run: cd web-admin && pnpm dev → 访问 http://localhost:port/#/basic-info/insurance-company
Expected: 页面加载,无控制台错误
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"
Files:
web-admin/src/views/cell-storage/fullmoon-todo/index.vue参照模板: web-admin/src/views/system/order/index.vue(复杂列表模式)或 system/unit/index.vue(简单列表模式)
页面为纯列表 + 编辑弹窗模式(无新增/删除,数据由系统自动生成):
列表列: | 列名 | 字段 | 宽度 | |------|------|------| | 采集协议号 | 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)API 调用:fetchFullmoonTodoList(列表)、fetchUpdateFullmoonTodoInsurance(保存)
defineOptions({ name: 'CellStorageFullmoonTodo' })
Run: 访问 http://localhost:port/#/cell-storage/fullmoon-todo
Expected: 列表加载,筛选和编辑弹窗正常
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"
Files:
web-admin/src/views/cell-storage/storage-cert/index.vue参照模板: web-admin/src/views/system/unit/index.vue(简单列表模式)
页面为纯列表 + 上传弹窗 + 查看弹窗模式(无新增/删除):
列表列: | 列名 | 字段 | 宽度 | |------|------|------| | 采集协议号 | 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 |
筛选栏: 关键词(协议号/姓名)、制作状态下拉(全部/未制作/已制作)
操作按钮:
上传储存证弹窗:
保存逻辑:调用现有采集协议单编辑接口中的 storageCert 子资源保存(upsert_storage_cert),或者新增一个专用接口。
defineOptions({ name: 'CellStorageStorageCert' })
Run: 访问 http://localhost:port/#/cell-storage/storage-cert
Expected: 列表加载,上传和查看功能正常
git add web-admin/src/views/cell-storage/storage-cert/index.vue
git commit -m "feat: add storage certificate management page"
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);
在角色管理中,为需要的角色勾选新增的 3 个菜单权限。
git add docs/sql/insurance_and_fullmoon.sql
git commit -m "feat: add menu initialization SQL for new modules"