code-review-invoice-platform.md 11 KB

行业发票平台 — 代码审查报告

审查日期: 2026-07-08
分支: fapiao
审查范围: 未提交的全部 83 个文件(SQL ×2、Java ×60、前端 ×21)


概览

类别 文件数 说明
SQL DDL 1 (012_invoice_tables.sql) 10 张表,结构完整
SQL DML 1 (013_invoice_menu.sql) 1 父菜单 + 8 子菜单
Java Entity 10 MyBatis-Plus 实体,继承基类正确
Java Mapper 10 空接口,未接入 DB
Java DTO/VO 20 含校验注解
Java Controller 8 REST API,路由完整
Java Service 8 全部为内存 Mock 实现
Java Enum 4 含中文 label
前端 API TS 8 axios 封装,类型定义完整
前端 Vue 页面 7 Element Plus
前端 Vue 组件 4 Dialog 弹窗类
前端 Mock TS 1 与后端 mock 数据镜像
核心修改 1 (TenantInnerInterceptor) 新增 2 张条件表

🔴 严重问题(必须修复)

问题 1:JSON 字段命名前后端全量不匹配

位置: 全部前端 API 文件 + 全部后端 Controller 返回值

后端 Java 默认序列化为 camelCase:

{ "orderNo": "xxx", "tradeStatus": "SUCCESS", "alipayTradeNo": "yyy" }

前端所有 TS 接口定义使用 snake_case:

interface OrderVO {
  order_no?: string;
  trade_status?: string;
  alipay_trade_no?: string;
}

影响: 所有 API 返回的数据,前端无法正确读取。res.data.data?.list 中每个对象的属性全部为 undefined

修复方案(二选一):

方案 A — 后端统一配置(推荐,改动最小):

# application.yml
spring:
  jackson:
    property-naming-strategy: SNAKE_CASE

方案 B — 前端接口定义全部改为 camelCase,同步修改所有 .vue 文件中的模板绑定。


问题 2:商品编辑不调用后端 API

文件: frontend/src/views/module_payment/invoice/goods/index.vue
行号: 69-77

async function handleEdit(row: GoodsVO) {
  try {
    const { value } = await ElMessageBox.prompt('编辑商品名称', '编辑', { inputValue: row.name });
    if (value) {
      row.name = value;  // ← 直接改了本地变量,从未调 updateGoods API
      ElMessage.success('编辑成功');
    }
  } catch { /* cancelled */ }
}

影响: 用户编辑商品后,刷新页面数据丢失。后端从未收到更新请求。

修复方案:

async function handleEdit(row: GoodsVO) {
  try {
    const { value } = await ElMessageBox.prompt('编辑商品名称', '编辑', { inputValue: row.name });
    if (value) {
      await updateGoods(row.id!, { name: value, category_id: row.category_id });
      ElMessage.success('编辑成功');
      fetchGoods();
    }
  } catch { /* cancelled */ }
}

问题 3:员工表单提交字段名与后端不一致

文件: frontend/src/views/module_payment/invoice/employee/components/EmployeeFormDialog.vue
行号: 62-64

// form 使用 camelCase
const form = reactive({ role: '', name: '', phone: '', allowSelectSupplier: false });
// 直接 spread 提交
await createEmployee({ ...form });

如果执行了问题 1 的修复(后端改为 snake_case),则 allowSelectSupplier 需要改为 allow_select_supplier

修复方案: 统一命名策略后,确保前端提交的字段名与后端 @RequestBody 期望一致。使用 Jackson snake_case 时,前端提交的 JSON key 也必须用 snake_case。


🟡 中等问题(应该修复)

问题 4:TaskQueryDTO 分页字段类型错误

文件: java/src/main/java/com/payment/platform/module/payment/invoice/task/dto/TaskQueryDTO.java

private String pageNo = "1";    // ← 应该是 Integer
private String pageSize = "10"; // ← 应该是 Integer

TaskService.list() 中手动做了 Integer.parseInt()。其他所有模块的 QueryDTO 都使用 Integer 类型。

修复方案: 改为 Integer,与其他模块保持一致,删除 parseInt 调用。


问题 5:TaxService.currentMode 非线程安全

文件: java/src/main/java/com/payment/platform/module/payment/invoice/tax/service/TaxService.java
行号: 11

private String currentMode = "PERSONAL";  // 实例字段,多用户共享

Spring Service 默认是 singleton,所有请求共享同一个实例。用户 A 切换为 ENTERPRISE 后,用户 B 读到的也是 ENTERPRISE。

影响: 多用户并发时,缴税模式会互相覆盖。

修复方案: 后续接入数据库时,将 tax_mode 存到 pay_invoice_company_config 表(DDL 中已有 tax_method 字段),按 enterprise_id 隔离。


问题 6:全部 Service 是 Mock 实现,10 个 Mapper 未被使用

文件: 全部 *Service.java + 全部 *Mapper.java

  • 所有数据存储在 ConcurrentHashMap
  • 10 个 @Mapper 接口定义了 BaseMapper<T> 但从未被 @Autowired 注入到 Service
  • Entity 上的 @TableName 注解从未被验证过是否能正确映射到数据库表

说明: 原型阶段可以接受 mock 实现,但以下工作需要在接入真实 DB 前完成:

  • 验证 Entity 字段名与数据库列名的映射(MyBatis-Plus 默认 camelCase → snake_case)
  • 验证 @TableName 对应的表确实存在于数据库中
  • Service 注入 Mapper 替换 ConcurrentHashMap

问题 7:OrderService.initMockData() 金额逻辑需要注释

文件: java/src/main/java/com/payment/platform/module/payment/invoice/order/service/OrderService.java
行号: 27-92

Mock 数据中:

  • goodsAmount = 9.98 + totalTaxPaid = 0.02 = orderTotalAmount = 10.00
  • totalTaxPaid = 0.02 = personalIncomeTax = 0.02 + 增值税 0 + 附加税 0 ✓
  • taxAmount = 10.00(含税金额),invoicePreTaxAmount = 9.90(不含税), invoiceTaxAmount = 0.10(发票税额)

发票维度的计算(9.90 + 0.10 = 10.00)与订单维度(9.98 + 0.02 = 10.00)对不上——货物金额在发票上显示为 9.90,但在订单计税时显示为 9.98。这可能反映了真实业务中发票金额与计税基础的差异(个税不进发票),但缺乏注释说明。

修复方案: 在 initMockData() 方法上加注释,解释金额之间的业务关系。


问题 8:所有 Controller 定义 currentUser() 但未调用

文件: 全部 8 个 Controller

每个 Controller 都有:

private LoginUser currentUser() {
    Authentication a = SecurityContextHolder.getContext().getAuthentication();
    if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
    return null;
}

但没有任何一个方法调用它。当前缺少:

  • 租户 ID 注入(tenant_id 字段未被填充到 entity)
  • 企业 ID 注入(enterprise_id 字段未被填充到 entity)
  • 操作权限校验

修复方案: 在 create/update 方法中调用 currentUser() 获取 tenantId,填充到 entity;或者通过 MyBatis-Plus 的 @TableField(fill = FieldFill.INSERT) + MetaObjectHandler 自动填充(base entity 已有此注解)。


🟢 建议改进

9. 前端 mock/ 目录未被激活

文件: frontend/src/mock/module_payment/invoice/index.ts

Mock 数据函数定义完整,但没有发现 vite-plugin-mock 或 MSW 的配置来拦截请求。当前前端直接调用后端 API(后端返回 mock 数据),这个文件是死代码。

建议: 要么接入 mock 插件让前端可独立开发,要么删除此文件避免误导。


10. 发票占位图片不存在

文件: frontend/src/views/module_payment/invoice/order/components/OrderInvoiceDialog.vue
行号: 32-33

const redImg = "/placeholder-invoice-red.png";
const blueImg = "/placeholder-invoice-blue.png";

这两个图片文件在项目中不存在,<el-image> 会触发 error slot 显示占位文字。页面功能正常但视觉效果打折扣。


11. SQL DDL 缺少外键约束

pay_invoice_order_item.order_idpay_invoice_tax_detail.order_id 没有 FOREIGN KEY 约束。虽然应用层可保证一致性,但缺少数据库层面的引用完整性保护。


12. Entity 字段过多

OrderEntity 包含 25+ 个业务字段(交易信息 + 发票信息 + 税务信息混在一张表)。后续建议拆分:

pay_invoice_order          → 交易基础信息
pay_invoice_order_tax      → 各税种金额(已有 tax_detail,但主表仍有冗余汇总字段)

当前设计是一次性把支付宝回调的所有字段落了宽表,短期可用,长期维护成本高。


13. TenantInnerInterceptor 修改审查

文件: java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java
修改内容: CONDITIONAL_TABLES 集合中新增了 pay_invoice_orderpay_invoice_task

审查结论: ✅ 修改逻辑正确。

  • 已认证用户访问这两张表 → 应用租户过滤 ✓
  • 定时任务/导出回调等无认证场景访问 → 跳过租户过滤 ✓

需要注意: 以下发票表未加入 CONDITIONAL_TABLES,意味着它们始终应用租户过滤(无认证场景会查不到数据):

  • pay_invoice_supplier
  • pay_invoice_employee
  • pay_invoice_goods
  • pay_invoice_goods_category
  • pay_invoice_company_config
  • pay_invoice_transfer_account
  • pay_invoice_order_item
  • pay_invoice_tax_detail

如果这些表只由登录用户在 UI 中操作,当前配置是正确的。如果后续有定时任务需要扫这些表,需要把对应的表名加入 CONDITIONAL_TABLES。


优先级执行清单

优先级 问题 修复点
🔴 P0 JSON 命名不匹配 后端加 jackson.property-naming-strategy: SNAKE_CASE,或前端全部改用 camelCase
🔴 P0 商品编辑不调 API goods/index.vuehandleEdit 调用 updateGoods
🔴 P0 表单提交字段统一 确认前端提交 JSON key 与后端匹配
🟡 P1 TaskQueryDTO 类型 pageNo/pageSize 改为 Integer
🟡 P1 TaxService 线程安全 currentMode 按 enterprise 隔离或存 DB
🟡 P1 Mock 金额加注释 解释 9.98 vs 9.90 的业务含义
🟡 P1 Controller 注入租户/企业 ID create/update 时填充 tenantId、enterpriseId
🟢 P2 Mapper 接入 DB Service 注入 Mapper 替换 ConcurrentHashMap
🟢 P2 发票占位图 上传实际图片或替换为 CSS 占位
🟢 P2 Mock 文件 接入 vite-plugin-mock 或删除
🟢 P2 SQL 外键 加 FOREIGN KEY 约束

架构评价

做得好的地方:

  • 模块划分清晰:order / supplier / employee / goods / company / task / account / tax 各司其职
  • 代码风格一致:所有 Controller 同一模式,所有 Service 同一结构
  • 枚举值完备且带中文 label
  • Entity 继承基类正确:有 enterprise_id 的用 PaymentEnterpriseBaseEntity,只有 tenant_id 的用 PaymentTenantBaseEntity
  • TenantInnerInterceptor 的修改最小化且逻辑正确
  • 前端 Vue 组件化合理,Dialog 组件可复用

需要关注的风险点:

  • Mock → 真实 DB 的切换工作量被低估(需验证所有 @TableName、字段映射、分页查询、事务)
  • JSON 命名策略统一是阻塞性前置条件
  • 缺少任何形式的自动化测试