# 发票平台账号配置设计 ## 背景 当前发票模块已经有三类配置和业务数据: - `biz_invoice`:开票业务配置,按水司账户维护供应商、开票限额、开票类型、自动开票、扩展参数。 - `biz_invoice_taxrate`:税收分类编码、税率、规格单位等明细配置。 - `biz_cust_invoice`:客户发票抬头、税号、开户行、地址电话等客户资料。 这几类配置可以支撑“能不能开票、怎么组成发票明细、给谁开票”,但没有独立维护“对接哪个发票平台账号、平台地址、APPID、销方税号、回调地址、密钥引用”的能力。诺诺联调已经证明接口链路可通,但当前实现仍偏向代码或环境变量配置,不适合运营后台长期维护,也不适合未来接入方欣、百望、航信、用友等其他供应商。 ## 目标 1. 把发票平台账号做成通用配置,不绑定诺诺专有表结构。 2. 允许一个水司账户配置多个发票平台账号,并由 `biz_invoice` 业务配置选择默认账号。 3. 发票申请、查询、作废、冲红都能回溯当时使用的平台账号,避免后续配置变更影响已开票记录。 4. 密钥不落业务库,只在业务库保存 `secret_ref`,实际密钥从环境变量、Nacos、KMS 或 Vault 解析。 5. 保持现有 `/business/invoice` 配置语义,让业务开票规则和平台账号解耦。 ## 不纳入本次范围 - 不一次性实现所有外部供应商客户端。 - 不重做发票税率、客户抬头、费用组成等既有配置。 - 不迁移历史发票记录的平台账号归属;历史记录可以继续按已有字段展示和查询。 - 不把真实 APPKEY、私钥、证书内容保存到数据库。 ## 核心概念 | 概念 | 存储位置 | 说明 | | --- | --- | --- | | 供应商模板 `supplier` | `biz_invoice.supplier` | 现有枚举 `InvoiceTemplateEnum`,描述业务采用哪套开票模板或供应商类型。 | | 平台编码 `platform_code` | 新平台账号表、`biz_invoice`、`biz_invoice_record` | 稳定技术路由键,例如 `NUONUO`、`FANGXIN`、`BAIWANG`、`HANGXIN`。 | | 平台账号编码 `platform_account_code` | 新平台账号表、`biz_invoice`、`biz_invoice_record` | 同一平台下的账号标识,例如 `NUONUO_STAGE_339901999999142`。 | | 平台密钥引用 `app_key_secret_ref` | 新平台账号表 | 指向外部密钥来源的引用值,不保存密钥明文。 | 供应商模板和平台编码不强制一一对应。比如 `NUONUO_QUANDIAN` 和 `NUONUO` 可以复用 `platform_code=NUONUO`,通过模板字段决定业务明细映射,通过平台编码决定调用哪个技术客户端。 ## 数据模型 ### 新增表 `biz_invoice_platform_account` 建议新增 PostgreSQL DDL: ```sql CREATE TABLE IF NOT EXISTS biz_invoice_platform_account ( id BIGINT NOT NULL, account_id BIGINT NOT NULL, platform_code VARCHAR(32) NOT NULL, platform_account_code VARCHAR(64) NOT NULL, account_name VARCHAR(100) NOT NULL, base_url VARCHAR(255) NOT NULL, version VARCHAR(32), app_id VARCHAR(128) NOT NULL, app_key_secret_ref VARCHAR(255) NOT NULL, seller_taxnum VARCHAR(50) NOT NULL, company_code VARCHAR(64), callback_url VARCHAR(512), default_buyer_email VARCHAR(100), default_buyer_mobile VARCHAR(20), extra_properties TEXT, status SMALLINT NOT NULL DEFAULT 1, tenant_id BIGINT, creator VARCHAR(64), create_time TIMESTAMP NOT NULL DEFAULT now(), updater VARCHAR(64), update_time TIMESTAMP NOT NULL DEFAULT now(), deleted SMALLINT NOT NULL DEFAULT 0, PRIMARY KEY (id) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_invoice_platform_account_code ON biz_invoice_platform_account(account_id, platform_code, platform_account_code) WHERE deleted = 0; CREATE INDEX IF NOT EXISTS idx_invoice_platform_account_account ON biz_invoice_platform_account(account_id, status) WHERE deleted = 0; ``` 字段说明: | 字段 | 用途 | | --- | --- | | `account_id` | 水司账户 ID,和现有开票配置保持同一归属维度。 | | `platform_code` | 技术平台编码,作为客户端注册和路由键。 | | `platform_account_code` | 账号编码,业务配置和发票记录只引用这个编码。 | | `base_url` | 平台环境地址,例如诺诺测试环境 `https://cmp-stage.nntest.cn`。 | | `version` | 平台接口版本,诺诺当前使用 `1.0.0`。 | | `app_id` | 平台分配的应用标识。 | | `app_key_secret_ref` | 密钥引用,例如 `env:NUONUO_STAGE_APP_KEY` 或 `nacos:invoice.nuonuo.stage.app-key`。 | | `seller_taxnum` | 销方税号。 | | `company_code` | 平台侧企业编码,有的平台不需要时为空。 | | `callback_url` | 平台回调地址。 | | `extra_properties` | 平台差异化配置,JSON 字符串,例如门店编码、开票员、收款人、复核人。 | | `status` | 启停状态,`1` 启用,`0` 停用。 | ### 扩展 `biz_invoice` 在现有开票业务配置表增加默认平台账号绑定: ```sql ALTER TABLE biz_invoice ADD COLUMN IF NOT EXISTS platform_code VARCHAR(32); ALTER TABLE biz_invoice ADD COLUMN IF NOT EXISTS platform_account_code VARCHAR(64); CREATE INDEX IF NOT EXISTS idx_biz_invoice_platform_route ON biz_invoice(account_id, supplier, invoice_type, platform_code, platform_account_code) WHERE deleted = 0; ``` `biz_invoice` 继续表示开票规则。新增字段只表示这条业务规则默认走哪个平台账号。 ### 复用 `biz_invoice_record` 发票记录中已有或计划中的 `platform_code`、`platform_account_code` 字段继续保留。申请成功或进入平台处理中时,记录本次使用的平台编码和账号编码。后续查询、作废、冲红优先使用发票记录上的平台字段,不再根据当前业务配置重新选择账号。 ## 密钥方案 数据库只保存 `app_key_secret_ref`,不保存真实密钥。后端增加统一解析接口: ```java public interface InvoicePlatformSecretProvider { String resolve(String secretRef); } ``` 第一阶段支持两种引用: | 引用格式 | 解析来源 | 用途 | | --- | --- | --- | | `env:NUONUO_STAGE_APP_KEY` | 系统环境变量 | 本地联调、流水线、临时测试。 | | `nacos:invoice.nuonuo.stage.app-key` | 应用配置中心属性 | 测试环境、生产环境。 | 后续接入 KMS 或 Vault 时扩展该接口,不影响业务表结构和平台账号表结构。日志中禁止打印 `appKey`、解密后的密钥、`secret_ref` 解析结果。 ## 路由流程 ### 开票申请 1. 根据 `chargeIds` 校验费用归属,并解析水司账户 `accountId`。 2. 读取 `biz_invoice` 中启用的业务配置,匹配 `accountId`、`invoiceType`,并优先使用请求中的 `supplier`。 3. 如果请求没有传 `supplier`,且同一 `accountId + invoiceType` 只有一条启用配置,则使用该配置。 4. 如果请求没有传 `supplier`,且存在多条启用配置,返回明确业务异常,要求前端传供应商模板。 5. 使用业务配置中的 `platform_code`、`platform_account_code` 查询启用的平台账号。 6. 从 `InvoicePlatformClientRegistry` 按 `platform_code` 获取客户端。 7. 调用平台客户端,写入 `biz_invoice_record.platform_code`、`biz_invoice_record.platform_account_code`、`sys_request_no` 和平台返回状态。 ### 查询、作废、冲红 1. 根据申请单号或发票记录 ID 读取 `biz_invoice_record`。 2. 优先使用记录中的 `platform_code`、`platform_account_code` 查询平台账号。 3. 如果历史记录缺少平台字段,且系统中只有一个可用平台账号,可以走兼容兜底;如果存在多个候选账号,返回需要人工确认的业务异常。 4. 按 `platform_code` 取客户端并执行对应操作。 5. 更新发票记录状态,不覆盖历史平台字段。 ## 服务分层 新增后端结构: ```text controller/admin/invoiceplatform/ InvoicePlatformAccountController.java vo/InvoicePlatformAccountCreateReqVO.java vo/InvoicePlatformAccountUpdateReqVO.java vo/InvoicePlatformAccountPageReqVO.java vo/InvoicePlatformAccountRespVO.java vo/InvoicePlatformAccountTestReqVO.java vo/InvoicePlatformAccountTestRespVO.java dal/dataobject/invoiceplatform/ InvoicePlatformAccountDO.java dal/mysql/invoiceplatform/ InvoicePlatformAccountMapper.java service/invoice/platform/config/ InvoicePlatformAccountConfig.java InvoicePlatformSecretProvider.java EnvAndPropertyInvoicePlatformSecretProvider.java service/invoice/platform/routing/ InvoicePlatformAccountResolver.java InvoicePlatformClientRegistry.java InvoiceRouteContext.java ``` 管理后台 API: | 方法 | 路径 | 说明 | | --- | --- | --- | | `POST` | `/business/invoice-platform-account/create` | 创建平台账号。 | | `PUT` | `/business/invoice-platform-account/update` | 更新平台账号,密钥只更新引用。 | | `DELETE` | `/business/invoice-platform-account/delete?id=` | 删除平台账号,已有发票记录不变。 | | `GET` | `/business/invoice-platform-account/get?id=` | 获取详情,返回 `secret_ref`,不返回密钥。 | | `GET` | `/business/invoice-platform-account/page` | 分页查询。 | | `POST` | `/business/invoice-platform-account/test-connect` | 使用账号配置发起平台连通性测试。 | ## 客户端注册 每个供应商客户端声明自己的平台编码: ```java public interface InvoicePlatformClient { String platformCode(); ApplyResult apply(ApplyContext ctx); QueryResult query(QueryContext ctx); InvalidateResult invalidate(PostProcessContext ctx); RedInkResult redInk(PostProcessContext ctx); } ``` 注册表在启动时收集所有客户端: ```java @Component public class InvoicePlatformClientRegistry { private final Map clients; public InvoicePlatformClientRegistry(List candidates) { this.clients = candidates.stream() .collect(Collectors.toUnmodifiableMap( client -> client.platformCode().toUpperCase(Locale.ROOT), Function.identity())); } public InvoicePlatformClient getRequired(String platformCode) { InvoicePlatformClient client = clients.get(platformCode.toUpperCase(Locale.ROOT)); if (client == null) { throw exception(INVOICE_PLATFORM_CLIENT_NOT_EXISTS); } return client; } } ``` 这样平台选择从“Spring 只注入一个全局 `InvoicePlatformClient`”升级为“按记录或业务配置路由到对应客户端”。 ## 诺诺账号示例 联调账号在平台账号表中表现为: | 字段 | 值 | | --- | --- | | `account_id` | 对应水司账户 ID | | `platform_code` | `NUONUO` | | `platform_account_code` | `NUONUO_STAGE_339901999999142` | | `account_name` | `诺诺测试账号` | | `base_url` | `https://cmp-stage.nntest.cn` | | `version` | `1.0.0` | | `app_id` | `convert` | | `app_key_secret_ref` | `env:NUONUO_STAGE_APP_KEY` | | `seller_taxnum` | `339901999999142` | 真实密钥通过运行环境设置 `NUONUO_STAGE_APP_KEY`,不写入仓库、不写入数据库迁移脚本。 ## 兼容策略 1. 现有 `biz_invoice.extra_properties` 保留,用于开票业务模板的扩展参数。 2. 现有诺诺 YAML 配置可以保留为本地联调兜底,但生产调用优先使用平台账号表。 3. 平台账号表上线后,新增发票记录必须写入 `platform_code` 和 `platform_account_code`。 4. 历史记录缺少平台字段时,只允许在唯一候选账号场景自动兜底。 5. `InvoiceApplyReqVO` 增加可选 `supplier` 字段,前端在多供应商场景必须传值。 ## 测试策略 | 层级 | 覆盖点 | | --- | --- | | Mapper/Service 单测 | 平台账号创建、更新、禁用、唯一性、分页过滤。 | | Secret Provider 单测 | `env:` 和 `nacos:` 引用解析,缺失密钥报错,日志不包含密钥值。 | | Resolver 单测 | 单配置自动选择、多配置歧义报错、停用账号不可用、记录字段优先。 | | Registry 单测 | 多客户端注册、重复平台编码启动失败、未知平台编码报错。 | | InvoiceRecordService 单测 | 开票申请写入平台字段,查询/作废/冲红按历史记录路由。 | | Nuonuo 合约测试 | 请求体使用账号表配置组装,查询接口兼容空数组、对象数组和失败响应。 | | Stage Smoke | 使用 `NUONUO_STAGE_APP_KEY` 环境变量执行诺诺测试环境查询接口,默认跳过。 | ## 落地顺序 1. 新增平台账号表、DO、Mapper、VO、Controller、Service。 2. 增加 `biz_invoice` 默认平台账号字段,并更新创建、更新、返回 VO。 3. 新增密钥解析接口和运行时账号配置对象。 4. 新增平台账号解析器和平台客户端注册表。 5. 改造发票申请、查询、作废、冲红调用链。 6. 把诺诺客户端从固定属性读取改为接收运行时账号配置。 7. 增加单元测试、契约测试和默认跳过的联调 Smoke 测试。