# 农产品收购发票开票平台系统设计 ## 1. 系统定位 本平台作为“业务系统”和“票通数电发票平台”之间的开票中台: - 前端 Web:登录、查看待开票农产品收购数据、选择数据、发起开票、处理票通认证、查看开票结果。 - Ktor 后端:统一封装我方业务接口、票通接口、票通加密签名、开票状态机、结果回写、重试与审计。 - 我方业务系统:提供公司、待开票列表、开票前写库、开票结果回写等接口。 - 票通系统:提供数电账号认证、短信登录、实名认证二维码、蓝字发票开具、发票查询、结果推送等接口。 当前文档资料中,本业务主要对应票通“直接开票”场景:调用 `invoiceBlue.pt` 开具蓝字数电发票;开票结果通过票通推送或主动查询同步。票通要求业务报文使用 UTF-8,业务 JSON 先 3DES 加密,外层报文再按字段排序后用 RSA 签名。 ## 2. 总体架构 ```mermaid 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 调用我方登录或用户信息接口,获取 `userId`、`companyId`、公司名称、纳税人识别号 `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`,回写我方系统; - 失败:置为 `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 开票结果同步 建议同时支持两条链路: 1. 票通推送:实现 `POST /api/callbacks/piaotong/invoice`,接收票通推送发票主要信息或全票面信息。 2. 主动查询:后台任务定时调用 `queryInvoice.pt` 查询 `PROCESSING`、`NEED_AUTH_RETRYING` 的发票。 票通状态码映射: | 票通 code | 含义 | 本地状态 | | --- | --- | --- | | `0000` | 开票成功 | `SUCCESS` | | `6666` | 未开票 | `PENDING` 或 `PROCESSING` | | `7777` | 开票中 | `PROCESSING` | | `9999` | 开票失败 | `FAILED` | | `3999` | 需要扫码或短信认证 | `NEED_AUTH` | | `4999` | 红字确认单申请中 | 后续冲红扩展 | | `5999` | 红字确认单审核中 | 后续冲红扩展 | 结果落库后,Ktor 调用我方“写状态”接口,把成功/失败、票号、数电发票号码、失败原因、票通流水号等回写。 ## 4. 后端模块设计 建议 Ktor 后端按以下模块拆分: ```text 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 对外接口草案 ```http 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` 请求示例: ```json { "candidateIds": ["A001", "A002"], "taxAccount": "18900000000", "invoiceKind": "82", "buyer": { "name": "购买方名称", "taxpayerNum": "XX0000000000000000" } } ``` 响应示例: ```json { "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`。 配置项必须放在环境变量或配置中心: ```properties 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. 开票日志和失败重试。 冲红、二维码开票、发票文件下载、微信/支付宝卡包、企业注册入驻可放到二期。