# 因公付款服务系统 - Plugin 模块规划 ## 一、规划背景 基于 [因公付款服务系统架构设计.md](../docs/因公付款服务系统架构设计.md) 文档,本规划旨在将因公付款服务的核心功能模块落地到 `app/plugin/` 目录下,采用 `module_payment` 作为一级模块,下设多个二级子模块。 ## 二、目录结构设计 ``` app/plugin/ ├── module_payment/ # 因公付款服务一级模块 │ │ │ ├── plugin.toml # 模块元数据 │ │ │ ├── enterprise/ # 企业管理子模块 │ │ ├── __init__.py │ │ ├── model.py # 企业实体定义 │ │ ├── schema.py # Pydantic请求/响应模型 │ │ ├── crud.py # 数据库CRUD操作 │ │ ├── service.py # 业务逻辑层 │ │ ├── controller.py # API路由控制器 │ │ └── utils.py # 工具函数 │ │ │ ├── department/ # 部门管理子模块 │ │ ├── __init__.py │ │ ├── model.py │ │ ├── schema.py │ │ ├── crud.py │ │ ├── service.py │ │ └── controller.py │ │ │ ├── employee/ # 员工管理子模块 │ │ ├── __init__.py │ │ ├── model.py │ │ ├── schema.py │ │ ├── crud.py │ │ ├── service.py │ │ ├── controller.py │ │ └── utils.py │ │ │ ├── expense_institution/ # 费控制度子模块 │ │ ├── __init__.py │ │ ├── model.py │ │ ├── schema.py │ │ ├── crud.py │ │ ├── service.py │ │ └── controller.py │ │ │ ├── expense_rule/ # 使用规则子模块 │ │ ├── __init__.py │ │ ├── model.py │ │ ├── schema.py │ │ ├── crud.py │ │ ├── service.py │ │ └── controller.py │ │ │ ├── quota/ # 额度管理子模块 │ │ ├── __init__.py │ │ ├── model.py │ │ ├── schema.py │ │ ├── crud.py │ │ ├── service.py │ │ └── controller.py │ │ │ ├── bill/ # 账单管理子模块 │ │ ├── __init__.py │ │ ├── model.py │ │ ├── schema.py │ │ ├── crud.py │ │ ├── service.py │ │ ├── controller.py │ │ └── utils.py │ │ │ ├── voucher/ # 凭证管理子模块 │ │ ├── __init__.py │ │ ├── model.py │ │ ├── schema.py │ │ ├── crud.py │ │ ├── service.py │ │ └── controller.py │ │ │ ├── payment/ # 支付跳转子模块 │ │ ├── __init__.py │ │ ├── schema.py │ │ ├── service.py │ │ ├── controller.py │ │ └── utils.py # 支付宝跳转链接生成工具 │ │ │ └── notification/ # 消息通知子模块 │ ├── __init__.py │ ├── handlers/ # 通知处理器 │ │ ├── __init__.py │ │ ├── enterprise_handler.py # 企业状态变更处理 │ │ ├── employee_handler.py # 员工变更处理 │ │ ├── bill_handler.py # 账单变动处理 │ │ └── voucher_handler.py # 凭证变动处理 │ ├── service.py │ └── controller.py # WebSocket/HTTP回调入口 │ ├── module_ai/ # 现有AI模块(保留) ├── module_example/ # 现有示例模块(保留) ├── module_generator/ # 现有代码生成模块(保留) └── module_task/ # 现有任务调度模块(保留) ``` ## 三、模块职责划分 ### 3.1 module_payment/plugin.toml 配置 ```toml name = "payment" title = "因公付款服务" version = "1.0.0" description = "基于支付宝企业码的因公付款服务系统核心模块" optional = false tags = ["payment", "enterprise", "expense", "alipay"] ``` ### 3.2 各子模块详细职责 | 子模块 | 职责 | 对应支付宝API | 优先级 | |-------|------|--------------|-------| | enterprise | 企业入驻、签约、解约、注销 | alipay.commerce.ec.enterprise.* | P0 | | department | 部门创建、修改、删除、查询 | (本地管理,无支付宝API) | P1 | | employee | 员工添加、删除、激活、邀请链接 | alipay.commerce.ec.employee.* | P0 | | expense_institution | 费控制度CRUD、成员管理 | alipay.ebpp.invoice.institution.* | P0 | | expense_rule | 使用规则创建、编辑、删除 | alipay.ebpp.invoice.institution.expenserule.* | P1 | | quota | 额度创建、发放、查询、修改 | alipay.ebpp.invoice.expensecontrol.quota.* | P0 | | bill | 账单接收、查询、对账 | alipay.commerce.ec.consume.* | P0 | | voucher | 凭证存储、查询 | alipay.commerce.ec.voucher.* | P1 | | payment | 扫一扫/付款码跳转链接生成 | (本地生成,无支付宝API) | P0 | | notification | 支付宝消息接收、处理、分发 | (Webhook回调处理) | P0 | ## 四、数据模型设计 ### 4.1 企业模块 (enterprise/model.py) ```python import enum from sqlalchemy import String, DateTime from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin, UserMixin class EnterpriseStatusEnum(enum.Enum): REGISTERED = "registered" # 已注册 SIGNED = "signed" # 已签约 UNSIGNED = "unsigned" # 已解约 DELETED = "deleted" # 已注销 class EnterpriseModel(ModelMixin, UserMixin): __tablename__ = "t_enterprise" __table_args__ = {"comment": "企业表"} enterprise_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="企业码企业ID") account_id: Mapped[str | None] = mapped_column(String(64), comment="支付宝账号ID") name: Mapped[str] = mapped_column(String(128), comment="企业名称") status: Mapped[str] = mapped_column(String(32), default=EnterpriseStatusEnum.REGISTERED.value, comment="状态") ``` ### 4.2 部门模块 (department/model.py) ```python from sqlalchemy import String, ForeignKey from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin class DepartmentModel(ModelMixin): __tablename__ = "t_department" __table_args__ = {"comment": "部门表"} department_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="部门ID") enterprise_id: Mapped[str] = mapped_column(String(64), index=True, comment="所属企业ID") name: Mapped[str] = mapped_column(String(128), comment="部门名称") parent_id: Mapped[str | None] = mapped_column(String(64), comment="上级部门ID") ``` ### 4.3 员工模块 (employee/model.py) ```python import enum from sqlalchemy import String, DateTime, ForeignKey from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin, UserMixin class EmployeeStatusEnum(enum.Enum): PENDING = "pending" # 待激活 ACTIVE = "active" # 已激活 DELETED = "deleted" # 已删除 class EmployeeModel(ModelMixin, UserMixin): __tablename__ = "t_employee" __table_args__ = {"comment": "员工表"} employee_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="员工ID") enterprise_id: Mapped[str] = mapped_column(String(64), index=True, comment="所属企业ID") alipay_user_id: Mapped[str | None] = mapped_column(String(64), comment="支付宝用户ID") name: Mapped[str] = mapped_column(String(64), comment="员工姓名") phone: Mapped[str | None] = mapped_column(String(32), comment="手机号") email: Mapped[str | None] = mapped_column(String(128), comment="邮箱") department_id: Mapped[str | None] = mapped_column(String(64), comment="部门ID") status: Mapped[str] = mapped_column(String(32), default=EmployeeStatusEnum.PENDING.value, comment="状态") bind_time: Mapped[datetime | None] = mapped_column(DateTime, comment="绑定时间") ``` ### 4.4 费控制度模块 (expense_institution/model.py) ```python from sqlalchemy import String, DECIMAL, DateTime from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin, UserMixin class ExpenseInstitutionModel(ModelMixin, UserMixin): __tablename__ = "t_expense_institution" __table_args__ = {"comment": "费控制度表"} institution_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="制度ID") enterprise_id: Mapped[str] = mapped_column(String(64), index=True, comment="所属企业ID") name: Mapped[str] = mapped_column(String(128), comment="制度名称") expense_type: Mapped[str] = mapped_column(String(32), default="DEFAULT", comment="费用类型") expense_sub_type: Mapped[str] = mapped_column(String(32), default="DEFAULT", comment="费用类型子类") status: Mapped[str] = mapped_column(String(32), default="active", comment="状态") ``` ### 4.5 使用规则模块 (expense_rule/model.py) ```python from sqlalchemy import String, DECIMAL, DateTime, ForeignKey from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin class ExpenseRuleModel(ModelMixin): __tablename__ = "t_expense_rule" __table_args__ = {"comment": "使用规则表"} rule_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="规则ID") institution_id: Mapped[str] = mapped_column(String(64), index=True, comment="所属制度ID") name: Mapped[str | None] = mapped_column(String(128), comment="规则名称") max_amount: Mapped[float | None] = mapped_column(DECIMAL(12,2), comment="最大限额") valid_from: Mapped[datetime | None] = mapped_column(DateTime, comment="有效期开始") valid_to: Mapped[datetime | None] = mapped_column(DateTime, comment="有效期结束") merchant_pid: Mapped[str | None] = mapped_column(String(64), comment="限定商户PID") ``` ### 4.6 额度模块 (quota/model.py) ```python import enum from sqlalchemy import String, DECIMAL, DateTime, ForeignKey from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin, UserMixin class QuotaStatusEnum(enum.Enum): ACTIVE = "active" # 正常 FROZEN = "frozen" # 冻结 EXHAUSTED = "exhausted" # 已用完 EXPIRED = "expired" # 已过期 class QuotaModel(ModelMixin, UserMixin): __tablename__ = "t_quota" __table_args__ = {"comment": "额度表"} quota_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="额度ID") employee_id: Mapped[str] = mapped_column(String(64), index=True, comment="员工ID") institution_id: Mapped[str] = mapped_column(String(64), index=True, comment="制度ID") outer_source_id: Mapped[str] = mapped_column(String(64), unique=True, comment="外部来源ID(幂等)") total_amount: Mapped[float] = mapped_column(DECIMAL(12,2), comment="总金额") available_amount: Mapped[float] = mapped_column(DECIMAL(12,2), comment="可用金额") valid_from: Mapped[datetime] = mapped_column(DateTime, comment="有效期开始") valid_to: Mapped[datetime] = mapped_column(DateTime, comment="有效期结束") status: Mapped[str] = mapped_column(String(32), default=QuotaStatusEnum.ACTIVE.value, comment="状态") ``` ### 4.7 账单模块 (bill/model.py) ```python import enum from sqlalchemy import String, DECIMAL, DateTime, Text from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin class BillStatusEnum(enum.Enum): PAID = "paid" # 已支付 REFUNDED = "refunded" # 已退款 CLOSED = "closed" # 已关闭 class BillModel(ModelMixin): __tablename__ = "t_bill" __table_args__ = {"comment": "账单表"} bill_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="账单ID") trade_no: Mapped[str] = mapped_column(String(64), index=True, comment="支付宝交易号") enterprise_id: Mapped[str] = mapped_column(String(64), index=True, comment="企业ID") employee_id: Mapped[str] = mapped_column(String(64), index=True, comment="员工ID") institution_id: Mapped[str | None] = mapped_column(String(64), comment="制度ID") quota_id: Mapped[str | None] = mapped_column(String(64), comment="额度ID") total_amount: Mapped[float] = mapped_column(DECIMAL(12,2), comment="账单金额") enterprise_pay_amount: Mapped[float] = mapped_column(DECIMAL(12,2), default=0, comment="企业付金额") personal_pay_amount: Mapped[float] = mapped_column(DECIMAL(12,2), default=0, comment="个人付金额") discount_amount: Mapped[float] = mapped_column(DECIMAL(12,2), default=0, comment="优惠金额") trade_time: Mapped[datetime | None] = mapped_column(DateTime, comment="交易时间") merchant_name: Mapped[str | None] = mapped_column(String(256), comment="商户名称") status: Mapped[str] = mapped_column(String(32), default=BillStatusEnum.PAID.value, comment="状态") ``` ### 4.8 凭证模块 (voucher/model.py) ```python from sqlalchemy import String, JSON, ForeignKey from sqlalchemy.orm import Mapped, mapped_column from app.core.base_model import ModelMixin class VoucherModel(ModelMixin): __tablename__ = "t_voucher" __table_args__ = {"comment": "凭证表"} voucher_id: Mapped[str] = mapped_column(String(64), unique=True, index=True, comment="凭证ID") bill_id: Mapped[str] = mapped_column(String(64), index=True, comment="关联账单ID") content: Mapped[dict | None] = mapped_column(JSON, comment="凭证内容") ``` ### 4.9 消息通知模块 (notification/handlers) 通知处理器采用策略模式,根据消息类型分发到不同的Handler处理: ```python # notification/handlers/__init__.py from notification.handlers.enterprise_handler import EnterpriseChangeHandler from notification.handlers.employee_handler import EmployeeChangeHandler from notification.handlers.bill_handler import BillChangeHandler from notification.handlers.voucher_handler import VoucherChangeHandler HANDLER_MAP = { "alipay.commerce.ec.enterprise.change.notify": EnterpriseChangeHandler, "alipay.commerce.ec.employee.change.notify": EmployeeChangeHandler, "alipay.commerce.ec.consume.change.notify": BillChangeHandler, "alipay.commerce.ec.voucher.change.notify": VoucherChangeHandler, } ``` ## 五、服务层设计 ### 5.1 企业签约服务 (enterprise/service.py) ```python from typing import TYPE_CHECKING from alipaySdk import AlipaySDK if TYPE_CHECKING: from enterprise.model import EnterpriseModel class EnterpriseService: def __init__(self, alipay_sdk: AlipaySDK): self._sdk = alipay_sdk async def create_invite_url(self, enterprise: "EnterpriseModel") -> str: """生成企业注册邀请链接""" result = await self._sdk.execute( "alipay.commerce.ec.enterprise.registerinvite.create", {"enterprise_name": enterprise.name} ) return result.get("invite_url") async def handle_status_change(self, notify_data: dict) -> None: """处理企业状态变更通知""" # 更新企业状态 pass ``` ### 5.2 员工服务 (employee/service.py) ```python class EmployeeService: async def add_employee(self, enterprise_id: str, name: str, phone: str) -> dict: """添加企业员工""" result = await self._sdk.execute( "alipay.commerce.ec.employee.add", { "enterprise_id": enterprise_id, "name": name, "phone": phone, } ) return result async def delete_employee(self, employee_id: str) -> dict: """删除企业员工""" result = await self._sdk.execute( "alipay.commerce.ec.employee.delete", {"employee_id": employee_id} ) return result async def get_invite_url(self, employee_id: str) -> str: """获取员工邀请链接""" result = await self._sdk.execute( "alipay.commerce.ec.employee.invite.query", {"employee_id": employee_id} ) return result.get("sign_url") ``` ### 5.3 费控服务 (expense_institution/service.py) ```python class ExpenseInstitutionService: async def create_institution(self, enterprise_id: str, name: str, **kwargs) -> dict: """创建费控制度""" result = await self._sdk.execute( "alipay.ebpp.invoice.institution.create", { "enterprise_id": enterprise_id, "name": name, "expense_type": kwargs.get("expense_type", "DEFAULT"), "expense_sub_type": kwargs.get("expense_sub_type", "DEFAULT"), } ) return result async def modify_institution(self, institution_id: str, **kwargs) -> dict: """编辑费控制度""" result = await self._sdk.execute( "alipay.ebpp.invoice.institution.modify", {"institution_id": institution_id, **kwargs} ) return result ``` ### 5.4 额度服务 (quota/service.py) ```python class QuotaService: async def create_quota(self, employee_id: str, institution_id: str, total_amount: float, valid_from: datetime, valid_to: datetime, outer_source_id: str) -> dict: """创建手工发放额度""" result = await self._sdk.execute( "alipay.ebpp.invoice.expensecontrol.quota.create", { "target_type": "INSTITUTION", "target_id": institution_id, "employee_id": employee_id, "amount": total_amount, "valid_from": valid_from.isoformat(), "valid_to": valid_to.isoformat(), "outer_source_id": outer_source_id, } ) return result async def query_quota(self, employee_id: str, institution_id: str) -> dict: """查询员工可用额度""" result = await self._sdk.execute( "alipay.ebpp.invoice.expensecontrol.quota.query", { "employee_id": employee_id, "target_type": "INSTITUTION", "target_id": institution_id, } ) return result ``` ### 5.5 支付跳转服务 (payment/service.py) ```python import json from urllib.parse import quote class PaymentService: def generate_scan_params(self, account_id: str, rule_group_id: str, payment_id: str, isv_app_id: str) -> dict: """生成扫一扫跳转参数""" params = { "pdSubBizScene": "enterprisePay", "specifiedEnableChannelInfo": json.dumps({ "enableScene": "agreementpay", "assetInfo": { "instId": "INST_ALIPAY", "assetId": account_id, "assetTypeCode": "ENTERPRISEPAY", "assetType": "ENTERPRISEPAYASSET" } }), "assignJointAccountId": account_id, "identityPayBizInfo": json.dumps({ "identityPaySubBizScene": "ISV_PAY", "bizGroupId": account_id, "groupId": account_id, "identityPayBizScene": "ENTERPRISE_CODE" }), "CHANNEL_INDEX": "[\"ENTERPRISEPAYASSET_DC_ENTERPRISEPAY_DEFAULT\"]", "enterprise_pay_info": json.dumps({ "paymentId": payment_id, "ruleGroupId": rule_group_id }), "sourcePlatformInfo": json.dumps({ "paymentId": payment_id, "ruleGroupId": rule_group_id, "isvAppId": isv_app_id }) } return params def generate_payment_code_url(self, account_id: str, rule_group_id: str, payment_id: str, isv_app_id: str) -> str: """生成付款码跳转链接""" params = self.generate_scan_params(account_id, rule_group_id, payment_id, isv_app_id) params["channelMode"] = "NONE_CHANNEL_MODE" encoded_params = quote(json.dumps(params), safe='') return f"alipays://platformapi/startapp?appId=20000056&customBizCode=enterprisePayForThirdPart&customBizParams={encoded_params}" ``` ## 六、API路由设计 ### 6.1 企业管理路由 (enterprise/controller.py) ``` POST /api/payment/enterprise/invite # 创建企业邀请 GET /api/payment/enterprise/{enterprise_id} # 查询企业详情 PUT /api/payment/enterprise/{enterprise_id} # 修改企业信息 POST /api/payment/enterprise/{enterprise_id}/unsign # 企业解约 POST /api/payment/enterprise/{enterprise_id}/delete # 企业注销 ``` ### 6.2 员工管理路由 (employee/controller.py) ``` POST /api/payment/employee # 添加员工 DELETE /api/payment/employee/{employee_id} # 删除员工 GET /api/payment/employee/{employee_id} # 查询员工详情 PUT /api/payment/employee/{employee_id} # 修改员工信息 GET /api/payment/employee/list # 查询员工列表 GET /api/payment/employee/{employee_id}/invite-url # 获取邀请链接 ``` ### 6.3 费控管理路由 (expense_institution/controller.py) ``` POST /api/payment/institution # 创建费控制度 PUT /api/payment/institution/{institution_id} # 编辑费控制度 GET /api/payment/institution/{institution_id} # 查询制度详情 GET /api/payment/institution/list # 查询制度列表 DELETE /api/payment/institution/{institution_id} # 删除费控制度 POST /api/payment/institution/{institution_id}/members # 编辑制度成员 ``` ### 6.4 额度管理路由 (quota/controller.py) ``` POST /api/payment/quota # 创建额度 GET /api/payment/quota/{quota_id} # 查询额度详情 GET /api/payment/quota/employee/{employee_id} # 查询员工可用额度 PUT /api/payment/quota/{quota_id} # 修改额度 DELETE /api/payment/quota/{quota_id} # 删除额度 ``` ### 6.5 账单管理路由 (bill/controller.py) ``` GET /api/payment/bill/{bill_id} # 查询账单详情 GET /api/payment/bill/list # 查询账单列表 GET /api/payment/bill/download # 下载对账单 ``` ### 6.6 支付跳转路由 (payment/controller.py) ``` POST /api/payment/pay/scan-url # 生成扫一扫跳转链接 POST /api/payment/pay/code-url # 生成付款码跳转链接 POST /api/payment/pay/invoice-url # 生成发票关联跳转链接 ``` ### 6.7 消息通知路由 (notification/controller.py) ``` POST /api/payment/notification/enterprise # 企业状态变更回调 POST /api/payment/notification/employee # 员工变更回调 POST /api/payment/notification/bill # 账单变动回调 POST /api/payment/notification/voucher # 凭证变动回调 WS /ws/payment/notification # WebSocket长连接 ``` ## 七、依赖关系 ``` ┌─────────────────────────────────────────────────────────────────┐ │ API Controller Layer │ │ enterprise | employee | expense_institution | quota | bill │ │ payment | notification │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Service Layer │ │ EnterpriseService | EmployeeService | ExpenseService | ... │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ Alipay SDK Layer │ │ (统一封装支付宝API调用) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ CRUD Layer │ │ Model | Schema | CRUD (与数据库交互) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ PostgreSQL Database │ └─────────────────────────────────────────────────────────────────┘ ``` ## 八、实施计划 ### 阶段一:基础框架(1周) 1. 创建 `module_payment` 目录结构 2. 定义 `plugin.toml` 元数据 3. 创建各子模块的基础文件骨架 4. 配置数据库连接和ORM模型 ### 阶段二:核心模块开发(3-4周) | 周次 | 开发内容 | |-----|---------| | 第2周 | enterprise + department 模块 | | 第3周 | employee + notification 基础 | | 第4周 | expense_institution + expense_rule | | 第5周 | quota + payment | ### 阶段三:账单和凭证模块(1-2周) 1. bill 模块开发 2. voucher 模块开发 3. 消息通知处理器完善 ### 阶段四:集成测试(1周) 1. 模块间依赖测试 2. 支付宝API联调 3. 整体业务流程测试 ## 九、注意事项 1. **命名规范**:遵循 `module_{一级模块}_{二级模块}` 的目录命名约定 2. **ModelMixin**:所有模型类需继承 `ModelMixin`,支持通用CRUD 3. **幂等性**:额度等关键操作需使用 `outer_source_id` 保证幂等 4. **错误处理**:支付宝API调用需统一异常处理和日志记录 5. **事务管理**:跨表操作需使用数据库事务保证一致性