Plugin模块规划.md 28 KB

因公付款服务系统 - Plugin 模块规划

一、规划背景

基于 因公付款服务系统架构设计.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 配置

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)

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)

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)

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)

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)

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)

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)

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)

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处理:

# 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)

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)

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)

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)

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)

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. 事务管理:跨表操作需使用数据库事务保证一致性