审查日期: 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 张条件表 |
位置: 全部前端 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 文件中的模板绑定。
文件: 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 */ }
}
文件: 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。
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 调用。
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 隔离。
文件: 全部 *Service.java + 全部 *Mapper.java
ConcurrentHashMap 中@Mapper 接口定义了 BaseMapper<T> 但从未被 @Autowired 注入到 Service@TableName 注解从未被验证过是否能正确映射到数据库表说明: 原型阶段可以接受 mock 实现,但以下工作需要在接入真实 DB 前完成:
@TableName 对应的表确实存在于数据库中ConcurrentHashMapOrderService.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() 方法上加注释,解释金额之间的业务关系。
currentUser() 但未调用文件: 全部 8 个 Controller
每个 Controller 都有:
private LoginUser currentUser() {
Authentication a = SecurityContextHolder.getContext().getAuthentication();
if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
return null;
}
但没有任何一个方法调用它。当前缺少:
tenant_id 字段未被填充到 entity)enterprise_id 字段未被填充到 entity)修复方案: 在 create/update 方法中调用 currentUser() 获取 tenantId,填充到 entity;或者通过 MyBatis-Plus 的 @TableField(fill = FieldFill.INSERT) + MetaObjectHandler 自动填充(base entity 已有此注解)。
mock/ 目录未被激活文件: frontend/src/mock/module_payment/invoice/index.ts
Mock 数据函数定义完整,但没有发现 vite-plugin-mock 或 MSW 的配置来拦截请求。当前前端直接调用后端 API(后端返回 mock 数据),这个文件是死代码。
建议: 要么接入 mock 插件让前端可独立开发,要么删除此文件避免误导。
文件: 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 显示占位文字。页面功能正常但视觉效果打折扣。
pay_invoice_order_item.order_id 和 pay_invoice_tax_detail.order_id 没有 FOREIGN KEY 约束。虽然应用层可保证一致性,但缺少数据库层面的引用完整性保护。
OrderEntity 包含 25+ 个业务字段(交易信息 + 发票信息 + 税务信息混在一张表)。后续建议拆分:
pay_invoice_order → 交易基础信息
pay_invoice_order_tax → 各税种金额(已有 tax_detail,但主表仍有冗余汇总字段)
当前设计是一次性把支付宝回调的所有字段落了宽表,短期可用,长期维护成本高。
文件: java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java
修改内容: CONDITIONAL_TABLES 集合中新增了 pay_invoice_order 和 pay_invoice_task
审查结论: ✅ 修改逻辑正确。
需要注意: 以下发票表未加入 CONDITIONAL_TABLES,意味着它们始终应用租户过滤(无认证场景会查不到数据):
pay_invoice_supplierpay_invoice_employeepay_invoice_goodspay_invoice_goods_categorypay_invoice_company_configpay_invoice_transfer_accountpay_invoice_order_itempay_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 约束 |
做得好的地方:
PaymentEnterpriseBaseEntity,只有 tenant_id 的用 PaymentTenantBaseEntityTenantInnerInterceptor 的修改最小化且逻辑正确需要关注的风险点:
@TableName、字段映射、分页查询、事务)