Files
Ticket/history/系统设计.md
2026-05-07 10:25:02 +08:00

11 KiB
Raw Permalink Blame History

农产品收购发票开票平台系统设计

1. 系统定位

本平台作为“业务系统”和“票通数电发票平台”之间的开票中台:

  • 前端 Web:登录、查看待开票农产品收购数据、选择数据、发起开票、处理票通认证、查看开票结果。
  • Ktor 后端:统一封装我方业务接口、票通接口、票通加密签名、开票状态机、结果回写、重试与审计。
  • 我方业务系统:提供公司、待开票列表、开票前写库、开票结果回写等接口。
  • 票通系统:提供数电账号认证、短信登录、实名认证二维码、蓝字发票开具、发票查询、结果推送等接口。

当前文档资料中,本业务主要对应票通“直接开票”场景:调用 invoiceBlue.pt 开具蓝字数电发票;开票结果通过票通推送或主动查询同步。票通要求业务报文使用 UTF-8,业务 JSON 先 3DES 加密,外层报文再按字段排序后用 RSA 签名。

2. 总体架构

flowchart LR
  Web["Web 前端"]
  Ktor["Ktor 后端"]
  Biz["我方业务系统"]
  DB["平台数据库"]
  PT["票通 OpenAPI"]

  Web -->|"登录/查询/开票/认证"| Ktor
  Ktor -->|"公司、待开票、预写库、状态回写"| Biz
  Ktor -->|"开票任务、状态、票通账号、审计日志"| DB
  Ktor -->|"加密签名请求"| PT
  PT -->|"推送结果 callback"| Ktor
  Ktor -->|"状态更新"| Web

建议 Ktor 后端承担“防重复、状态机、签名加密、票通异常兼容”的职责,前端不要直接调用票通,也不要持有票通密钥。

3. 核心业务流程

3.1 平台登录与公司上下文

  1. 用户登录 Web。
  2. Ktor 调用我方登录或用户信息接口,获取 userIdcompanyId、公司名称、纳税人识别号 taxpayerNum
  3. Ktor 建立平台会话/JWT,前端后续请求都带当前公司上下文。

3.2 查询待开票列表

  1. 前端调用 GET /api/invoice-candidates
  2. Ktor 按当前 companyId 调用我方“根据公司 id 查询待开票列表”接口。
  3. Ktor 对列表做轻量规范化:金额、税率、商品名称、收购方/销售方信息、是否可开票、已锁定状态。
  4. 前端展示并支持勾选。

3.3 发起开票

  1. 前端选中待开票数据,调用 POST /api/invoice-jobs
  2. Ktor 校验当前公司票通配置和数电账号状态。
  3. 如果 getTaxBureauAccountAuthStatus.pt 返回需要认证:
    • operationProposed=1:提示扫码认证;
    • operationProposed=2:展示扫码和短信两种方式;
    • operationProposed=3:提示短信登录;
    • operationProposed=0:继续开票。
  4. Ktor 调用我方“开票前写库”接口,锁定这些业务数据,并取得我方开票批次号或业务流水号。
  5. Ktor 生成稳定且唯一的 invoiceReqSerialNo。同一张发票重试必须复用同一个流水号,避免重复开票。
  6. Ktor 按票通字段组装 invoiceBlue.pt 业务报文,调用票通。
  7. Ktor 记录票通同步返回结果:
    • 成功受理或开票中:本地状态置为 PROCESSING
    • 立即成功:置为 SUCCESS,回写我方系统;
    • 失败:置为 FAILEDNEED_AUTH,回写我方系统。

3.4 认证流程

票通文档中认证不是简单登录态,包含登录认证和风险认证。推荐做成统一认证面板。

  • 查询认证状态:getTaxBureauAccountAuthStatus.pt
  • 获取短信验证码:sendLoginSmsCode.pt
  • 短信登录:smsLogin.pt
  • 获取实名认证二维码:getAuthenticationQrcode.pt
  • 查询二维码扫码状态:queryAuthQrcodeScanStatus.pt
  • 退出电子税局登录:logout 类接口,对应文档 2.46

前端行为:

  • operationProposed=1:调用后端获取二维码,轮询扫码状态。
  • operationProposed=2:同时展示“扫码认证”和“短信认证”。
  • operationProposed=3:展示手机号/账号、验证码输入、发送验证码按钮。
  • 认证完成后,用户点击“继续开票”,后端用原 invoiceReqSerialNo 继续或重试。

3.5 开票结果同步

建议同时支持两条链路:

  1. 票通推送:实现 POST /api/callbacks/piaotong/invoice,接收票通推送发票主要信息或全票面信息。
  2. 主动查询:后台任务定时调用 queryInvoice.pt 查询 PROCESSINGNEED_AUTH_RETRYING 的发票。

票通状态码映射:

票通 code 含义 本地状态
0000 开票成功 SUCCESS
6666 未开票 PENDINGPROCESSING
7777 开票中 PROCESSING
9999 开票失败 FAILED
3999 需要扫码或短信认证 NEED_AUTH
4999 红字确认单申请中 后续冲红扩展
5999 红字确认单审核中 后续冲红扩展

结果落库后,Ktor 调用我方“写状态”接口,把成功/失败、票号、数电发票号码、失败原因、票通流水号等回写。

4. 后端模块设计

建议 Ktor 后端按以下模块拆分:

server/
  src/main/kotlin/com/bbit/agriinvoice/
    Application.kt
    config/
      AppConfig.kt
      PiaotongConfig.kt
      BizSystemConfig.kt
    routes/
      AuthRoutes.kt
      InvoiceCandidateRoutes.kt
      InvoiceJobRoutes.kt
      PiaotongAuthRoutes.kt
      PiaotongCallbackRoutes.kt
    domain/
      Company.kt
      InvoiceCandidate.kt
      InvoiceJob.kt
      InvoiceStatus.kt
      TaxAccount.kt
    service/
      LoginService.kt
      InvoiceCandidateService.kt
      InvoiceIssueService.kt
      InvoiceStatusService.kt
      PiaotongAuthService.kt
      PiaotongCallbackService.kt
    integration/
      biz/
        BizSystemClient.kt
        BizDtos.kt
      piaotong/
        PiaotongClient.kt
        PiaotongCrypto.kt
        PiaotongDtos.kt
        PiaotongEndpoints.kt
    repository/
      InvoiceJobRepository.kt
      TaxAccountRepository.kt
      AuditLogRepository.kt

关键服务职责:

  • PiaotongCrypto:3DES 加解密、RSA 签名验签、外层报文构造、响应解密。
  • PiaotongClient:封装票通 endpoint,所有票通接口只接受/返回业务 DTO。
  • InvoiceIssueService:开票主流程、幂等控制、状态推进。
  • PiaotongAuthService:数电账号状态查询、短信登录、二维码认证。
  • InvoiceStatusService:票通状态映射、本地落库、我方业务系统状态回写。

5. Web 页面设计

5.1 页面

  • 登录页:登录平台并进入公司上下文。
  • 待开票列表页:筛选、勾选、查看金额合计、发起开票。
  • 开票确认弹窗:展示本次开票单据、购买方/销售方、商品行、价税合计。
  • 票通认证弹窗:根据后端返回展示扫码或短信认证。
  • 开票结果页:展示处理中、成功、失败、需要认证,支持刷新和重试。
  • 系统配置页:维护票通数电账号、开票员、税号、默认商品编码等。

5.2 前端状态

列表项建议展示:

  • 未开票
  • 已锁定
  • 开票中
  • 待认证
  • 开票成功
  • 开票失败

前端不要自己判断票通接口细节,只消费后端聚合后的 statusactionRequiredauthOptionsmessage

6. Ktor 对外接口草案

POST /api/auth/login
GET  /api/me

GET  /api/invoice-candidates
POST /api/invoice-jobs
GET  /api/invoice-jobs/{jobId}
POST /api/invoice-jobs/{jobId}/retry

GET  /api/piaotong/accounts/{account}/auth-status
POST /api/piaotong/accounts/{account}/sms-code
POST /api/piaotong/accounts/{account}/sms-login
POST /api/piaotong/accounts/{account}/auth-qrcode
GET  /api/piaotong/accounts/{account}/auth-qrcode/{authId}
POST /api/piaotong/accounts/{account}/logout

POST /api/callbacks/piaotong/invoice

POST /api/invoice-jobs 请求示例:

{
  "candidateIds": ["A001", "A002"],
  "taxAccount": "18900000000",
  "invoiceKind": "82",
  "buyer": {
    "name": "购买方名称",
    "taxpayerNum": "XX0000000000000000"
  }
}

响应示例:

{
  "jobId": "job_20260424153000001",
  "status": "NEED_AUTH",
  "message": "当前数电账号需要短信或扫码认证",
  "authOptions": ["QRCODE", "SMS"]
}

7. 数据库表建议

7.1 invoice_job

字段 说明
id 平台开票任务 ID
company_id 公司 ID
taxpayer_num 销方纳税人识别号
invoice_req_serial_no 票通发票请求流水号,唯一
biz_batch_no 我方系统开票前写库返回批次号
tax_account 数电账号
invoice_kind 发票种类,数电普票通常为 82
status 本地状态
pt_code 票通状态码
pt_message 票通返回信息
invoice_no 发票号码
all_ele_inv_no 数电发票号码
invoice_date 开票日期
total_amount 合计金额
total_tax 合计税额
raw_request 脱敏后的票通业务请求
raw_response 脱敏后的票通响应
created_at / updated_at 时间

7.2 invoice_job_item

保存本次开票关联的我方待开票数据 ID、商品行、金额、税率、数量、税收分类编码。

7.3 tax_account

保存公司下可用数电账号、姓名、身份类型、手机号、最近认证状态。密码或密钥类数据必须加密保存,不能明文落库。

7.4 audit_log

保存登录、开票、认证、回写、回调、重试等操作审计。

8. 票通报文封装

参考现有 Java Demo,后端需要实现:

  1. 业务 JSON 使用 UTF-8 序列化。
  2. 使用票通提供的 3DES key 加密业务 JSON,得到外层 content
  3. 外层字段包含:
    • platformCode
    • signType=RSA
    • format=JSON
    • version=1.0
    • content
    • timestamp
    • serialNo
  4. 对外层字段按 key 排序,拼成 key=value&key=value,忽略 null,使用 SHA1WithRSA 和我方私钥签名。
  5. 响应先用票通公钥验签,再用 3DES 解密 content

配置项必须放在环境变量或配置中心:

PIAOTONG_BASE_URL=https://fpkj.vpiaotong.com/tp/openapi
PIAOTONG_PLATFORM_CODE=...
PIAOTONG_PLATFORM_ALIAS=...
PIAOTONG_3DES_KEY=...
PIAOTONG_PRIVATE_KEY=...
PIAOTONG_PUBLIC_KEY=...

9. 幂等与异常策略

  • invoiceReqSerialNo 是开票幂等核心。同一张发票重试必须复用,不能重新生成。
  • 调用我方“开票前写库”成功后,即使票通调用超时,也不能直接判失败,应进入 PROCESSING 并主动查询。
  • 票通返回 3999 时,不应生成新发票任务,应把原任务置为 NEED_AUTH,认证后继续用原流水号重试或查询。
  • 我方状态回写失败时,本地记录为 BIZ_SYNC_FAILED,后台补偿重试。
  • 回调接口必须验签、去重、按状态版本更新,避免乱序回调覆盖成功状态。

10. 推荐一期范围

一期先做最小闭环:

  1. 登录并获取公司 ID。
  2. 查询待开票列表。
  3. 选择数据并发起蓝字数电普票开具。
  4. 票通短信登录和扫码认证。
  5. 开票状态查询和结果回写。
  6. 票通回调接收。
  7. 开票日志和失败重试。

冲红、二维码开票、发票文件下载、微信/支付宝卡包、企业注册入驻可放到二期。