read-c-users-1-claude-claude-md-before-s-fluttering-pancake.md 14 KB

Java vs Python 差异修复计划

上下文

基于前一轮审查报告,以 Python (FastAPI) 为参考标准,修复 Java (Spring Boot) 后端中发现的全部 14 项差异(P0/P1/P2)。所有修改仅涉及 Java 端代码(除 P2-12 需同步修复 Python float 精度问题)。


P0: 立即修复(5项)

P0-1: 通知幂等性 — 添加 Redis 去重

文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\service\NotificationService.java

改动:

  1. 注入 RedisLockUtil(已有 RedisTemplate 可用)
  2. verifyAndDispatch() 方法开头(L67 提取 notify_id 后),用 "notify:" + notifyId 作为 key 调 redisLockUtil.lock(key, 60)
  3. 若 lock 返回 null → 重复通知 → 直接 return "success"(不 return "fail",以免触发支付宝重试)
  4. 在 finally 块中调 redisLockUtil.unlock(key, lockValue) 释放

验证: 用相同 notify_id 连续两次调用 POST /alipay,第二次应直接返回 success 而不触发 handler


P0-2: POST /alipay 响应格式 — 改为纯文本

文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\controller\NotificationController.java

改动:

// 改前:
@PostMapping("/alipay")
public Result<String> alipayNotify(@RequestParam Map<String, String> params) {
    String result = notificationService.verifyAndDispatch(params);
    return Result.ok(result);
}

// 改后:
@PostMapping("/alipay")
public ResponseEntity<String> alipayNotify(@RequestParam Map<String, String> params) {
    String result = notificationService.verifyAndDispatch(params);
    return ResponseEntity.ok()
            .contentType(MediaType.TEXT_PLAIN)
            .body(result);
}

删除 Result.ok() 包装,改为 ResponseEntity<String> + text/plain Content-Type

验证: curl POST /alipay,响应体应为纯文本 successfail,非 JSON


P0-3: statConsumeAmount — 实现真实查询

文件:

  • D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\account\service\AccountService.java
  • D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\mapper\PayBillMapper.java

改动:

  1. AccountService 注入 PayBillMapper
  2. PayBillMapper 添加自定义查询方法(参考 doStatAmount 的 CASE-WHEN 聚合模式):

    @Select("""
    SELECT
      COALESCE(SUM(CASE WHEN gmt_recieve_pay >= CURRENT_DATE
        THEN consume_amount ELSE 0 END), 0) as amount_of_today,
      COALESCE(SUM(CASE WHEN gmt_recieve_pay >= CURRENT_DATE - INTERVAL '7 days'
        THEN consume_amount ELSE 0 END), 0) as amount_of_7days,
      COALESCE(SUM(consume_amount), 0) as amount_of_all
    FROM pay_bill WHERE consume_type = 'CONSUME' AND status = 'PROCESSED'
    AND tenant_id = #{tenantId}
    AND enterprise_id = #{enterpriseId}
    AND employee_id = #{payeeType}
    """)
    Map<String, BigDecimal> statConsumeAmount(@Param("tenantId") Long tenantId,
    @Param("enterpriseId") String enterpriseId,
    @Param("payeeType") String payeeType);
    
  3. 替换 AccountService.statConsumeAmount() 的硬编码桩为调用此方法

验证: 插入测试数据到 pay_bill,调用 GET /stat/consume/amount,应返回非零值


P0-4: 重试锁 — AtomicBoolean → Redis 分布式锁

文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\account\service\AlipayTransferService.java

改动:

  1. 注入 RedisLockUtil
  2. retryDealingTransfers() 中,将 L458 的 retryRunning.compareAndSet(false, true) 替换为:

    String lockValue = redisLockUtil.lock("retry:dealing_transfers", 120);
    if (lockValue == null) {
    log.debug("[重试任务] 上一轮尚未完成或另一实例执行中,跳过");
    return;
    }
    
  3. 将 L550 的 retryRunning.set(false) 替换为:

    redisLockUtil.unlock("retry:dealing_transfers", lockValue);
    
  4. 删除 L448 的 AtomicBoolean retryRunning 字段

验证: 确认多实例部署时不重复执行;进程崩溃后 TTL 120s 到期自动释放


P0-5: JWT 密钥 — 统一密钥为 Python 版本

文件: D:\project2\payment-platform\java\src\main\resources\application.yml

当前值 (L52): vgb0tnl9d58+6n-6h-ea&u^1#0sccp!794=krylxcjq75vzps$ Python值: vgb0tnl9d58+6n-6h-ea&u^1#1sccp!794=krylxcjq75vzps$ 差异: 第24字符 #0 vs #1

改动: 改为 vgb0tnl9d58+6n-6h-ea&u^1#1sccp!794=krylxcjq75vzps$(Python 版本)

验证: 用 Python 生成的 token 调 Java 端点,应能通过认证


P1: 尽快修复(4项)

P1-1: IssueBatchService — 补充缺失方法

文件:

  • D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\expense\quota\service\IssueBatchService.java
  • Python参考: D:\project2\payment-platform\backend\app\plugin\module_payment\expense\quota\service.py (L657-740)

现有状态: create(), cancel(), list() 已实现。records() 被错误映射到 page()

改动:

  1. 添加 records(String batchNo, int page, int size) 方法 — 按 batch_no 查询 QuotaEntity 中属于该批次的条目(参考 Python issue_batch_records_query_service
  2. 修改 QuotaController.java L148-153 的 POST /payment/quota/issuebatch/records 从调用 issueBatchService.page() 改为调用 issueBatchService.records()

验证: 创建批次后调 POST /issuebatch/records,应返回该批次的员工额度明细而非批次头列表


P1-2: AuthService — 补全验证码 & SMS 验证

文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\system\auth\service\AuthService.java

现状: CaptchaServiceSmsCodeServiceAutoLoginService 三个服务类已存在但未在 AuthService 中调用

改动:

  1. 注入 CaptchaService
  2. login() 方法开头添加验证码验证:

    if (request.getCaptchaKey() != null) {
    if (!captchaService.verify(request.getCaptchaKey(), request.getCaptchaCode())) {
        throw new BusinessException(ErrorCode.CAPTCHA_ERROR);
    }
    }
    
  3. LoginRequest DTO 添加 captchaKey / captchaCode 字段(如缺失)

  4. loginSms() 开头添加 SMS验证:

    if (!smsCodeService.verifyCode(request.getPhone(), request.getSmsCode())) {
    throw new BusinessException(ErrorCode.SMS_CODE_ERROR);
    }
    
  5. loginSms() 添加员工激活状态检查(参考 Python L208-214)

验证:

  • 登录时传错误验证码 → 返回验证码错误
  • SMS登录传错误code → 返回 SMS 验证失败
  • 未激活员工 SMS 登录 → 返回激活状态错误

P1-3: PointsController — 租户范围对齐

文件:

  • D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\points\controller\PointsController.java
  • D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\points\service\PointsService.java

问题: 当前通过请求参数 enterprise_id 显式传入租户,而不是从 auth context 推断 → 存在跨租户越权风险

改动:

  1. PointsService 或 Controller 中从 SecurityContextHolder 获取当前 LoginUser
  2. GET /payment/points — 自动过滤 tenant_id = currentUser.getTenantId()
  3. POST /payment/points/{id}/add / deduct — 从 auth 获取 enterprise_id,而非 DTO
  4. 添加 GET /payment/points 直接返回当前租户积分(而非分页列表),与 Python GET /payment/points 语义对齐
  5. 保留 /{id} 路径的 CRUD 作为管理端点(管理员需要,Python 没有但 Java 多出的 CRUD 保留)

验证:

  • A租户用户调 GET /points → 仅返回A租户积分
  • A租户用户调 POST /points/{B的id}/add → 返回403

P1-4: BillHandler — 修复字段映射错误

文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\handler\BillHandler.java

Bug 1 (L266): orderItem.getOrderType() 被赋值给 orderStatus,应改为 orderItem.getOrderStatus() Bug 2 (L323): voucher.getVoucherContent() 被赋值给 voucherStatus,应改为 voucher.getVoucherStatus()

改动:

// L266 改前:  orderEntity.setOrderStatus(orderItem.getOrderType());
// L266 改后:  orderEntity.setOrderStatus(orderItem.getOrderStatus());

// L323 改前:  voucher.setVoucherStatus(v.getVoucherContent());
// L323 改后:  voucher.setVoucherStatus(v.getVoucherStatus());

验证: 支付宝通知到达后,检查 DB 中 order.order_status 和 voucher.voucher_status 是否为实际状态值


P2: 重构优化(5项)

P2-1: PayDepartmentEntity — 继承基类

文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\department\entity\PayDepartmentEntity.java

改动:

  1. public class PayDepartmentEntity implements Serializable 改为 public class PayDepartmentEntity extends PaymentTenantBaseEntity
  2. 删除与基类重复的字段声明: id, tenantId, status, description, createdTime, updatedTime
  3. 保留 PayDepartmentEntity 特有的字段: departmentId, departmentName, departmentCode, parentDepartmentId, sortOrder, leaderEmployeeId, leaderEmployeeName, enterpriseId
  4. 如果 ID 需要 AUTO 而非 ASSIGN_ID,在 id 上添加 @TableId(type = IdType.AUTO) 覆盖基类默认值

验证: 编译通过 + 部门 CRUD 功能正常


P2-2: JSON 字段 — 添加 @JsonRawValue

受影响文件 (6个entity):

  • TransferEntity.javapayeeInfo, extInfo
  • PayBillEntity.javanotifyMsg, extInfos
  • EmployeeEntity.javadepartmentIds, accountingEntityIds, labelNames, profiles, departmentList, roleList
  • EnterpriseEntity.javabaseInfo, profiles
  • AlipayNotifyLogEntity.javamessage
  • DepositEntity.java / WithdrawEntity.javanotifyContent

改动: 对每个 JSON 字段的 getter 添加 @JsonRawValue,setter 添加 @JsonRawValue

@JsonRawValue
public String getPayeeInfo() { return payeeInfo; }

@JsonRawValue
public void setPayeeInfo(String payeeInfo) { this.payeeInfo = payeeInfo; }

这确保 Jackson 序列化时不会对已经 JSON 编码的字符串进行二次转义

验证: 序列化 TransferEntity 到 JSON,payeeInfo 字段应为展开的 JSON 对象而非转义字符串


P2-3: Python float → Decimal

文件:

  • D:\project2\payment-platform\backend\app\plugin\module_payment\expense\institution\model.py
  • D:\project2\payment-platform\backend\app\plugin\module_payment\expense\rule\model.py

改动:

from decimal import Decimal

# institution/model.py L62-64
amount: Mapped[Decimal | None] = mapped_column(
    Numeric(12, 2), default=0, comment="发放金额")

# institution/model.py L65-67
single_limit: Mapped[Decimal | None] = mapped_column(
    Numeric(12, 2), default=0, comment="单次限额")

# rule/model.py L43-45
single_limit: Mapped[Decimal | None] = mapped_column(
    Numeric(12, 2), default=0, comment="单次限额")

验证: Python 测试金额运算,确认精度无损失


P2-4: AlipayConfig — 多租户支持

文件:

  • D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\alipay\AlipayClientFactory.java
  • D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\alipay\AlipayConfig.java

改动:

  1. AlipayClientFactory 中:
    • private AlipayClient client 单例改为 private final Map<String, AlipayClient> clients = new ConcurrentHashMap<>()
    • 新增 getClient(String enterpriseId) 方法: 从 DB open_conf 表获取该企业的 Alipay 配置,懒加载创建 client
    • 保留 getClient() (无参) 作为默认/兜底 client
  2. 各调用方 (AlipayTransferService, AlipayDepartmentService 等) 改为调 getClient(enterpriseId) 代替 getClient()

验证: 两家企业用不同 app_id 调用 Alipay API,确认各自使用正确凭证


P2-5: DataScopeInnerInterceptor — 补全数据权限

文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\permission\DataScopeInnerInterceptor.java

当前状态: 仅实现 scope=1(仅本人),仅过滤 4 张系统表

改动:

  1. 注入 DeptService 用于查询部门子树
  2. 扩展 DATA_SCOPE_TABLES 列表包含 pay_department, pay_employee, pay_transfer, pay_bill 等支付表
  3. LoginUser 读取 dataScope 字段(需确认 LoginUser 是否已有此字段,如无则添加)
  4. 按 data_scope 值分支:
    • 1 仅本人: WHERE created_id = userId(已实现)
    • 2 本部门: WHERE dept_id = userDeptId
    • 3 本部门及以下: WHERE dept_id IN (userDeptId + 递归子部门ID列表)
    • 4 全部: 不添加任何过滤(已实现 — 跳过 interceptor)
    • 5 自定义: 不添加过滤(业务层处理)
  5. 将 L94 的 log.warn("数据权限SQL改写失败, 使用原始SQL", e) 在生产环境改为抛出异常(fail-closed 而非 fail-open)

验证: 不同 data_scope 角色的用户查询同一列表,返回结果集应不同


实施顺序

第一轮 (并行可做): P0-1, P0-2, P0-3, P0-5 (独立文件)
第二轮: P0-4 (依赖 RedisLockUtil 确认可用)
第三轮: P1-1, P1-2, P1-4 (独立文件)
第四轮: P1-3 (依赖 SecurityContext 模式确认)
第五轮: P2-1, P2-2, P2-3 (纯重构,不改变行为)
第六轮: P2-4, P2-5 (架构变更,需额外测试)

验证方法

  1. 编译: mvn compile -f java/pom.xml 确保无编译错误
  2. 单元测试: 每个改动提交前运行对应模块测试
  3. 集成验证:
    • P0-2: curl -X POST localhost:8081/api/v1/payment/notify/alipay -d '...' → 响应为纯文本
    • P0-5: 用 Python 签发 token → 调 Java 端点 → 200 OK
    • P1-3: 用租户A token → 调 GET /payment/points → 仅返回A的数据
  4. Swagger UI: 打开 http://localhost:8081/swagger-ui.html 确认端点路径和响应结构正确