11 KiB
11 KiB
农产品收购发票开票平台系统设计
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 平台登录与公司上下文
- 用户登录 Web。
- Ktor 调用我方登录或用户信息接口,获取
userId、companyId、公司名称、纳税人识别号taxpayerNum。 - Ktor 建立平台会话/JWT,前端后续请求都带当前公司上下文。
3.2 查询待开票列表
- 前端调用
GET /api/invoice-candidates。 - Ktor 按当前
companyId调用我方“根据公司 id 查询待开票列表”接口。 - Ktor 对列表做轻量规范化:金额、税率、商品名称、收购方/销售方信息、是否可开票、已锁定状态。
- 前端展示并支持勾选。
3.3 发起开票
- 前端选中待开票数据,调用
POST /api/invoice-jobs。 - Ktor 校验当前公司票通配置和数电账号状态。
- 如果
getTaxBureauAccountAuthStatus.pt返回需要认证:operationProposed=1:提示扫码认证;operationProposed=2:展示扫码和短信两种方式;operationProposed=3:提示短信登录;operationProposed=0:继续开票。
- Ktor 调用我方“开票前写库”接口,锁定这些业务数据,并取得我方开票批次号或业务流水号。
- Ktor 生成稳定且唯一的
invoiceReqSerialNo。同一张发票重试必须复用同一个流水号,避免重复开票。 - Ktor 按票通字段组装
invoiceBlue.pt业务报文,调用票通。 - Ktor 记录票通同步返回结果:
- 成功受理或开票中:本地状态置为
PROCESSING; - 立即成功:置为
SUCCESS,回写我方系统; - 失败:置为
FAILED或NEED_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 开票结果同步
建议同时支持两条链路:
- 票通推送:实现
POST /api/callbacks/piaotong/invoice,接收票通推送发票主要信息或全票面信息。 - 主动查询:后台任务定时调用
queryInvoice.pt查询PROCESSING、NEED_AUTH_RETRYING的发票。
票通状态码映射:
| 票通 code | 含义 | 本地状态 |
|---|---|---|
0000 |
开票成功 | SUCCESS |
6666 |
未开票 | PENDING 或 PROCESSING |
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 前端状态
列表项建议展示:
未开票已锁定开票中待认证开票成功开票失败
前端不要自己判断票通接口细节,只消费后端聚合后的 status、actionRequired、authOptions、message。
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,后端需要实现:
- 业务 JSON 使用 UTF-8 序列化。
- 使用票通提供的 3DES key 加密业务 JSON,得到外层
content。 - 外层字段包含:
platformCodesignType=RSAformat=JSONversion=1.0contenttimestampserialNo
- 对外层字段按 key 排序,拼成
key=value&key=value,忽略 null,使用SHA1WithRSA和我方私钥签名。 - 响应先用票通公钥验签,再用 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. 推荐一期范围
一期先做最小闭环:
- 登录并获取公司 ID。
- 查询待开票列表。
- 选择数据并发起蓝字数电普票开具。
- 票通短信登录和扫码认证。
- 开票状态查询和结果回写。
- 票通回调接收。
- 开票日志和失败重试。
冲红、二维码开票、发票文件下载、微信/支付宝卡包、企业注册入驻可放到二期。