本文从 DGJ2.0 真实代码、共享前端组件和历史提交出发,梳理“服务站开票资料管理”的页面入口、字段校验、图片上传、OPS Center 申请、审核列表和生效资料查询。

先说明最容易误解的结论:

  1. DGJ 当前模块管理的是服务站开票资料申请,不是按销售单开具一张具体发票。
  2. DGJ 不保存该申请的业务表;申请、审核和生效资料由 OPS Center 持有。
  3. DGJ 代码中没有查到发票代码、发票号码、开票金额、红字发票、作废或税控回调能力。
  4. application/controllers/inner/Invoice.php 名称含 Invoice,但它处理的是 TMS 配送状态,不属于税票模块。
  5. 一般纳税人自动映射为数电专票 5,小规模纳税人和个体户自动映射为数电普票 6。
  6. 页面、DGJ Service 和 OPS 接口存在三层校验差异;不能把前端必填当成后端一定校验。
  7. 申请审核通过不等于某张销售单已经开票,更不等于红冲/作废已经完成。

读完后,应当能回答:

  • 用户从哪里进入,页面加载会调哪些接口。
  • 申请中每个字段来自哪里、哪些字段条件必填。
  • 图片先落本地还是直接上传 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 InvoiceTaxInvoiceSer、TaxInvoiceEnums开票主体和收票资料申请
Sa InvoiceSCM_SA_INVOICE、SaInvoiceSer销售出库/销退业务单据
TMS Invoice Controllercontrollers/inner/Invoice.php按配送单号修改销售出库配送状态

t_scm_sa_invoice 的 Invoice 是历史 ERP “出入库单”命名,不是税务发票。排查时不能因为表名含 invoice 就把它当开票记录。

2. 参与角色

角色所属系统职责
服务站资料维护人DGJ PC填写企业、银行、收票和法人资料
有查看权限用户DGJ PC查看生效资料和申请历史
有提交权限用户DGJ PC新增或重新提交资料申请
DGJ TaxInvoice ControllerDGJ权限校验、取当前 sid、统一响应
DGJ TaxInvoice ServiceDGJ字段校验、类型映射和请求组装
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
页面 Controllerapplication/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
getTaxInvoiceTaxInvoiceSer::getTaxInvoiceInvoiceProvider::find
submitTaxInvoiceSer::submitInvoiceProvider::submit
getListTaxInvoiceSer::getListInvoiceProvider::getList
getBranchBankListTaxInvoiceSer::getBranchBankListBankProvider::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/getTaxInvoicePOSTTAX_INVOICE/opscenter/openapi/orgmgr/dgj/customer/findInvoice查当前生效资料
保存申请/index.php/basedata/taxInvoice/submitPOSTTAX_INVOICE_ADD/opscenter/openapi/orgmgr/dgj/customer/createInvoice创建资料申请
历史列表/index.php/basedata/taxInvoice/getListPOSTTAX_INVOICE/opscenter/openapi/orgmgr/dgj/customer/listInvoiceApply分页查申请
远程支行搜索/index.php/basedata/taxInvoice/getBranchBankListPOST未见业务权限/opscenter/ext/sap/bank按银行名查支行
图片上传/index.php/file/ossFile/uploadFileToOSSmultipart未见业务权限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 字段类型/长度是否必填业务含义
JXCSIDcustomerCode整数/字符串系统必填服务站/客户编码
companyName同名中文 ≤25必填开票主体名称
taxNo同名字符 ≤20必填纳税人识别号
taxpayerType同名1/2/3必填一般/小规模/个体户
Service 派生invoiceType5/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可选备注
legalPersonNameextInfo.legalPersonName后端 ≤20前端必填法人姓名
legalPersonIdextInfo.legalPersonId后端 ≤20前端必填法人身份证号
legalPersonIdImageextInfo.legalPersonIdImageURL前端必填法人身份证正面
productOilModuleStatusextInfo.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 已知后端校验问题

问题代码表现风险
开票电话长度校验写错变量第二次仍检查 receiverTelinvoiceTel 可能绕过长度限制
邮箱不校验直接 ?: ''绕过前端可提交空/非法邮箱
法人字段不校验必填只检查长度上限直接调用接口可缺失
税号只校验长度不校验字符和统一社会信用代码格式非法税号进入 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/badgoods OSS 路径。
  • 未看到病毒扫描、图片解码重写或扩展名白名单。

异常分支还会先 var_dump 错误信息再输出 JSON,可能破坏响应格式并泄漏内部错误。

12.5 文件生命周期

阶段存储位置清理
PHP 临时文件系统临时目录PHP 管理
DGJ 中转文件UPLOAD_PATH/taxInvoice/YYYYMMDD上传成功后 unlink
最终文件文件中心/OSS本仓库未见生命周期规则
申请记录OPS 保存 URLOPS 负责

如果 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状态码
applyStatusNameOPS 返回状态名称
auditNote审核备注
auditor审核人
updateTime审核/更新时间
企业和银行字段展开查看申请快照
extInfo法人、身份证和成品油信息

列表采用滚动加载:

  1. 初始 page=1。
  2. 每次加载后 page++。
  3. 返回少于 10 条时认为没有更多数据。
  4. 前端直接把 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. 复制历史申请再提交

列表每行都有“复制”操作。它会:

  1. 从 extInfo 拆出法人字段。
  2. 将 productOilModuleStatus 转成字符串,缺失默认 1。
  3. 将 isInvoice、invoiceType、taxpayerType 转成字符串。
  4. 把申请快照的所有字段写入当前表单。
  5. 将 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配送单号
status10 待揽件、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 关键关联键

键作用
customerCodeDGJ sid 与 OPS 客户主体关联
applyNo定位一次资料申请
sapOpeningBankCode关联 SAP 支行
图片 URL关联文件中心对象
applyStatus判断申请进度

22. 事务、幂等和并发

22.1 DGJ 事务边界

DGJ 只做同步外部调用:

  1. 校验请求。
  2. 组装 JSON。
  3. 调 createInvoice。
  4. 根据响应返回。

不存在“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 超时后的恢复顺序

  1. 保存提交时间、sid、公司名和税号。
  2. 不立即再次点击保存。
  3. 调 getList(page=1),查最新 applyNo/createTime。
  4. 若存在刚才申请,按现有申请继续。
  5. 若列表也超时,查 Provider 日志 requestId 和 OPS 日志。
  6. 只有确认 OPS 没有创建后才重新提交。

23.3 图片失败后的恢复

  1. 查 DGJ 上传响应是否被 var_dump 破坏。
  2. 查 UPLOAD_PATH/taxInvoice/YYYYMMDD 是否有残留。
  3. 查 FileProvider 请求和 OSS 返回。
  4. 若 OSS 已有对象且 URL 可访问,可复用 URL,避免重复上传。
  5. 若对象不可访问,清理残留后重传。

24. 隐私与安全

24.1 敏感数据分级

数据风险等级最低保护要求
法人身份证号和照片极高脱敏、最小权限、加密、审计
银行账号高日志脱敏、访问审计
营业执照、开户许可证高私有对象、签名 URL 或鉴权下载
税号、公司地址电话中高避免公开日志和前端泄漏
收票人、电话、邮箱、地址高个人信息最小化
OPS 审核意见中业务权限

24.2 当前风险

风险代码证据
Provider 完整打印请求和响应BaseProvider::request
前端保存前 console.log(this.form)共享组件 handleSave
图片 URL 直接回显表单和历史详情
上传后端类型/大小限制被注释OssFile
上传异常 var_dumpOssFile 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 保存后提示超时

不要直接重提:

  1. 查列表最新申请。
  2. 以 applyNo 判断是否创建。
  3. 查 DGJ Provider 请求/响应日志。
  4. 查 OPS createInvoice 日志。
  5. 确认无记录后再提交。

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 已审核,页面仍显示旧资料

  1. 查历史列表的新申请状态和更新时间。
  2. 查 findInvoice 返回的是旧版还是新版。
  3. 确认 OPS 通过后生效时机。
  4. 查浏览器是否使用旧页面缓存。
  5. 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 提交最小验证原则

提交会创建真实申请,不应在生产随意测试。联调时应:

  1. 使用专用测试 sid。
  2. 使用脱敏测试图片。
  3. 在 OPS 约定可清理的测试申请。
  4. 记录测试申请号。
  5. 验证后清理或标记测试数据。

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/P99find/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法人姓名/证件号/图片空前后端一致
R22isInvoice 非 1/2后端拒绝,不静默转 1
R23成品油状态非 1/2/3后端拒绝

29.4 一般纳税人条件字段

编号场景断言
R24成品油截图空拒绝
R25支行 name 空拒绝
R26支行 code 空拒绝
R27name/code 不匹配拒绝或 OPS 明确校验
R28银行账号非数字/超过 40前后端一致拒绝
R29开票地址空/超过 50拒绝
R30开票电话空/非法/过长校验正确变量并拒绝
R31小规模/个体缺上述字段可按规则提交

29.5 文件上传

编号场景断言
R32JPG/PNG 小于 2MB成功返回 fileUrl
R33非图片后端拒绝
R34大于 2MB后端拒绝
R35双扩展名/无扩展名安全拒绝
R36伪造 MIME魔数/解码校验拒绝
R37DGJ 临时目录不可写JSON 错误格式完整,无 dump
R38FileProvider 超时残留文件可清理
R39OSS 私有对象合法用户可签名查看,未授权不可看

29.6 外部调用和幂等

编号场景断言
R40OPS 业务失败展示可理解错误
R41OPS HTTP 500转 ProviderError,不创建本地假成功
R42OPS 超过 5 秒但已创建查列表可识别,重试不重复
R43双击保存只创建一张申请
R44两用户并发修改版本冲突规则明确
R45getList 返回 0/9/10/11 条滚动分页正确
R46findInvoice 空、列表有待审页面能区分未生效和未提交
R47OPS 审核通过find 返回新版本
R48OPS 驳回列表红色提示,可复制重提

29.7 安全和老流程

编号场景断言
R49Provider 日志身份证、银行账号、电话等脱敏
R50浏览器控制台不打印完整表单
R51历史详情越权不能跨 sid 查看
R52TMS inner/Invoice税票改动不影响配送状态
R53销售出库/销退不因税票枚举改动受影响
R54旧发票类型 1/2历史展示兼容

30. 当前代码高风险点

风险严重度影响建议
上传后端 MIME/大小校验被注释P0任意/大文件上传恢复并增加魔数解码
Provider 记录完整敏感资料P0身份和银行信息泄露字段级脱敏
上传错误前 var_dumpP0JSON 污染和内部信息泄露删除,统一异常响应
上传和支行无业务权限P1越权调用增加 TAX_INVOICE
超时无幂等查询P1重复申请客户端流水号+查后重试
前端状态判断使用赋值P1错误响应误判改严格比较
开票电话长度检查错变量P1非法资料进入 OPS修为 invoiceTel
邮箱/法人后端校验缺失P1绕过前端补全服务端规则
taxpayerType 无白名单P1未定义映射枚举校验
BankProvider 吞业务错误返回空P2下游故障伪装成无结果保留错误码和可观测性
支行以名称为 valueP2同名/复制后 code 不一致value 使用 code
前端依赖固定远程组件版本P2组件不可用页面失效资产治理和可回滚版本
没有本地只读兜底P2OPS 故障页面完全不可用评估脱敏缓存,但需一致性设计

31. 静态代码无法证明的事项

  1. OPS 申请主表、附件表、生效资料表的物理名称和索引。
  2. OPS 完整 applyStatus 枚举和合法转换。
  3. createInvoice 是否有幂等、并发申请和重复资料限制。
  4. OPS 审核是否走 OA,以及审核人权限。
  5. 审核通过后生效资料替换的事务时点。
  6. 老生效版本是否保留、能否回滚。
  7. 支行接口的数据源、刷新频率和 code 唯一性。
  8. 文件中心对象是公开读、私有读还是永久签名 URL。
  9. 证件文件的生命周期、加密和删除机制。
  10. CENTER_API 网关鉴权、TLS、重试和熔断策略。
  11. 实际数电发票开具系统、表、API、状态和负责人。
  12. 销售退货后是否自动红冲、按什么金额口径。
  13. 具体发票邮件交付是否读取这里的 email。
  14. isInvoice=2 对后续业务的真实影响。
  15. 成品油模块状态是否由 OPS/税务系统再次核验。

32. 证据索引

32.1 DGJ 后端

结论权威证据
页面权限和四个接口application/controllers/basedata/TaxInvoice.php
字段校验和映射application/Services/TaxInvoice/TaxInvoiceSer.php
纳税人和数电类型application/KzData/Enums/TaxInvoiceEnums.php
OPS 三个资料 APIapplication/Providers/OpsCenter/Customer/InvoiceProvider.php
SAP 支行 APIapplication/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/6Git commit 169ecb48a6
模块初始引入Git commit a71d3b44ef
支行 code/name 改造Git commit 182bb9ad68
邮箱字段Git commit 2614d0e2d2
法人和 extInfoGit commits 3150e9f76a、eef1d070fc

32.3 排除证据

容易误认对象为什么排除
controllers/inner/Invoice.php请求键和行为都是 TMS 配送
SCM_SA_INVOICE销售出库/销退分表,不含税务发票要素
SCM_PU_INVOICE采购入库单
SCM_TF/STF_INVOICE调拨出入库单
Statement “红冲”注释对账单撤销口径,不等于税务红票

33. 推荐阅读和排查顺序

  1. 先读本文第 1、3、19、20 章,建立边界。
  2. 读 TaxInvoiceEnums,理解 1/2/3 → 5/6。
  3. 读 TaxInvoiceSer::submit,掌握字段和后端校验。
  4. 读前端组件,补齐动态必填、上传和复制逻辑。
  5. 读两个 OPS Provider,确认真实 API 和超时。
  6. 读 BaseProvider,理解成功判定和日志风险。
  7. 读 OssFile,理解文件中转和安全问题。
  8. 线上问题最后以 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无落库后再重提"]

记住七句话:

  1. 这是开票资料申请,不是具体发票开具。
  2. sid 在 OPS 里叫 customerCode。
  3. 申请历史和当前生效资料是两个接口、两种事实。
  4. 一般纳税人映射数电专票 5,其他映射数电普票 6。
  5. OPS 超时先查列表,不能盲目重复提交。
  6. 证件、身份证和银行账号不能完整进入日志。
  7. TMS inner/Invoice 和 t_scm_sa_invoice 都不是税务发票。

请求-日志-数据变更追踪卡

多入口请求链路

场景调用方与入口请求载荷/上下文Controller/ConsumerService/Provider汇合点最终业务事实
税票配置设置页面sid、抬头/税号、开票配置Settings::settlement_tax_invoiceSettings/TaxInvoice Servicestation tax config保存站点税票资料
开票申请PC/业务单页面来源单、购方信息、金额、票种TaxInvoice 业务 Controller税票 Service/Providertax invoice apply ID申请提交外部税务/开票系统
开票结果/文件外部回调/查询外部发票号、状态、文件 URL/IDcallback/taskProvider/TaxInvoice Serviceapply ID + external invoice no状态完成并保存合规文件引用
TMS 发货票据/inner/Invoice出库/配送单据参数inner/Invoice.php销售出库 ServiceSCM_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、operatorapplication/controllers/basedata/TaxInvoice.php -> application/Services/TaxInvoice/TaxInvoiceSer.php原资料、字段格式、唯一性和用户权限配置本地事务 fields old -> new,保留更新时间/操作人request ID + sid/config ID + changed field names敏感值日志脱敏;校验失败零写入
页面回查保存后页面/申请使用旧资料sid、config versionapplication/controllers/Settings.php当前有效配置、旧页面/新页面和缓存查询只读 不写request ID + sid + config updatedAt两套视图都回归;缓存旧时只失效配置缓存

子模块追踪:tax-apply 税务发票申请

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
建申请对可开票来源提交税票source billNos、applyNo、amount、config IDapplication/Services/TaxInvoice/TaxInvoiceSer.php来源已确认金额、已申请/已开额、税票配置和重复来源税票本地事务写申请/明细,applied old -> old+n、status pendingrequest ID + apply/source billNos + amount超可开额/配置失效零写入;多来源合计必须守恒
申请回查提交后金额或来源不符applyNo、source IDsapplication/KzData/Enums/TaxInvoiceEnums.php申请主明细、来源关系和状态查询只读 不写applyNo + source IDs + totals主单无明细按领域取消/重建,禁止手补金额

子模块追踪:tax-provider 外部开票受理

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
提交开票pending 申请调用 OPS/外部开票applyNo、external request ID、invoice dataapplication/Providers/OpsCenter/Customer/InvoiceProvider.php本地申请快照、配置版本、外部字段合同外部调用事务外;受理后本地关系事务 pending -> submittedrequest ID + applyNo + external request ID + codestimeout 先查外部是否受理;不重复生成外部发票
银行资料开票需校验银行信息sid、bank config/requestapplication/Providers/OpsCenter/Customer/BankProvider.php本地银行配置和外部校验结果查询事务外,本地业务 DB 不写request ID + sid + provider code外部 200 仍判断业务码;敏感账号仅脱敏日志

子模块追踪:tax-callback 开票结果回写

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
结果回调外部成功/失败/作废结果external request/invoice ID、applyNo、statusapplication/controllers/inner/Invoice.php -> application/Services/TaxInvoice/TaxInvoiceSer.php申请当前态、外部最终态、发票号和幂等键回调本地事务 submitted -> success/failed/void,成功保存号码request ID + external/apply ID + status重复/乱序不回退终态;未知外部号零业务写入
状态回查外部成功本地仍 submittedapplyNo、external invoice IDapplication/Providers/BaseProvider.php两端最终态、回调日志和关系查询只读;确认后幂等补回调both IDs + provider/query result只补状态/文件段,不重新提交开票

子模块追踪:tax-file 发票文件与 OSS

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
保存文件开票成功获得 PDF/OFD/图片apply/invoice ID、file/object keyapplication/controllers/file/OssFile.php发票成功态、文件归属、类型和旧引用文件服务事务外;元数据本地事务 none/pending -> readyrequest ID + invoice/file ID + size/type上传超时按 object key 查存在性;不重复开票
下载验收用户查看/下载发票invoice/file ID、sid/userapplication/controllers/file/OssFile.php文件权限、URL 有效期和校验信息下载查询只读 不写request ID + file ID + HTTP codeURL 失效刷新链接;OSS 配置与文件可访问性需环境验证

子模块追踪:tms-invoice TMS 出库 Invoice 概念隔离

环节入口/触发请求/业务键代码链路读取事实写入与字段变化日志证据异常与补偿
销售出库Inner/TMS 使用 Invoice 表示出库单sale/invoice billNo、SKU、qtyapplication/controllers/inner/Invoice.php销售订单、可出量、库存和配送关系销售本地事务写 SCM_SA_INVOICE*、数量/库存 old -> new;税票表 0 变化request ID + sale/invoice billNo + transType路由/字段命名不可误接 TaxInvoiceSer;出库失败走库存补偿
概念回归搜索/改名/接口变更URI、class/method、table constantapplication/config/tables.php税票申请表与销售出库分表、调用方集合对照查询只读;两个领域互不产生表变化request IDs + resolved tables修改 Invoice 公共路由前扫描所有调用方并分别回归开票和出库