基于 因公付款服务系统架构设计.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/ # 现有任务调度模块(保留)
name = "payment"
title = "因公付款服务"
version = "1.0.0"
description = "基于支付宝企业码的因公付款服务系统核心模块"
optional = false
tags = ["payment", "enterprise", "expense", "alipay"]
| 子模块 | 职责 | 对应支付宝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 |
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="状态")
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")
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="绑定时间")
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="状态")
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")
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="状态")
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="状态")
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="凭证内容")
通知处理器采用策略模式,根据消息类型分发到不同的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,
}
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
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")
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
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
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}"
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 # 企业注销
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 # 获取邀请链接
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 # 编辑制度成员
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} # 删除额度
GET /api/payment/bill/{bill_id} # 查询账单详情
GET /api/payment/bill/list # 查询账单列表
GET /api/payment/bill/download # 下载对账单
POST /api/payment/pay/scan-url # 生成扫一扫跳转链接
POST /api/payment/pay/code-url # 生成付款码跳转链接
POST /api/payment/pay/invoice-url # 生成发票关联跳转链接
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 │
└─────────────────────────────────────────────────────────────────┘
module_payment 目录结构plugin.toml 元数据| 周次 | 开发内容 |
|---|---|
| 第2周 | enterprise + department 模块 |
| 第3周 | employee + notification 基础 |
| 第4周 | expense_institution + expense_rule |
| 第5周 | quota + payment |
module_{一级模块}_{二级模块} 的目录命名约定ModelMixin,支持通用CRUDouter_source_id 保证幂等