基于前一轮审查报告,以 Python (FastAPI) 为参考标准,修复 Java (Spring Boot) 后端中发现的全部 14 项差异(P0/P1/P2)。所有修改仅涉及 Java 端代码(除 P2-12 需同步修复 Python float 精度问题)。
文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\service\NotificationService.java
改动:
RedisLockUtil(已有 RedisTemplate 可用)verifyAndDispatch() 方法开头(L67 提取 notify_id 后),用 "notify:" + notifyId 作为 key 调 redisLockUtil.lock(key, 60)"success"(不 return "fail",以免触发支付宝重试)redisLockUtil.unlock(key, lockValue) 释放验证: 用相同 notify_id 连续两次调用 POST /alipay,第二次应直接返回 success 而不触发 handler
文件: 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,响应体应为纯文本 success 或 fail,非 JSON
文件:
D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\account\service\AccountService.javaD:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\notification\mapper\PayBillMapper.java改动:
AccountService 注入 PayBillMapper在 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);
替换 AccountService.statConsumeAmount() 的硬编码桩为调用此方法
验证: 插入测试数据到 pay_bill,调用 GET /stat/consume/amount,应返回非零值
文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\account\service\AlipayTransferService.java
改动:
RedisLockUtil在 retryDealingTransfers() 中,将 L458 的 retryRunning.compareAndSet(false, true) 替换为:
String lockValue = redisLockUtil.lock("retry:dealing_transfers", 120);
if (lockValue == null) {
log.debug("[重试任务] 上一轮尚未完成或另一实例执行中,跳过");
return;
}
将 L550 的 retryRunning.set(false) 替换为:
redisLockUtil.unlock("retry:dealing_transfers", lockValue);
删除 L448 的 AtomicBoolean retryRunning 字段
验证: 确认多实例部署时不重复执行;进程崩溃后 TTL 120s 到期自动释放
文件: 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 端点,应能通过认证
文件:
D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\expense\quota\service\IssueBatchService.javaD:\project2\payment-platform\backend\app\plugin\module_payment\expense\quota\service.py (L657-740)现有状态: create(), cancel(), list() 已实现。records() 被错误映射到 page()
改动:
records(String batchNo, int page, int size) 方法 — 按 batch_no 查询 QuotaEntity 中属于该批次的条目(参考 Python issue_batch_records_query_service)QuotaController.java L148-153 的 POST /payment/quota/issuebatch/records 从调用 issueBatchService.page() 改为调用 issueBatchService.records()验证: 创建批次后调 POST /issuebatch/records,应返回该批次的员工额度明细而非批次头列表
文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\system\auth\service\AuthService.java
现状: CaptchaService、SmsCodeService、AutoLoginService 三个服务类已存在但未在 AuthService 中调用
改动:
CaptchaService在 login() 方法开头添加验证码验证:
if (request.getCaptchaKey() != null) {
if (!captchaService.verify(request.getCaptchaKey(), request.getCaptchaCode())) {
throw new BusinessException(ErrorCode.CAPTCHA_ERROR);
}
}
在 LoginRequest DTO 添加 captchaKey / captchaCode 字段(如缺失)
在 loginSms() 开头添加 SMS验证:
if (!smsCodeService.verifyCode(request.getPhone(), request.getSmsCode())) {
throw new BusinessException(ErrorCode.SMS_CODE_ERROR);
}
在 loginSms() 添加员工激活状态检查(参考 Python L208-214)
验证:
文件:
D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\points\controller\PointsController.javaD:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\points\service\PointsService.java问题: 当前通过请求参数 enterprise_id 显式传入租户,而不是从 auth context 推断 → 存在跨租户越权风险
改动:
PointsService 或 Controller 中从 SecurityContextHolder 获取当前 LoginUserGET /payment/points — 自动过滤 tenant_id = currentUser.getTenantId()POST /payment/points/{id}/add / deduct — 从 auth 获取 enterprise_id,而非 DTOGET /payment/points 直接返回当前租户积分(而非分页列表),与 Python GET /payment/points 语义对齐/{id} 路径的 CRUD 作为管理端点(管理员需要,Python 没有但 Java 多出的 CRUD 保留)验证:
文件: 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 是否为实际状态值
文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\module\payment\department\entity\PayDepartmentEntity.java
改动:
public class PayDepartmentEntity implements Serializable 改为 public class PayDepartmentEntity extends PaymentTenantBaseEntityid, tenantId, status, description, createdTime, updatedTimedepartmentId, departmentName, departmentCode, parentDepartmentId, sortOrder, leaderEmployeeId, leaderEmployeeName, enterpriseIdid 上添加 @TableId(type = IdType.AUTO) 覆盖基类默认值验证: 编译通过 + 部门 CRUD 功能正常
受影响文件 (6个entity):
TransferEntity.java — payeeInfo, extInfoPayBillEntity.java — notifyMsg, extInfosEmployeeEntity.java — departmentIds, accountingEntityIds, labelNames, profiles, departmentList, roleListEnterpriseEntity.java — baseInfo, profilesAlipayNotifyLogEntity.java — messageDepositEntity.java / WithdrawEntity.java — notifyContent改动: 对每个 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 对象而非转义字符串
文件:
D:\project2\payment-platform\backend\app\plugin\module_payment\expense\institution\model.pyD:\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 测试金额运算,确认精度无损失
文件:
D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\alipay\AlipayClientFactory.javaD:\project2\payment-platform\java\src\main\java\com\payment\platform\core\alipay\AlipayConfig.java改动:
AlipayClientFactory 中:
private AlipayClient client 单例改为 private final Map<String, AlipayClient> clients = new ConcurrentHashMap<>()getClient(String enterpriseId) 方法: 从 DB open_conf 表获取该企业的 Alipay 配置,懒加载创建 clientgetClient() (无参) 作为默认/兜底 clientAlipayTransferService, AlipayDepartmentService 等) 改为调 getClient(enterpriseId) 代替 getClient()验证: 两家企业用不同 app_id 调用 Alipay API,确认各自使用正确凭证
文件: D:\project2\payment-platform\java\src\main\java\com\payment\platform\core\permission\DataScopeInnerInterceptor.java
当前状态: 仅实现 scope=1(仅本人),仅过滤 4 张系统表
改动:
DeptService 用于查询部门子树DATA_SCOPE_TABLES 列表包含 pay_department, pay_employee, pay_transfer, pay_bill 等支付表LoginUser 读取 dataScope 字段(需确认 LoginUser 是否已有此字段,如无则添加)WHERE created_id = userId(已实现)WHERE dept_id = userDeptIdWHERE dept_id IN (userDeptId + 递归子部门ID列表)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 (架构变更,需额外测试)
mvn compile -f java/pom.xml 确保无编译错误curl -X POST localhost:8081/api/v1/payment/notify/alipay -d '...' → 响应为纯文本http://localhost:8081/swagger-ui.html 确认端点路径和响应结构正确