# 行业发票平台 — 代码审查报告 **审查日期**: 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: ```json { "orderNo": "xxx", "tradeStatus": "SUCCESS", "alipayTradeNo": "yyy" } ``` 前端所有 TS 接口定义使用 snake_case: ```typescript interface OrderVO { order_no?: string; trade_status?: string; alipay_trade_no?: string; } ``` **影响**: 所有 API 返回的数据,前端无法正确读取。`res.data.data?.list` 中每个对象的属性全部为 `undefined`。 **修复方案(二选一)**: 方案 A — 后端统一配置(推荐,改动最小): ```yaml # 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 ```typescript 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 */ } } ``` **影响**: 用户编辑商品后,刷新页面数据丢失。后端从未收到更新请求。 **修复方案**: ```typescript 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 ```typescript // 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` ```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 ```java 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` 但从未被 `@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 都有: ```java 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 ```typescript const redImg = "/placeholder-invoice-red.png"; const blueImg = "/placeholder-invoice-blue.png"; ``` 这两个图片文件在项目中不存在,`` 会触发 error slot 显示占位文字。页面功能正常但视觉效果打折扣。 --- ### 11. SQL DDL 缺少外键约束 `pay_invoice_order_item.order_id` 和 `pay_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_order` 和 `pay_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.vue` 的 `handleEdit` 调用 `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 命名策略统一是阻塞性前置条件 - 缺少任何形式的自动化测试