本文从 DGJ2.0 真实代码、共享前端组件和历史提交出发,梳理“服务站开票资料管理”的页面入口、字段校验、图片上传、OPS Center 申请、审核列表和生效资料查询。
先说明最容易误解的结论:
- DGJ 当前模块管理的是服务站开票资料申请,不是按销售单开具一张具体发票。
- DGJ 不保存该申请的业务表;申请、审核和生效资料由 OPS Center 持有。
- DGJ 代码中没有查到发票代码、发票号码、开票金额、红字发票、作废或税控回调能力。
application/controllers/inner/Invoice.php名称含 Invoice,但它处理的是 TMS 配送状态,不属于税票模块。- 一般纳税人自动映射为数电专票
5,小规模纳税人和个体户自动映射为数电普票6。 - 页面、DGJ Service 和 OPS 接口存在三层校验差异;不能把前端必填当成后端一定校验。
- 申请审核通过不等于某张销售单已经开票,更不等于红冲/作废已经完成。
读完后,应当能回答:
- 用户从哪里进入,页面加载会调哪些接口。
- 申请中每个字段来自哪里、哪些字段条件必填。
- 图片先落本地还是直接上传 OSS,失败后如何定位。
- DGJ 与 OPS Center 的真实请求和响应边界。
- “申请记录”和“当前生效资料”为什么是两个查询。
- 当前代码能证明哪些状态,哪些状态只能依赖 OPS 返回标签。
- 为什么销售出库单中的
Invoice与税务发票不是同一个概念。 - 哪些问题应查 DGJ,哪些必须交给 OPS/财税系统。
1. 业务目标与范围
1.1 本模块真正解决什么
服务站作为交易主体,需要向平台提供开票所需的企业和收票信息。DGJ 提供维护入口,完成:
- 识别纳税人类型。
- 自动选择数电专票或数电普票资料类型。
- 上传营业执照、开户许可证、成品油模块截图等材料。
- 维护税号、开户支行、账号、地址、电话、邮箱和收件人。
- 提交资料给 OPS Center 审核。
- 查询历史申请及审核结果。
- 查询当前已生效资料。
- 驳回后复制历史申请,修改后重新提交。
1.2 不属于本模块的能力
| 能力 | 本仓库是否有证据 | 应归属的系统 |
|---|---|---|
| 按销售单申请开票 | 未找到 | 财税/订单/结算系统待确认 |
| 计算可开票金额 | 未找到 | 财税或结算系统 |
| 生成发票代码和号码 | 未找到 | 数电发票/税控系统 |
| 开具蓝字发票 | 未找到 | 数电发票/税控系统 |
| 红冲、红字确认单 | 未找到 | 数电发票/税控系统 |
| 发票作废 | 未找到 | 数电发票/税控系统 |
| 下载 PDF/OFD/XML | 未找到 | 财税或文件系统 |
| 邮件/短信发送具体发票 | 只有资料邮箱字段,没有发送代码 | 外部财税系统待确认 |
| TMS 出库单配送状态 | 有代码,但与税票无关 | TMS/配送模块 |
1.3 三个“Invoice”要分开
| 名称 | 代码对象 | 实际含义 |
|---|---|---|
| Tax Invoice | TaxInvoiceSer、TaxInvoiceEnums | 开票主体和收票资料申请 |
| Sa Invoice | SCM_SA_INVOICE、SaInvoiceSer | 销售出库/销退业务单据 |
| TMS Invoice Controller | controllers/inner/Invoice.php | 按配送单号修改销售出库配送状态 |
t_scm_sa_invoice 的 Invoice 是历史 ERP “出入库单”命名,不是税务发票。排查时不能因为表名含 invoice 就把它当开票记录。
2. 参与角色
| 角色 | 所属系统 | 职责 |
|---|---|---|
| 服务站资料维护人 | DGJ PC | 填写企业、银行、收票和法人资料 |
| 有查看权限用户 | DGJ PC | 查看生效资料和申请历史 |
| 有提交权限用户 | DGJ PC | 新增或重新提交资料申请 |
| DGJ TaxInvoice Controller | DGJ | 权限校验、取当前 sid、统一响应 |
| DGJ TaxInvoice Service | DGJ | 字段校验、类型映射和请求组装 |
| DGJ 文件上传接口 | DGJ | 临时落盘并转存 OSS/文件服务 |
| OPS Center | 外部系统 | 保存申请、审核、返回历史和生效资料 |
| SAP 支行接口 | OPS 外部边界 | 按银行名称返回支行编码和名称 |
| 财税/数电发票系统 | 未在本仓库定位 | 实际发票开具、红冲、作废和交付 |
3. 系统边界
flowchart LR
U["服务站用户"] --> PAGE["DGJ开票资料页面"]
PAGE --> CTRL["basedata/TaxInvoice"]
PAGE --> UP["file/OssFile"]
CTRL --> SER["TaxInvoiceSer"]
SER --> IP["OPS InvoiceProvider"]
SER --> BP["OPS BankProvider"]
IP --> OPS["OPS Center"]
BP --> SAP["OPS/SAP支行服务"]
UP --> LOCAL["DGJ临时目录"]
LOCAL --> FILE["FileProvider/OSS"]
OPS --> AUDIT["OPS审核与生效资料"]
AUDIT -. "本仓库未见直接链路" .-> TAX["财税/数电发票系统"]
3.1 数据所有权
| 数据 | DGJ 是否落业务表 | 权威来源 |
|---|---|---|
| 当前生效开票资料 | 否 | OPS findInvoice |
| 历史资料申请 | 否 | OPS listInvoiceApply |
| 审核人/审核时间/备注 | 否 | OPS 列表响应 |
| 申请状态 | 否 | OPS 列表响应 |
| 图片文件 | 临时落盘后删除 | OSS/文件中心 URL |
| 支行列表 | 否 | OPS/SAP bank 接口 |
DGJ 登录用户和 sid | 是/会话 | DGJ 登录上下文 |
| 具体发票记录 | 未找到 | 外部财税系统待确认 |
4. 页面与代码地图
4.1 页面入口
| 层级 | 文件/配置 | 作用 |
|---|---|---|
| 菜单配置 | application/config/menus_config.php | “开票资料管理”,权限 TAX_INVOICE |
| 页面 Controller | application/controllers/Settings.php::settlement_tax_invoice | 校验查看权限并渲染页面 |
| 页面模板 | application/views/settings/tax-invoice.php | 挂载 dgj-invoice-edit |
| V2 模板 | application/views_v2/settings/tax-invoice.php | 与旧模板相同 |
| 前端组件 | OSS kzuiadmin.min@2.1.6.js | 表单、校验、上传、列表和复制 |
| 成品油说明 | statics/app/invoice_explain.html | 展示远程说明图片 |
4.2 后端入口
| Controller 方法 | Service 方法 | 外部 Provider |
|---|---|---|
getTaxInvoice | TaxInvoiceSer::getTaxInvoice | InvoiceProvider::find |
submit | TaxInvoiceSer::submit | InvoiceProvider::submit |
getList | TaxInvoiceSer::getList | InvoiceProvider::getList |
getBranchBankList | TaxInvoiceSer::getBranchBankList | BankProvider::branchBankList |
OssFile::uploadFileToOSS | 文件移动和上传 | FileProvider::uploadFileToOSS |
4.3 枚举和公共层
application/KzData/Enums/TaxInvoiceEnums.php:纳税人类型和发票资料类型。application/Providers/BaseProvider.php:Guzzle 请求、5 秒超时转换、请求响应日志。application/controllers/file/OssFile.php:资料图片上传。application/config/menus_config.php:页面权限。application/controllers/tasks/AclMigrate.php:历史权限编码341/342。
5. 接口总表
| 使用方 | DGJ URL | 方法 | 权限 | 下游 URL | 用途 |
|---|---|---|---|---|---|
| 页面初始化 | /index.php/basedata/taxInvoice/getTaxInvoice | POST | TAX_INVOICE | /opscenter/openapi/orgmgr/dgj/customer/findInvoice | 查当前生效资料 |
| 保存申请 | /index.php/basedata/taxInvoice/submit | POST | TAX_INVOICE_ADD | /opscenter/openapi/orgmgr/dgj/customer/createInvoice | 创建资料申请 |
| 历史列表 | /index.php/basedata/taxInvoice/getList | POST | TAX_INVOICE | /opscenter/openapi/orgmgr/dgj/customer/listInvoiceApply | 分页查申请 |
| 远程支行搜索 | /index.php/basedata/taxInvoice/getBranchBankList | POST | 未见业务权限 | /opscenter/ext/sap/bank | 按银行名查支行 |
| 图片上传 | /index.php/file/ossFile/uploadFileToOSS | multipart | 未见业务权限 | FileProvider/OSS | 返回文件 URL |
所有 OPS 请求都是同步 POST,InvoiceProvider 使用 JSON body 和 5 秒超时。BankProvider 使用父类默认 form body 类型,具体序列化取决于 BaseProvider::$formType。
6. 页面初始化
6.1 页面加载顺序
sequenceDiagram
participant U as 用户
participant C as dgj-invoice-edit
participant DGJ as DGJ Controller
participant OPS as OPS Center
U->>C: 打开开票资料管理
C->>C: 默认isInvoice=1
C->>C: 默认productOilModuleStatus=1
par 当前生效资料
C->>DGJ: getTaxInvoice
DGJ->>OPS: findInvoice(customerCode=sid)
OPS-->>DGJ: 当前生效资料或空
DGJ-->>C: data
and 历史申请第一页
C->>DGJ: getList(page=1)
DGJ->>OPS: listInvoiceApply(size=10)
OPS-->>DGJ: 申请列表
DGJ-->>C: data
end
C->>C: 有生效资料则只读展示
6.2 当前资料回填
前端对响应做以下转换:
- 从
extInfo拆出legalPersonName、legalPersonId、legalPersonIdImage。 - 从
extInfo.productOilModuleStatus读取成品油选项,缺失时默认"1"。 - 把
isInvoice、invoiceType、taxpayerType转成字符串,匹配单选框。 - 有
data时把editable设为false,页面进入只读状态。 - 用户点击“修改”后才重新进入可编辑状态。
6.3 前端判断风险
共享组件中存在:
if (e.status = "success") {
// ...
}
这里是赋值而不是比较。它会把 status 改成 success,导致前端主要依赖 e.data 判断是否有生效资料。后端返回错误但仍进入该分支的具体表现需要浏览器回归。
7. 提交请求契约
7.1 代表性请求
{
"companyName": "示例汽车服务有限公司",
"taxNo": "9133XXXXXXXXXXXXXX",
"taxpayerType": "1",
"licenseImage": "https://file.example/license.jpg",
"permitImage": "https://file.example/permit.jpg",
"jsRefinedOilImage": "https://file.example/oil.jpg",
"certificateImage": "https://file.example/certificate.jpg",
"sapOpeningBankName": "某银行某支行",
"sapOpeningBankCode": "BANK_CODE",
"bankAccount": "6222XXXXXXXXXXXX",
"invoiceAddr": "企业注册地址",
"invoiceTel": "0571XXXXXXXX",
"isInvoice": "1",
"receivingAddr": "发票接收地址",
"receiver": "收票人",
"receiverTel": "138XXXXXXXX",
"email": "finance@example.com",
"legalPersonName": "法人姓名",
"legalPersonId": "身份证号",
"legalPersonIdImage": "https://file.example/id-card.jpg",
"productOilModuleStatus": "1",
"remark": "补充说明"
}
JXCSID 不由浏览器显式传入业务 JSON,而是由 DGJ getPageData 的登录上下文补入。Service 最终把它映射为 OPS customerCode。
7.2 字段字典
| DGJ 字段 | OPS 字段 | 类型/长度 | 是否必填 | 业务含义 |
|---|---|---|---|---|
JXCSID | customerCode | 整数/字符串 | 系统必填 | 服务站/客户编码 |
companyName | 同名 | 中文 ≤25 | 必填 | 开票主体名称 |
taxNo | 同名 | 字符 ≤20 | 必填 | 纳税人识别号 |
taxpayerType | 同名 | 1/2/3 | 必填 | 一般/小规模/个体户 |
| Service 派生 | invoiceType | 5/6 | 派生 | 数电专票/数电普票 |
licenseImage | 同名 | URL | 必填 | 营业执照 |
permitImage | 同名 | URL | 必填 | 开户许可证 |
jsRefinedOilImage | 同名 | URL | 一般纳税人必填 | 金税成品油模块截图 |
certificateImage | 同名 | URL | 可选 | 一般纳税人资格证书 |
sapOpeningBankName | 同名 | 字符串 | 一般纳税人必填 | 开户支行名称 |
sapOpeningBankCode | 同名 | 字符串 | 一般纳税人必填 | SAP 支行编码 |
bankAccount | 同名 | 数字;后端 ≤40 | 一般纳税人必填 | 银行账号 |
invoiceAddr | 同名 | 中文 ≤50 | 一般纳税人必填 | 开票/注册地址 |
invoiceTel | 同名 | 前端电话校验 | 一般纳税人必填 | 开票电话 |
isInvoice | 同名 | 1/2 | 必填 | 是/否开票意愿 |
receivingAddr | 同名 | 中文 ≤50 | 必填 | 收票地址 |
receiver | 同名 | 前端 ≤25 | 必填 | 收票人 |
receiverTel | 同名 | 后端 ≤20 | 必填 | 收票电话 |
email | 同名 | 前端 ≤50+邮箱格式 | 前端必填 | 电子发票接收邮箱 |
remark | 同名 | 前端 ≤25 | 可选 | 备注 |
legalPersonName | extInfo.legalPersonName | 后端 ≤20 | 前端必填 | 法人姓名 |
legalPersonId | extInfo.legalPersonId | 后端 ≤20 | 前端必填 | 法人身份证号 |
legalPersonIdImage | extInfo.legalPersonIdImage | URL | 前端必填 | 法人身份证正面 |
productOilModuleStatus | extInfo.productOilModuleStatus | 整数 | 必填 | 成品油模块情况 |
7.3 Service 最终出站结构
flowchart LR
PAGE["页面平铺表单"] --> VALID["DGJ字段校验"]
VALID --> MAP["类型映射与默认值"]
MAP --> ROOT["OPS根字段"]
MAP --> EXT["OPS extInfo"]
ROOT --> REQ["createInvoice JSON"]
EXT --> REQ
根字段包括企业、税号、图片、银行、收票、邮箱和备注;法人信息与成品油状态被收进 extInfo。
8. 纳税人类型与数电发票类型
8.1 枚举
taxpayerType | 纳税人类型 | 自动 invoiceType | 发票资料类型 |
|---|---|---|---|
1 | 一般纳税人 | 5 | 数电发票(增值税专用发票) |
2 | 小规模纳税人 | 6 | 数电发票(普通发票) |
3 | 个体户 | 6 | 数电发票(普通发票) |
历史枚举 1/2 仍保留为“增值税专用发票/普通发票”,但自 2024 年“电子发票”提交起,当前映射已改为 5/6。
8.2 前后端联动
flowchart TD
A["选择taxpayerType"] --> B{"是否1一般纳税人?"}
B -->|是| C["invoiceType=5"]
C --> D["增加银行/账号/开票地址电话/成品油截图校验"]
B -->|否| E["invoiceType=6"]
E --> F["删除上述条件校验"]
D --> G["提交时Service再次校验"]
F --> G
前端发票类型下拉框是禁用状态,用户不能自由选择。后端也不信任请求中的 invoiceType,而是根据 taxpayerType 重新映射。
8.3 非法枚举风险
后端只检查 taxpayerType 非空,没有 in_array(1,2,3)。非法值会访问不存在的 TAXINVOICE_TYPE_MAP 键,可能产生 Notice、空类型或 OPS 校验错误。需要补枚举白名单。
9. 条件必填和校验差异
9.1 一般纳税人额外必填
| 字段 | 前端 | DGJ 后端 |
|---|---|---|
jsRefinedOilImage | 必填 | 必填 |
sapOpeningBankName | 必填 | 必填 |
sapOpeningBankCode | 选中支行时赋值 | 必填 |
bankAccount | 必填且仅数字 | 必填、只校验长度 |
invoiceAddr | 必填 | 必填、≤50 |
invoiceTel | 电话格式 | 必填,但长度代码存在错误 |
9.2 所有类型必填
| 字段 | 前端 | DGJ 后端 |
|---|---|---|
| 公司名称、税号、纳税人类型 | 必填 | 必填 |
| 营业执照、开户许可证 | 必填 | 必填 |
| 是否开票、收票地址、收票人、收票电话 | 必填 | 必填 |
| 邮箱 | 必填+格式 | 未做必填和格式校验 |
| 法人姓名、身份证号、身份证图片 | 必填+身份证校验 | 只做姓名/证件号长度,图片可空 |
| 成品油选项 | 默认 1 且必填 | 非空 |
9.3 已知后端校验问题
| 问题 | 代码表现 | 风险 |
|---|---|---|
| 开票电话长度校验写错变量 | 第二次仍检查 receiverTel | invoiceTel 可能绕过长度限制 |
| 邮箱不校验 | 直接 ?: '' | 绕过前端可提交空/非法邮箱 |
| 法人字段不校验必填 | 只检查长度上限 | 直接调用接口可缺失 |
| 税号只校验长度 | 不校验字符和统一社会信用代码格式 | 非法税号进入 OPS |
| 成品油值只转整数 | 不校验 1/2/3 | 非法状态进入 OPS |
isInvoice 宽松归一 | 只有值 2 保留,其他都变 1 | 非法值被静默当“是” |
| 银行账号只检查长度 | 后端不校验数字 | 绕过前端可含非数字 |
9.4 校验责任
flowchart TD
INPUT["用户输入"] --> FE["共享组件校验"]
FE -->|通过| DGJ["DGJ Service校验"]
DGJ -->|通过| OPS["OPS Center校验"]
FE -->|失败| FERR["页面提示"]
DGJ -->|失败| DERR["DGJ错误响应"]
OPS -->|失败| OERR["透传OPS message"]
任何可被 HTTP 直接调用的字段,都应在 DGJ 后端完成格式和枚举校验;前端校验只改善体验,不构成安全边界。
10. 成品油模块
10.1 三个页面选项
| 值 | 页面语义 |
|---|---|
1 | 已在开票系统开通成品油模块 |
2 | 尚未开通,当地税务局要求收到油品进项发票后才能开通 |
3 | 尚未开通,近期也不准备开通 |
页面说明强调:
- 服务站只有开通成品油模块,才能向下游开具成品油发票。
- 开通前收到的成品油进项发票可抵扣,但可能因跨月影响下游开票。
10.2 数据流
sequenceDiagram
participant U as 一般纳税人
participant FE as DGJ前端
participant UP as 文件上传
participant SER as TaxInvoiceSer
participant OPS as OPS Center
U->>UP: 上传金税成品油模块截图
UP-->>FE: jsRefinedOilImage URL
U->>FE: 选择productOilModuleStatus
FE->>SER: 平铺字段
SER->>SER: 放入extInfo
SER->>OPS: createInvoice
10.3 边界
DGJ 只采集声明和截图,没有:
- 调税局验证是否真的开通。
- 根据选项阻断商品销售。
- 根据选项生成成品油发票。
- 自动判断进项发票月份。
这些能力若存在,应在财税/商品/交易系统另行定位。
11. 开户支行远程搜索
11.1 请求
{
"bankName": "中国银行杭州"
}
调用链:
sequenceDiagram
participant U as 用户
participant FE as 远程下拉框
participant DGJ as getBranchBankList
participant BP as BankProvider
participant SAP as OPS/SAP bank
U->>FE: 输入开户行关键字
FE->>DGJ: bankName
DGJ->>DGJ: 非空校验
DGJ->>BP: branchBankList
BP->>SAP: POST bankName
SAP-->>BP: code/name列表
BP-->>FE: list
U->>FE: 选择支行名称
FE->>FE: 保存name,提交前匹配code
11.2 选择和提交细节
前端下拉框的 label 和 value 都使用支行 name。保存前遍历当前 bankList,如果名称相等才写入 sapOpeningBankCode。
可能的问题:
- 用户复制历史申请后,名称存在但当前
bankList为空,code 依赖历史值是否一起复制。 - 多个支行同名时循环不
break,后匹配项会覆盖前一项 code。 - 搜索后没有选择、只保留旧名称,名称与 code 可能不一致。
- OPS 非成功时
BankProvider返回空数组而不是错误,页面只显示“无结果”。 - 超时仍由
BaseProvider抛出超时异常。
11.3 后端权限
getBranchBankList 没有显式调用 checkpurview。它可能仍受 BaseController 登录保护,但没有 TAX_INVOICE 业务权限门槛。需要补权限回归。
12. 图片上传
12.1 使用同一上传接口的材料
| 材料 | 表单字段 | 页面必填规则 |
|---|---|---|
| 营业执照 | licenseImage | 所有类型必填 |
| 开户许可证 | permitImage | 所有类型必填 |
| 成品油模块截图 | jsRefinedOilImage | 一般纳税人必填 |
| 一般纳税人资格证书 | certificateImage | 可选 |
| 法人身份证正面 | legalPersonIdImage | 前端必填 |
12.2 前端上传限制
- MIME 只允许
image/jpeg或image/png。 - 文件小于 2MB。
- 成功响应必须是
status=success。 - 成功后把
fileUrl写入表单。
12.3 后端真实流程
flowchart TD
A["multipart file"] --> B{"存在FILES且上传错误!=1?"}
B -->|否| X["返回错误"]
B -->|是| C["取原文件扩展名"]
C --> D["md5(time+name+rand)生成新名"]
D --> E["写UPLOAD_PATH/taxInvoice/YYYYMMDD"]
E --> F{"move_uploaded_file成功?"}
F -->|否| Y["上传文件错误"]
F -->|是| G{"识别为视频?"}
G -->|否| H["FileProvider上传OSS"]
G -->|是| I["直接OssClient上传refund/badgoods路径"]
H --> J["删除本地临时文件"]
I --> J
J --> K["返回fileUrl"]
12.4 前后端限制不一致
OssFile::uploadFileToOSS 中图片 MIME 和 2MB 限制已经被注释:
- 绕过前端可上传非图片。
- 绕过前端可上传大于 2MB 文件。
- 扩展名直接取原文件名。
- 视频会进入与售后不良品共用的
refund/badgoodsOSS 路径。 - 未看到病毒扫描、图片解码重写或扩展名白名单。
异常分支还会先 var_dump 错误信息再输出 JSON,可能破坏响应格式并泄漏内部错误。
12.5 文件生命周期
| 阶段 | 存储位置 | 清理 |
|---|---|---|
| PHP 临时文件 | 系统临时目录 | PHP 管理 |
| DGJ 中转文件 | UPLOAD_PATH/taxInvoice/YYYYMMDD | 上传成功后 unlink |
| 最终文件 | 文件中心/OSS | 本仓库未见生命周期规则 |
| 申请记录 | OPS 保存 URL | OPS 负责 |
如果 FileProvider 抛异常,当前代码不会执行 unlink,可能留下本地中转文件。需要定时清理和失败监控。
13. 提交到 OPS Center
13.1 调用链
sequenceDiagram
participant FE as 前端
participant C as TaxInvoice Controller
participant S as TaxInvoiceSer
participant P as InvoiceProvider
participant O as OPS Center
FE->>C: POST submit
C->>C: 校验TAX_INVOICE_ADD
C->>S: pageData含JXCSID
S->>S: 必填、长度、条件校验
S->>S: taxpayerType映射invoiceType
S->>S: 法人和成品油字段组装extInfo
S->>P: submit(data)
P->>O: POST JSON createInvoice,timeout=5s
O-->>P: code/message/data
P-->>S: code成功返回data,否则抛message
S-->>C: data
C-->>FE: 提交成功/错误
FE->>FE: 成功后reload
13.2 成功判定
Provider 只在响应存在 code 且等于 App\Providers\Code::SUCCESS 时返回 data,其他情况直接用外部 message 抛异常。
需要注意:
- HTTP 200 不代表业务成功。
- 非 JSON 或空响应经
BaseProvider::formatResult后可能变成null。 - 响应没有
message时,抛错内容可能不完整。 - DGJ 没有本地事务,因为没有本地业务落表。
13.3 日志
BaseProvider 默认记录:
- 完整 URL。
- headers。
- 完整请求
data。 - 完整响应 body。
本模块请求包含税号、银行账号、法人身份证号、电话、邮箱、地址和证件图片 URL,属于高敏感日志。必须确认日志访问权限、脱敏、留存周期和清理策略。
14. 历史申请列表
14.1 出站请求
{
"customerCode": 10001,
"page": {
"number": 1,
"size": 10
}
}
DGJ 页面只传 page,Service 从会话读取 sid,Provider 固定每页 10 条。
14.2 页面展示字段
| 字段 | 用途 |
|---|---|
applyNo | 申请单号 |
createTime | 申请时间 |
applyStatus | 状态码 |
applyStatusName | OPS 返回状态名称 |
auditNote | 审核备注 |
auditor | 审核人 |
updateTime | 审核/更新时间 |
| 企业和银行字段 | 展开查看申请快照 |
extInfo | 法人、身份证和成品油信息 |
列表采用滚动加载:
- 初始
page=1。 - 每次加载后
page++。 - 返回少于 10 条时认为没有更多数据。
- 前端直接把
e.data当数组拼接。
如果 OPS 返回分页对象而不是数组,组件会报错;这说明列表响应结构是一个重要外部契约。
15. 当前生效资料与申请历史
15.1 两种查询不能互换
| 查询 | OPS API | 语义 |
|---|---|---|
| 当前生效资料 | findInvoice | 业务当前应使用的已生效版本 |
| 申请历史 | listInvoiceApply | 每次提交快照及审核结果 |
flowchart TD
A["提交申请V1"] --> B["OPS审核"]
B -->|通过| C["V1成为生效资料"]
B -->|驳回| D["V1仅留在历史列表"]
C --> E["用户修改并提交V2"]
E --> F["OPS审核V2"]
F -->|审核中| G["当前生效仍可能是V1"]
F -->|通过| H["V2替换生效资料"]
F -->|驳回| I["生效仍是V1,V2留历史"]
上图中的“审核中是否继续保留旧版本”是常见设计,但当前 DGJ 静态代码无法证明 OPS 的确切替换时机,必须通过 OPS 契约确认。
15.2 页面只读和修改
- 查到生效资料:页面默认只读。
- 点击“修改”:仅把当前表单切为可编辑。
- 保存:不是 DGJ 本地 update,而是再次调用
createInvoice创建申请。 - 保存成功:刷新页面,重新查生效资料和历史列表。
因此“修改资料”更准确的业务语义是“基于当前资料发起新申请”。
16. 申请状态
16.1 静态代码能证明什么
前端只明确表达:
applyStatus == 1时不展示审核时间、审核人和审核备注。applyStatus == 3时申请号、时间和状态显示为红色。- 状态名称直接使用 OPS 返回的
applyStatusName。
据此可以确认 1 是尚无审核结果的状态;3 是需要醒目提示的终态。结合页面语义,3 很可能表示驳回,但 exact enum 不在本仓库,不能写成绝对事实。
16.2 证据分级状态图
stateDiagram-v2
[*] --> S1: createInvoice
S1: 1 未产生审核信息
S1 --> S2: OPS审核通过(状态码待确认)
S1 --> S3: OPS审核拒绝(3,名称待OPS确认)
S2 --> EFFECTIVE: findInvoice返回新版本
S3 --> COPY: 用户复制修改
COPY --> S1: 再次createInvoice
16.3 必须向 OPS 确认
- 完整状态码和状态名。
- 是否存在草稿、撤回、作废、审核中、审批超时。
- 通过后何时切换生效资料。
- 重复申请是否允许并行。
- 相同税号/客户编码的唯一约束。
- 审核是否走 OA。
- 驳回后是编辑原申请还是新建申请。
17. 复制历史申请再提交
列表每行都有“复制”操作。它会:
- 从
extInfo拆出法人字段。 - 将
productOilModuleStatus转成字符串,缺失默认 1。 - 将
isInvoice、invoiceType、taxpayerType转成字符串。 - 把申请快照的所有字段写入当前表单。
- 将
editable设为 true。
flowchart LR
L["历史申请快照"] --> C["点击复制"]
C --> F["覆盖当前表单"]
F --> E["进入编辑态"]
E --> U["修改字段/重新上传"]
U --> S["createInvoice新申请"]
风险:
- 复制对象可能包含
applyNo/applyStatus/auditor等只读字段;前端用Object.assign提交整个 form,Service 虽然只白名单组装出站字段,但前端调试日志会打印全部对象。 - 复制一般纳税人资料后若支行 code 缺失,需要重新搜索选择。
- 复制旧类型
invoiceType=1/2后,选择纳税人类型的 watcher 会改成5/6;若 watcher 未触发,Service 仍会按纳税人重新映射。 - 历史图片 URL 是否过期由文件系统决定。
18. MQ、回调与刷新机制
18.1 当前链路没有 MQ
在 DGJ 税票模块中没有查到:
- 提交后的 MQ producer。
- OPS 审核结果 consumer。
- OA 审批回调。
- 发票开具结果回调。
- 红冲或作废回调。
- 定时同步任务。
页面只通过同步 HTTP 查询当前状态。
18.2 用户如何看到审核结果
sequenceDiagram
participant U as 用户
participant FE as DGJ页面
participant DGJ as DGJ接口
participant OPS as OPS Center
U->>FE: 提交申请
FE->>DGJ: submit
DGJ->>OPS: createInvoice
OPS-->>DGJ: 已接收
DGJ-->>FE: 保存成功
FE->>FE: location.reload
FE->>DGJ: getTaxInvoice + getList
DGJ->>OPS: findInvoice + listInvoiceApply
OPS-->>DGJ: 当前资料 + 历史状态
DGJ-->>FE: 页面刷新展示
Note over U,OPS: 后续审核状态只能再次打开/刷新页面查询
18.3 一致性含义
这是典型的“外部系统单写、DGJ 实时读取”:
- 没有 DGJ 本地副本与 OPS 的双写一致性问题。
- OPS 不可用时,DGJ 页面也无法显示资料。
- OPS 返回错误时,没有本地旧数据兜底。
- 审核结果没有主动通知,用户看到的是下一次查询快照。
- OPS 修改数据后 DGJ 可立即读到与否,取决于 OPS 内部一致性和缓存。
19. 实际发票开具的边界
19.1 当前模块停止在哪里
flowchart LR
A["企业/税号/银行/收票资料"] --> B["OPS审核"]
B --> C["生效开票资料"]
C -. "后续系统待定位" .-> D["选择可开票业务单"]
D -.-> E["计算金额税率"]
E -.-> F["提交数电发票平台"]
F -.-> G["发票号码/PDF/OFD"]
G -.-> H["邮件/短信交付"]
G -.-> I["红冲/作废/重开"]
DGJ 本仓库只能追到 C,D-I 没有权威实现证据。
19.2 不能从资料字段推断的业务
| 已有字段 | 不能推断 |
|---|---|
invoiceType=5/6 | 已经开出专票/普票 |
isInvoice=1 | 每张销售单都会自动开票 |
email | 已向邮箱发送发票 |
receivingAddr | 已寄出纸质发票 |
productOilModuleStatus=1 | 税局已验证开通 |
| 申请审核通过 | 某张交易单具备可开票余额 |
19.3 后续业务应至少具备的对象
若未来补齐实际开票专题,应定位:
- 发票申请主表和来源单明细。
- 购方/销方快照。
- 含税金额、不含税金额、税额、税率和舍入规则。
- 蓝票、红票、红字确认单和原发票关联。
- 发票代码、号码、数电发票唯一标识。
- 开具、交付、下载、红冲、作废状态机。
- 外部请求号、幂等键和回调日志。
- 可开票金额与销售退货/退款的回算。
- PDF/OFD/XML 文件和访问权限。
这些对象在当前税票资料代码中都不存在。
20. 为什么 inner/Invoice.php 不是税票回调
20.1 真实请求字段
inner/Invoice.php 构造时要求:
| 字段 | 含义 |
|---|---|
transNo | 配送单号 |
status | 10 待揽件、20 配送中、90 已送达 |
operUid | 配送操作用户 |
completeStatus | 送达类型 |
它会按 delivery_no 查 SCM_SA_INVOICE,修改销售出库配送状态,写销售日志并发送维修厂订单状态 MQ。
20.2 实际流程
flowchart TD
A["TMS状态请求"] --> B["按delivery_no查销售出库单"]
B --> C{"status"}
C -->|10| D["billStatus=待揽件"]
C -->|20| E["billStatus=配送中"]
C -->|90| F["billStatus=已送达"]
D --> G["TransportSer更新"]
E --> G
F --> G
G --> H["销售日志"]
H --> I["sendSaOrderStatusToMq"]
20.3 排除规则
看到 Invoice 命名时先看业务键:
- 有
delivery_no/transNo/shipmentNo:销售配送。 - 有
billNo/transType=150601/150602:销售出库/销退。 - 有
taxNo/taxpayerType/invoiceType:开票资料。 - 有发票代码/号码/税额/红票号:才可能是具体税务发票。
21. 数据模型
21.1 DGJ 无本地业务表
TaxInvoiceSer 没有加载 Model,也没有执行 insert/update。application/config/tables.php 没有税票申请专用常量。
旧稿中把 t_bs_contact、销售单、收款单和对账单列为核心表,没有当前调用链证据。它们可能在更大的实际开票业务中有关,但不是本文“资料申请”的直接落表。
21.2 外部概念模型
下面是从 OPS 请求/响应字段推导的概念关系,不代表已确认的物理表:
erDiagram
CUSTOMER ||--o{ INVOICE_APPLY : submits
CUSTOMER ||--o| EFFECTIVE_INVOICE_PROFILE : owns
INVOICE_APPLY ||--o{ INVOICE_ATTACHMENT : contains
INVOICE_APPLY }o--o| AUDITOR : reviewed_by
INVOICE_APPLY }o--o| BANK_BRANCH : selects
INVOICE_APPLY {
string applyNo
string customerCode
int applyStatus
string companyName
string taxNo
int taxpayerType
int invoiceType
json extInfo
}
EFFECTIVE_INVOICE_PROFILE {
string customerCode
string companyName
string taxNo
int invoiceType
}
21.3 关键关联键
| 键 | 作用 |
|---|---|
customerCode | DGJ sid 与 OPS 客户主体关联 |
applyNo | 定位一次资料申请 |
sapOpeningBankCode | 关联 SAP 支行 |
| 图片 URL | 关联文件中心对象 |
applyStatus | 判断申请进度 |
22. 事务、幂等和并发
22.1 DGJ 事务边界
DGJ 只做同步外部调用:
- 校验请求。
- 组装 JSON。
- 调
createInvoice。 - 根据响应返回。
不存在“DGJ 本地提交成功、OPS 提交失败”的数据库双写,但存在“OPS 已创建、DGJ 响应超时”的未知结果窗口。
22.2 超时未知结果
sequenceDiagram
participant FE as 页面
participant DGJ as DGJ
participant OPS as OPS
FE->>DGJ: submit
DGJ->>OPS: createInvoice
OPS->>OPS: 申请已落库
OPS--xDGJ: 响应超过5秒
DGJ-->>FE: 请求超时
FE->>FE: 用户可能再次点击
FE->>DGJ: 重复submit
DGJ->>OPS: 第二次createInvoice
Note over OPS: 是否重复由OPS幂等/唯一约束决定
22.3 当前没有业务幂等键
出站请求没有显式:
requestId。- 客户端申请流水号。
- 版本号。
- 乐观锁。
- 重复提交 token。
BaseProvider 生成的 MD5 requestId 只用于日志,并未发给 OPS,不能作为业务幂等键。
22.4 建议幂等设计
| 动作 | 建议键 |
|---|---|
| 创建申请 | customerCode + clientApplyNo |
| 相同资料防重 | customerCode + normalizedPayloadHash |
| 更新生效版本 | customerCode + previousVersion |
| 图片上传 | 文件内容 hash + 业务类型 |
| 审核 | applyNo + targetStatus + auditVersion |
22.5 并发修改
两个用户可同时读取同一生效资料并分别提交:
flowchart TD
V1["生效版本V1"] --> A["用户A复制修改"]
V1 --> B["用户B复制修改"]
A --> A2["提交申请A"]
B --> B2["提交申请B"]
A2 --> O{"OPS并发规则?"}
B2 --> O
O --> X["允许并行/后通过覆盖/拒绝重复:待确认"]
DGJ 没有版本字段,无法阻止基于旧资料覆盖新资料。OPS 必须明确并发申请和生效版本规则。
23. 失败窗口与恢复
23.1 失败矩阵
| 失败位置 | 已发生事实 | 页面表现 | 是否可直接重试 | 正确动作 |
|---|---|---|---|---|
| 前端校验失败 | 无 | 字段提示 | 修正后可重试 | 按字段修改 |
| 图片前端拒绝 | 无上传 | MIME/大小提示 | 选合规文件 | 不绕过限制 |
| 临时文件移动失败 | 无最终 URL | 上传错误,可能混杂 dump | 可重传 | 查目录权限/磁盘 |
| FileProvider 失败 | 可能留本地文件 | 上传错误 | 先查 OSS 是否已有对象 | 清临时文件后重传 |
| 支行关键字为空 | 无 | “请填写开户行” | 输入后重试 | 正常 |
| 支行下游业务失败 | 无列表 | 页面像“无结果” | 可换词,但应查日志 | 区分真空和外部失败 |
| DGJ Service 校验失败 | 无 OPS 申请 | 后端错误 | 修正后重试 | 按错误处理 |
| OPS 明确业务失败 | OPS 未确认创建 | 透传 message | 依错误类型 | 查 OPS 校验 |
| OPS 已创建但响应超时 | 申请可能存在 | “超过5秒” | 不可盲重试 | 先查 getList |
| 提交成功后刷新失败 | OPS 已有申请 | 用户看不到新记录 | 不重提 | 重开页面/查列表 |
| OPS 审核后 DGJ 查询失败 | OPS 状态已变 | 页面不可用 | 查询可重试 | 修复依赖,不改数据 |
| 生效资料为空 | 可能无通过资料 | 页面可编辑 | 可提交新申请 | 先查历史状态 |
23.2 超时后的恢复顺序
- 保存提交时间、
sid、公司名和税号。 - 不立即再次点击保存。
- 调
getList(page=1),查最新applyNo/createTime。 - 若存在刚才申请,按现有申请继续。
- 若列表也超时,查 Provider 日志 requestId 和 OPS 日志。
- 只有确认 OPS 没有创建后才重新提交。
23.3 图片失败后的恢复
- 查 DGJ 上传响应是否被
var_dump破坏。 - 查
UPLOAD_PATH/taxInvoice/YYYYMMDD是否有残留。 - 查 FileProvider 请求和 OSS 返回。
- 若 OSS 已有对象且 URL 可访问,可复用 URL,避免重复上传。
- 若对象不可访问,清理残留后重传。
24. 隐私与安全
24.1 敏感数据分级
| 数据 | 风险等级 | 最低保护要求 |
|---|---|---|
| 法人身份证号和照片 | 极高 | 脱敏、最小权限、加密、审计 |
| 银行账号 | 高 | 日志脱敏、访问审计 |
| 营业执照、开户许可证 | 高 | 私有对象、签名 URL 或鉴权下载 |
| 税号、公司地址电话 | 中高 | 避免公开日志和前端泄漏 |
| 收票人、电话、邮箱、地址 | 高 | 个人信息最小化 |
| OPS 审核意见 | 中 | 业务权限 |
24.2 当前风险
| 风险 | 代码证据 |
|---|---|
| Provider 完整打印请求和响应 | BaseProvider::request |
前端保存前 console.log(this.form) | 共享组件 handleSave |
| 图片 URL 直接回显 | 表单和历史详情 |
| 上传后端类型/大小限制被注释 | OssFile |
上传异常 var_dump | OssFile catch |
| 支行和上传缺少业务权限 | Controller 未见 checkpurview |
| 远程说明和组件来自公共 OSS | 页面依赖外部静态资源 |
| 文件 URL 生命周期未知 | 本仓库无 ACL/TTL |
24.3 建议
- Provider 日志对税号、身份证、银行账号、电话、邮箱、地址和 URL 做字段级脱敏。
- 删除生产前端
console.log。 - 文件对象设私有读,页面通过短期签名 URL 查看。
- 上传端做 MIME、魔数、图片解码、大小、扩展名和恶意内容检查。
- 上传和支行接口补
TAX_INVOICE权限。 - 查看历史详情增加操作审计。
- 明确证件留存期限和申请作废后的删除流程。
25. 按现象排查
25.1 页面打开后一直空白
flowchart TD
A["页面空白/无资料"] --> B{"组件是否加载?"}
B -->|否| C["查kzuiadmin OSS资源/CSP/网络"]
B -->|是| D["查getTaxInvoice响应"]
D -->|401/403| E["查登录和TAX_INVOICE权限"]
D -->|5xx/超时| F["查DGJ Provider与OPS"]
D -->|success但data空| G["查getList历史申请"]
G -->|无记录| H["尚未提交,可新增"]
G -->|待审核/驳回| I["按状态处理"]
G -->|已通过但find空| J["OPS生效资料异常"]
25.2 保存后提示超时
不要直接重提:
- 查列表最新申请。
- 以
applyNo判断是否创建。 - 查 DGJ Provider 请求/响应日志。
- 查 OPS
createInvoice日志。 - 确认无记录后再提交。
25.3 一般纳税人总提示请选择开户支行
检查:
sapOpeningBankName是否来自远程下拉选择。bankList中是否仍有同名项。- 保存前是否成功写入
sapOpeningBankCode。 - 复制申请是否只带 name 没带 code。
- 同名支行是否被后一个 code 覆盖。
- OPS/SAP 搜索结果是否稳定。
25.4 邮箱页面通过但 OPS 驳回
可能原因:
- 直接调用 DGJ 绕过前端,后端未校验邮箱。
- 前端邮箱正则只支持较简单格式,合法复杂邮箱可能被误拒。
- OPS 有更严格格式或域名规则。
- 资料中邮箱为空,DGJ 仍向 OPS 发送空字符串。
应统一前后端和 OPS 的邮箱契约。
25.5 图片上传成功但历史详情打不开
flowchart TD
A["图片URL打不开"] --> B{"URL是否存在?"}
B -->|否| C["查上传响应是否写入表单"]
B -->|是| D{"HTTP状态"}
D -->|403| E["查OSS私有权限/签名过期"]
D -->|404| F["查对象key和生命周期清理"]
D -->|200非图片| G["上传类型校验缺失/对象内容异常"]
D -->|超时| H["查CDN/OSS网络"]
25.6 OPS 已审核,页面仍显示旧资料
- 查历史列表的新申请状态和更新时间。
- 查
findInvoice返回的是旧版还是新版。 - 确认 OPS 通过后生效时机。
- 查浏览器是否使用旧页面缓存。
- DGJ 本身没有资料缓存代码,若响应旧,重点查 OPS。
25.7 用户说“发票没有开出来”
先问清楚是哪种问题:
| 用户说法 | 实际问题 |
|---|---|
| 开票资料没通过 | 本文模块 |
| 没收到具体发票 | 财税交付系统 |
| 发票金额不对 | 可开票金额/税额系统 |
| 发票抬头不对 | 先查生效资料版本,再查具体发票快照 |
| 需要红冲 | 财税红票流程 |
| 出库单状态没更新 | TMS/销售配送,不是税票 |
26. 接口排查样例
路径示例用于本地或已授权环境。不要把 Cookie、Token、身份证、银行账号和真实证件 URL 写入文档或命令历史。
26.1 查当前生效资料
curl -X POST 'http://local-dgj/index.php/basedata/taxInvoice/getTaxInvoice' \
-H 'Content-Type: application/json' \
--data '{}'
sid 来自登录态,不建议由测试请求伪造。
26.2 查申请列表
curl -X POST 'http://local-dgj/index.php/basedata/taxInvoice/getList' \
-H 'Content-Type: application/json' \
--data '{"page":1}'
26.3 查支行
curl -X POST 'http://local-dgj/index.php/basedata/taxInvoice/getBranchBankList' \
-H 'Content-Type: application/json' \
--data '{"bankName":"银行关键字"}'
26.4 提交最小验证原则
提交会创建真实申请,不应在生产随意测试。联调时应:
- 使用专用测试
sid。 - 使用脱敏测试图片。
- 在 OPS 约定可清理的测试申请。
- 记录测试申请号。
- 验证后清理或标记测试数据。
27. 日志与代码检索
27.1 入口定位
rg -n "settlement_tax_invoice|TAX_INVOICE" \
application/controllers application/config
rg -n "class TaxInvoice|getTaxInvoice|getList|getBranchBankList|function submit" \
application/controllers/basedata application/Services/TaxInvoice
27.2 外部接口
rg -n "createInvoice|listInvoiceApply|findInvoice|listBankInfo|ext/sap/bank" \
application/Providers/OpsCenter
rg -n "requestSer|requestTimeOut|接口返回|调用接口" \
application/Providers/BaseProvider.php
27.3 上传
rg -n "uploadFileToOSS|taxInvoice|move_uploaded_file|FileProvider" \
application/controllers/file application/Providers/File
27.4 前端组件
线上共享组件不是仓库内源码。可下载到临时目录后只读检索:
curl -fsSL \
'https://kz-fe.oss-cn-hangzhou.aliyuncs.com/components/kzuiadmin.min@2.1.6.js' \
-o /tmp/kzuiadmin.min.js
rg -n "dgj-invoice-edit|getTaxInvoice|getBranchBankList|applyStatusName" \
/tmp/kzuiadmin.min.js
27.5 日志关键词
| 关键词 | 用途 |
|---|---|
createInvoice | 提交申请 |
listInvoiceApply | 历史列表 |
findInvoice | 生效资料 |
/opscenter/ext/sap/bank | 支行搜索 |
customerCode | 以 sid 跨系统追踪 |
applyNo | 定位单次申请 |
请求超时超过5秒 | 外部未知结果 |
uploadFileToOSS、文件名 hash | 图片上传 |
日志展示时必须脱敏,避免直接贴请求 body。
28. 监控与对账
28.1 建议指标
| 指标 | 维度 | 告警 |
|---|---|---|
createInvoice 成功率 | 环境、sid、错误码 | 5 分钟低于阈值 |
| OPS 接口 P95/P99 | find/list/create | 接近 5 秒告警 |
| 超时后重复申请数 | sid、时间窗口 | 大于 0 关注 |
| 生效资料为空率 | 有通过申请的 sid | 差异告警 |
| 支行空结果率 | 关键字、下游 code | 突增告警 |
| 上传失败率 | MIME、大小、异常类型 | 突增告警 |
| 上传本地残留量 | 日期目录 | 超阈值告警 |
| 证件 URL 403/404 | 文件类型 | 差异告警 |
| 敏感日志命中 | 字段名 | 必须为 0 或已脱敏 |
28.2 每日一致性
flowchart LR
A["OPS申请历史"] --> D["按customerCode/applyNo核对"]
B["OPS生效资料"] --> D
C["OSS证件对象"] --> D
D --> E{"通过申请=生效版本且文件可读?"}
E -->|是| F["正常"]
E -->|否| G["申请/生效差异"]
E -->|否| H["文件失效"]
E -->|否| I["资料字段缺失"]
28.3 不应做的对账
不能拿 t_scm_sa_invoice 数量与开票资料申请数比较。销售出库单和税票资料申请没有一一对应关系。
29. 回归测试矩阵
29.1 权限与页面
| 编号 | 场景 | 断言 |
|---|---|---|
| R01 | 有 TAX_INVOICE 打开页面 | 页面和生效资料正常 |
| R02 | 无 TAX_INVOICE 打开页面 | 拒绝访问 |
| R03 | 有查看无 TAX_INVOICE_ADD | 可看不可提交 |
| R04 | 无权限调用 getList/getTaxInvoice | 拒绝 |
| R05 | 无权限调用支行/上传 | 目标应拒绝,验证当前风险 |
| R06 | 共享组件加载失败 | 页面有可识别错误而非永久空白 |
29.2 纳税人和发票类型
| 编号 | 场景 | 断言 |
|---|---|---|
| R07 | 一般纳税人 | 自动 invoiceType=5 |
| R08 | 小规模纳税人 | 自动 invoiceType=6 |
| R09 | 个体户 | 自动 invoiceType=6 |
| R10 | 非法 taxpayerType | 后端拒绝,不产生 Notice/空类型 |
| R11 | 前端篡改 invoiceType | 后端按 taxpayerType 重算 |
| R12 | 旧资料 invoiceType=1/2 | 编辑提交迁移规则明确 |
29.3 必填与格式
| 编号 | 场景 | 断言 |
|---|---|---|
| R13 | 公司名空/超过 25 | 拒绝 |
| R14 | 税号空/超过 20/非法字符 | 统一拒绝 |
| R15 | 营业执照空 | 拒绝 |
| R16 | 开户许可证空 | 拒绝 |
| R17 | 收票地址空/超过 50 | 拒绝 |
| R18 | 收票人空 | 拒绝 |
| R19 | 收票电话空/非法/过长 | 前后端一致拒绝 |
| R20 | 邮箱空/非法/复杂合法邮箱 | 前后端规则一致 |
| R21 | 法人姓名/证件号/图片空 | 前后端一致 |
| R22 | isInvoice 非 1/2 | 后端拒绝,不静默转 1 |
| R23 | 成品油状态非 1/2/3 | 后端拒绝 |
29.4 一般纳税人条件字段
| 编号 | 场景 | 断言 |
|---|---|---|
| R24 | 成品油截图空 | 拒绝 |
| R25 | 支行 name 空 | 拒绝 |
| R26 | 支行 code 空 | 拒绝 |
| R27 | name/code 不匹配 | 拒绝或 OPS 明确校验 |
| R28 | 银行账号非数字/超过 40 | 前后端一致拒绝 |
| R29 | 开票地址空/超过 50 | 拒绝 |
| R30 | 开票电话空/非法/过长 | 校验正确变量并拒绝 |
| R31 | 小规模/个体缺上述字段 | 可按规则提交 |
29.5 文件上传
| 编号 | 场景 | 断言 |
|---|---|---|
| R32 | JPG/PNG 小于 2MB | 成功返回 fileUrl |
| R33 | 非图片 | 后端拒绝 |
| R34 | 大于 2MB | 后端拒绝 |
| R35 | 双扩展名/无扩展名 | 安全拒绝 |
| R36 | 伪造 MIME | 魔数/解码校验拒绝 |
| R37 | DGJ 临时目录不可写 | JSON 错误格式完整,无 dump |
| R38 | FileProvider 超时 | 残留文件可清理 |
| R39 | OSS 私有对象 | 合法用户可签名查看,未授权不可看 |
29.6 外部调用和幂等
| 编号 | 场景 | 断言 |
|---|---|---|
| R40 | OPS 业务失败 | 展示可理解错误 |
| R41 | OPS HTTP 500 | 转 ProviderError,不创建本地假成功 |
| R42 | OPS 超过 5 秒但已创建 | 查列表可识别,重试不重复 |
| R43 | 双击保存 | 只创建一张申请 |
| R44 | 两用户并发修改 | 版本冲突规则明确 |
| R45 | getList 返回 0/9/10/11 条 | 滚动分页正确 |
| R46 | findInvoice 空、列表有待审 | 页面能区分未生效和未提交 |
| R47 | OPS 审核通过 | find 返回新版本 |
| R48 | OPS 驳回 | 列表红色提示,可复制重提 |
29.7 安全和老流程
| 编号 | 场景 | 断言 |
|---|---|---|
| R49 | Provider 日志 | 身份证、银行账号、电话等脱敏 |
| R50 | 浏览器控制台 | 不打印完整表单 |
| R51 | 历史详情越权 | 不能跨 sid 查看 |
| R52 | TMS inner/Invoice | 税票改动不影响配送状态 |
| R53 | 销售出库/销退 | 不因税票枚举改动受影响 |
| R54 | 旧发票类型 1/2 | 历史展示兼容 |
30. 当前代码高风险点
| 风险 | 严重度 | 影响 | 建议 |
|---|---|---|---|
| 上传后端 MIME/大小校验被注释 | P0 | 任意/大文件上传 | 恢复并增加魔数解码 |
| Provider 记录完整敏感资料 | P0 | 身份和银行信息泄露 | 字段级脱敏 |
上传错误前 var_dump | P0 | JSON 污染和内部信息泄露 | 删除,统一异常响应 |
| 上传和支行无业务权限 | P1 | 越权调用 | 增加 TAX_INVOICE |
| 超时无幂等查询 | P1 | 重复申请 | 客户端流水号+查后重试 |
| 前端状态判断使用赋值 | P1 | 错误响应误判 | 改严格比较 |
| 开票电话长度检查错变量 | P1 | 非法资料进入 OPS | 修为 invoiceTel |
| 邮箱/法人后端校验缺失 | P1 | 绕过前端 | 补全服务端规则 |
| taxpayerType 无白名单 | P1 | 未定义映射 | 枚举校验 |
| BankProvider 吞业务错误返回空 | P2 | 下游故障伪装成无结果 | 保留错误码和可观测性 |
| 支行以名称为 value | P2 | 同名/复制后 code 不一致 | value 使用 code |
| 前端依赖固定远程组件版本 | P2 | 组件不可用页面失效 | 资产治理和可回滚版本 |
| 没有本地只读兜底 | P2 | OPS 故障页面完全不可用 | 评估脱敏缓存,但需一致性设计 |
31. 静态代码无法证明的事项
- OPS 申请主表、附件表、生效资料表的物理名称和索引。
- OPS 完整
applyStatus枚举和合法转换。 createInvoice是否有幂等、并发申请和重复资料限制。- OPS 审核是否走 OA,以及审核人权限。
- 审核通过后生效资料替换的事务时点。
- 老生效版本是否保留、能否回滚。
- 支行接口的数据源、刷新频率和 code 唯一性。
- 文件中心对象是公开读、私有读还是永久签名 URL。
- 证件文件的生命周期、加密和删除机制。
CENTER_API网关鉴权、TLS、重试和熔断策略。- 实际数电发票开具系统、表、API、状态和负责人。
- 销售退货后是否自动红冲、按什么金额口径。
- 具体发票邮件交付是否读取这里的
email。 isInvoice=2对后续业务的真实影响。- 成品油模块状态是否由 OPS/税务系统再次核验。
32. 证据索引
32.1 DGJ 后端
| 结论 | 权威证据 |
|---|---|
| 页面权限和四个接口 | application/controllers/basedata/TaxInvoice.php |
| 字段校验和映射 | application/Services/TaxInvoice/TaxInvoiceSer.php |
| 纳税人和数电类型 | application/KzData/Enums/TaxInvoiceEnums.php |
| OPS 三个资料 API | application/Providers/OpsCenter/Customer/InvoiceProvider.php |
| SAP 支行 API | application/Providers/OpsCenter/Customer/BankProvider.php |
| 5 秒超时和完整日志 | application/Providers/BaseProvider.php |
| 文件上传 | application/controllers/file/OssFile.php |
| 菜单入口 | application/config/menus_config.php |
| 页面渲染 | application/controllers/Settings.php |
32.2 前端和历史
| 结论 | 证据 |
|---|---|
| 页面挂载组件 | application/views/settings/tax-invoice.php |
| 前端字段规则和接口 | OSS kzuiadmin.min@2.1.6.js 的 dgj-invoice-edit |
| 成品油页面说明 | statics/app/invoice_explain.html |
| 2024 切换数电类型 5/6 | Git commit 169ecb48a6 |
| 模块初始引入 | Git commit a71d3b44ef |
| 支行 code/name 改造 | Git commit 182bb9ad68 |
| 邮箱字段 | Git commit 2614d0e2d2 |
| 法人和 extInfo | Git commits 3150e9f76a、eef1d070fc |
32.3 排除证据
| 容易误认对象 | 为什么排除 |
|---|---|
controllers/inner/Invoice.php | 请求键和行为都是 TMS 配送 |
SCM_SA_INVOICE | 销售出库/销退分表,不含税务发票要素 |
SCM_PU_INVOICE | 采购入库单 |
SCM_TF/STF_INVOICE | 调拨出入库单 |
| Statement “红冲”注释 | 对账单撤销口径,不等于税务红票 |
33. 推荐阅读和排查顺序
- 先读本文第 1、3、19、20 章,建立边界。
- 读
TaxInvoiceEnums,理解1/2/3 → 5/6。 - 读
TaxInvoiceSer::submit,掌握字段和后端校验。 - 读前端组件,补齐动态必填、上传和复制逻辑。
- 读两个 OPS Provider,确认真实 API 和超时。
- 读
BaseProvider,理解成功判定和日志风险。 - 读
OssFile,理解文件中转和安全问题。 - 线上问题最后以
sid/customerCode + applyNo去 OPS 查状态。
34. 一页式业务与排查 SOP
flowchart TD
START["输入:sid、公司名、税号或applyNo"] --> Q1{"问题属于哪类?"}
Q1 -->|资料填写/提交| A["查DGJ页面、校验和createInvoice"]
Q1 -->|审核/生效| B["查OPS listInvoiceApply/findInvoice"]
Q1 -->|图片| C["查上传临时目录、FileProvider、OSS"]
Q1 -->|支行| D["查bankName、name/code、OPS/SAP"]
Q1 -->|具体发票/红冲| E["转财税系统,不查TMS Invoice"]
A --> F{"提交是否超时?"}
F -->|是| G["先查历史列表,确认是否已创建"]
F -->|否| H["按错误字段修正"]
B --> I{"历史状态和生效资料一致?"}
I -->|否| J["查OPS生效时点/状态规则"]
I -->|是| K["资料链路完成"]
C --> L{"对象可访问?"}
L -->|否| M["查403/404/残留/生命周期"]
L -->|是| K
D --> N{"返回支行且code匹配?"}
N -->|否| O["区分真空结果与下游失败"]
N -->|是| K
G --> P{"找到刚提交申请?"}
P -->|是| K
P -->|否| Q["确认OPS无落库后再重提"]
记住七句话:
- 这是开票资料申请,不是具体发票开具。
sid在 OPS 里叫customerCode。- 申请历史和当前生效资料是两个接口、两种事实。
- 一般纳税人映射数电专票 5,其他映射数电普票 6。
- OPS 超时先查列表,不能盲目重复提交。
- 证件、身份证和银行账号不能完整进入日志。
- TMS
inner/Invoice和t_scm_sa_invoice都不是税务发票。
请求-日志-数据变更追踪卡
多入口请求链路
| 场景 | 调用方与入口 | 请求载荷/上下文 | Controller/Consumer | Service/Provider | 汇合点 | 最终业务事实 |
|---|---|---|---|---|---|---|
| 税票配置 | 设置页面 | sid、抬头/税号、开票配置 | Settings::settlement_tax_invoice | Settings/TaxInvoice Service | station tax config | 保存站点税票资料 |
| 开票申请 | PC/业务单页面 | 来源单、购方信息、金额、票种 | TaxInvoice 业务 Controller | 税票 Service/Provider | tax invoice apply ID | 申请提交外部税务/开票系统 |
| 开票结果/文件 | 外部回调/查询 | 外部发票号、状态、文件 URL/ID | callback/task | Provider/TaxInvoice Service | apply ID + external invoice no | 状态完成并保存合规文件引用 |
| TMS 发货票据 | /inner/Invoice | 出库/配送单据参数 | inner/Invoice.php | 销售出库 Service | SCM_SA_INVOICE | 销售出库单,不是税务发票 |
日志证据矩阵
| 链路段 | 日志来源 | 可检索锚点 | 成功信号 | 失败信号 | 与下一段关联方式 | | --- | --- | --- | --- | --- | --- | --- | | 配置/申请 | Settings/Tax Controller | request_id、sid、申请/来源单号 | 配置或申请 commit | 税号/金额/来源校验失败 | apply ID 进入 Provider | | 外部开票 | BaseProvider/税票 Provider | apply ID、external request ID、HTTP/业务码 | 对方受理并返回外部号 | timeout、业务拒绝、重复申请 | 外部号用于回调/查询 | | 结果回写 | Callback/Task Service | 外部发票号、apply ID、状态 | 状态推进且文件可访问 | 找不到申请、旧状态回退、文件失效 | apply ID 回查税票业务表 | | 概念核对 | inner/Invoice/销售出库日志 | 出库单号、SCM_SA_INVOICE | 只处理物流出库语义 | 被误当税票数据查询 | 来源单类型和表名区分 |
环节数据变更台账
| 步骤 | 代码位置 | 事务 | 读取事实 | 写入表/缓存/MQ | 字段或数量变化 | 回查证据 |
|---|---|---|---|---|---|---|
| 保存配置 | Settings/Tax Service | 配置事务 | 站点原税票资料 | TAX_INVOICE 配置相关表 | 字段 old -> new,敏感信息日志脱敏 | sid、更新时间、操作人 |
| 建申请 | TaxInvoice Service | 申请事务 | 可开票来源金额、已开金额 | 税票申请/明细 | apply amount +n;status pending | 来源单、申请单、金额合计 |
| 外部受理 | Provider | 外部边界 | 申请快照 | 外部开票系统、本地外部号关系 | 本地待处理 -> submitted;超时先查外部再重试 | external request/invoice ID |
| 结果/文件 | Callback/Query Service | 回调事务 | 当前状态、外部最终结果 | 税票状态、OssFile 引用 | submitted -> success/failed;成功保存号码/文件 | 外部发票号、文件可访问性 |
| 销售出库 | inner/Invoice.php | 销售事务 | 出库业务 | SCM_SA_INVOICE* | 仅出库数量/状态变化,税票表 0 变化 | 出库单与税票申请无错误关联 |
子模块追踪:tax-config 税票资料配置
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存资料 | 站点维护抬头、税号、银行地址等 | sid、config ID、operator | application/controllers/basedata/TaxInvoice.php -> application/Services/TaxInvoice/TaxInvoiceSer.php | 原资料、字段格式、唯一性和用户权限 | 配置本地事务 fields old -> new,保留更新时间/操作人 | request ID + sid/config ID + changed field names | 敏感值日志脱敏;校验失败零写入 |
| 页面回查 | 保存后页面/申请使用旧资料 | sid、config version | application/controllers/Settings.php | 当前有效配置、旧页面/新页面和缓存 | 查询只读 不写 | request ID + sid + config updatedAt | 两套视图都回归;缓存旧时只失效配置缓存 |
子模块追踪:tax-apply 税务发票申请
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 建申请 | 对可开票来源提交税票 | source billNos、applyNo、amount、config ID | application/Services/TaxInvoice/TaxInvoiceSer.php | 来源已确认金额、已申请/已开额、税票配置和重复来源 | 税票本地事务写申请/明细,applied old -> old+n、status pending | request ID + apply/source billNos + amount | 超可开额/配置失效零写入;多来源合计必须守恒 |
| 申请回查 | 提交后金额或来源不符 | applyNo、source IDs | application/KzData/Enums/TaxInvoiceEnums.php | 申请主明细、来源关系和状态 | 查询只读 不写 | applyNo + source IDs + totals | 主单无明细按领域取消/重建,禁止手补金额 |
子模块追踪:tax-provider 外部开票受理
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 提交开票 | pending 申请调用 OPS/外部开票 | applyNo、external request ID、invoice data | application/Providers/OpsCenter/Customer/InvoiceProvider.php | 本地申请快照、配置版本、外部字段合同 | 外部调用事务外;受理后本地关系事务 pending -> submitted | request ID + applyNo + external request ID + codes | timeout 先查外部是否受理;不重复生成外部发票 |
| 银行资料 | 开票需校验银行信息 | sid、bank config/request | application/Providers/OpsCenter/Customer/BankProvider.php | 本地银行配置和外部校验结果 | 查询事务外,本地业务 DB 不写 | request ID + sid + provider code | 外部 200 仍判断业务码;敏感账号仅脱敏日志 |
子模块追踪:tax-callback 开票结果回写
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 结果回调 | 外部成功/失败/作废结果 | external request/invoice ID、applyNo、status | application/controllers/inner/Invoice.php -> application/Services/TaxInvoice/TaxInvoiceSer.php | 申请当前态、外部最终态、发票号和幂等键 | 回调本地事务 submitted -> success/failed/void,成功保存号码 | request ID + external/apply ID + status | 重复/乱序不回退终态;未知外部号零业务写入 |
| 状态回查 | 外部成功本地仍 submitted | applyNo、external invoice ID | application/Providers/BaseProvider.php | 两端最终态、回调日志和关系 | 查询只读;确认后幂等补回调 | both IDs + provider/query result | 只补状态/文件段,不重新提交开票 |
子模块追踪:tax-file 发票文件与 OSS
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 保存文件 | 开票成功获得 PDF/OFD/图片 | apply/invoice ID、file/object key | application/controllers/file/OssFile.php | 发票成功态、文件归属、类型和旧引用 | 文件服务事务外;元数据本地事务 none/pending -> ready | request ID + invoice/file ID + size/type | 上传超时按 object key 查存在性;不重复开票 |
| 下载验收 | 用户查看/下载发票 | invoice/file ID、sid/user | application/controllers/file/OssFile.php | 文件权限、URL 有效期和校验信息 | 下载查询只读 不写 | request ID + file ID + HTTP code | URL 失效刷新链接;OSS 配置与文件可访问性需环境验证 |
子模块追踪:tms-invoice TMS 出库 Invoice 概念隔离
| 环节 | 入口/触发 | 请求/业务键 | 代码链路 | 读取事实 | 写入与字段变化 | 日志证据 | 异常与补偿 |
|---|---|---|---|---|---|---|---|
| 销售出库 | Inner/TMS 使用 Invoice 表示出库单 | sale/invoice billNo、SKU、qty | application/controllers/inner/Invoice.php | 销售订单、可出量、库存和配送关系 | 销售本地事务写 SCM_SA_INVOICE*、数量/库存 old -> new;税票表 0 变化 | request ID + sale/invoice billNo + transType | 路由/字段命名不可误接 TaxInvoiceSer;出库失败走库存补偿 |
| 概念回归 | 搜索/改名/接口变更 | URI、class/method、table constant | application/config/tables.php | 税票申请表与销售出库分表、调用方集合 | 对照查询只读;两个领域互不产生表变化 | request IDs + resolved tables | 修改 Invoice 公共路由前扫描所有调用方并分别回归开票和出库 |