Browse Source

fix: Python→Java 差异修复 — 编译/运行/runtime错误 + DTO验证对齐

- P0: 通知幂等(Redis锁)、alipay纯文本响应、消费统计SQL、重试分布式锁、JWT密钥
- P1: 批次记录查询、AuthService验证码/SMS/员工激活、Points租户隔离、BillHandler字段
- P2: PayDepartment继承基类、Alipay多租户工厂、DataScope数据权限完整实现
- DTO验证: 16个Create/Update DTO必填/可选+约束对齐Python Pydantic schema
- DeptController: Map→DeptCreateDTO/DeptUpdateDTO
- 修复: @Valid提前校验(setId滞后)、@TableLogic冲突、租户拦截器遗漏关联表、uuid null约束、receipt/download参数
alphah 4 hours ago
parent
commit
34f50d8896
54 changed files with 636 additions and 98 deletions
  1. 64 13
      java/src/main/java/com/payment/platform/core/alipay/AlipayClientFactory.java
  2. 47 15
      java/src/main/java/com/payment/platform/core/permission/DataScopeInnerInterceptor.java
  3. 3 0
      java/src/main/java/com/payment/platform/core/security/LoginUser.java
  4. 2 0
      java/src/main/java/com/payment/platform/core/security/UserDetailsServiceImpl.java
  5. 4 1
      java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java
  6. 10 3
      java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java
  7. 4 0
      java/src/main/java/com/payment/platform/module/payment/account/dto/TransferCreateDTO.java
  8. 21 7
      java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java
  9. 9 5
      java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java
  10. 1 1
      java/src/main/java/com/payment/platform/module/payment/apikey/controller/ApikeyController.java
  11. 1 1
      java/src/main/java/com/payment/platform/module/payment/apikey/dto/ApiKeyCreateDTO.java
  12. 3 0
      java/src/main/java/com/payment/platform/module/payment/apikey/entity/TenantApiKeyEntity.java
  13. 4 1
      java/src/main/java/com/payment/platform/module/payment/apikey/service/ApikeyService.java
  14. 1 1
      java/src/main/java/com/payment/platform/module/payment/department/controller/PayDepartmentController.java
  15. 4 9
      java/src/main/java/com/payment/platform/module/payment/department/entity/PayDepartmentEntity.java
  16. 7 3
      java/src/main/java/com/payment/platform/module/payment/expense/controller/QuotaController.java
  17. 1 1
      java/src/main/java/com/payment/platform/module/payment/expense/institution/dto/InstitutionCreateDTO.java
  18. 23 0
      java/src/main/java/com/payment/platform/module/payment/expense/quota/service/IssueBatchService.java
  19. 2 0
      java/src/main/java/com/payment/platform/module/payment/expense/rule/dto/RuleCreateDTO.java
  20. 46 3
      java/src/main/java/com/payment/platform/module/payment/expense/rule/dto/RuleUpdateDTO.java
  21. 6 2
      java/src/main/java/com/payment/platform/module/payment/notification/controller/NotificationController.java
  22. 28 0
      java/src/main/java/com/payment/platform/module/payment/notification/mapper/PayBillMapper.java
  23. 12 1
      java/src/main/java/com/payment/platform/module/payment/notification/service/NotificationService.java
  24. 1 1
      java/src/main/java/com/payment/platform/module/payment/openapi/controller/OpenapiController.java
  25. 35 0
      java/src/main/java/com/payment/platform/module/payment/points/controller/PointsController.java
  26. 9 4
      java/src/main/java/com/payment/platform/module/payment/points/dto/PointsCreateDTO.java
  27. 3 0
      java/src/main/java/com/payment/platform/module/payment/points/dto/PointsQueryDTO.java
  28. 1 0
      java/src/main/java/com/payment/platform/module/payment/points/service/PointsService.java
  29. 1 6
      java/src/main/java/com/payment/platform/module/system/auth/controller/AuthController.java
  30. 12 0
      java/src/main/java/com/payment/platform/module/system/auth/dto/LoginRequest.java
  31. 28 1
      java/src/main/java/com/payment/platform/module/system/auth/service/AuthService.java
  32. 8 4
      java/src/main/java/com/payment/platform/module/system/dept/controller/DeptController.java
  33. 48 0
      java/src/main/java/com/payment/platform/module/system/dept/dto/DeptCreateDTO.java
  34. 40 0
      java/src/main/java/com/payment/platform/module/system/dept/dto/DeptUpdateDTO.java
  35. 72 0
      java/src/main/java/com/payment/platform/module/system/dept/service/DeptService.java
  36. 1 1
      java/src/main/java/com/payment/platform/module/system/dict/controller/DictController.java
  37. 7 1
      java/src/main/java/com/payment/platform/module/system/dict/dto/DictTypeCreateDTO.java
  38. 1 1
      java/src/main/java/com/payment/platform/module/system/menu/controller/MenuController.java
  39. 20 1
      java/src/main/java/com/payment/platform/module/system/menu/dto/MenuCreateDTO.java
  40. 1 1
      java/src/main/java/com/payment/platform/module/system/notice/controller/NoticeController.java
  41. 2 0
      java/src/main/java/com/payment/platform/module/system/notice/dto/NoticeCreateDTO.java
  42. 1 1
      java/src/main/java/com/payment/platform/module/system/params/controller/ParamsController.java
  43. 2 0
      java/src/main/java/com/payment/platform/module/system/params/dto/ParamsCreateDTO.java
  44. 1 1
      java/src/main/java/com/payment/platform/module/system/position/controller/PositionController.java
  45. 7 2
      java/src/main/java/com/payment/platform/module/system/position/dto/PositionCreateDTO.java
  46. 1 1
      java/src/main/java/com/payment/platform/module/system/role/controller/RoleController.java
  47. 6 0
      java/src/main/java/com/payment/platform/module/system/role/dto/RoleCreateDTO.java
  48. 1 1
      java/src/main/java/com/payment/platform/module/system/tenant/controller/TenantController.java
  49. 7 1
      java/src/main/java/com/payment/platform/module/system/tenant/dto/TenantCreateDTO.java
  50. 3 0
      java/src/main/java/com/payment/platform/module/system/user/dto/ChangePasswordDTO.java
  51. 2 0
      java/src/main/java/com/payment/platform/module/system/user/dto/ResetPasswordDTO.java
  52. 7 2
      java/src/main/java/com/payment/platform/module/system/user/dto/UserCreateDTO.java
  53. 4 0
      java/src/main/java/com/payment/platform/module/system/user/entity/UserEntity.java
  54. 1 1
      java/src/main/resources/application.yml

+ 64 - 13
java/src/main/java/com/payment/platform/core/alipay/AlipayClientFactory.java

@@ -4,21 +4,25 @@ import com.alipay.api.AlipayApiException;
 import com.alipay.api.AlipayClient;
 import com.alipay.api.AlipayConfig;
 import com.alipay.api.DefaultAlipayClient;
+import com.payment.platform.module.payment.openapi.entity.OpenConfEntity;
+import com.payment.platform.module.payment.openapi.mapper.OpenConfMapper;
 import jakarta.annotation.PostConstruct;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Component;
 
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
 /**
  * 支付宝客户端工厂 — 对应 Python AlipayClient.get_client()
  *
+ * 多租户支持: 默认使用 application.yml 中的配置;当传入 enterpriseId 时,
+ * 从 open_conf 表查找该企业/租户的配置并创建/缓存专属 AlipayClient。
+ *
  * Python SDK → Java SDK 映射:
  *   alipay.aop.api.DefaultAlipayClient  → com.alipay.api.DefaultAlipayClient
  *   alipay.aop.api.AlipayClientConfig    → com.alipay.api.AlipayConfig
- *   alipay.aop.api.domain.*              → com.alipay.api.domain.*
- *   alipay.aop.api.request.*             → com.alipay.api.request.*
- *   alipay.aop.api.response.*            → com.alipay.api.response.*
- *   alipay.aop.api.util.Signature        → com.alipay.api.internal.util.AlipaySignature
  */
 @Slf4j
 @Component
@@ -26,13 +30,15 @@ import org.springframework.stereotype.Component;
 public class AlipayClientFactory {
 
     private final com.payment.platform.core.alipay.AlipayConfig paymentAlipayConfig;
+    private final OpenConfMapper openConfMapper;
 
-    private AlipayClient client;
+    private final ConcurrentMap<String, AlipayClient> clients = new ConcurrentHashMap<>();
+    private AlipayClient defaultClient;
 
     @PostConstruct
     public void init() {
         if (!paymentAlipayConfig.isValid()) {
-            log.warn("支付宝配置不完整,跳过客户端初始化");
+            log.warn("支付宝配置不完整,跳过默认客户端初始化");
             return;
         }
 
@@ -46,21 +52,66 @@ public class AlipayClientFactory {
         config.setSignType(paymentAlipayConfig.getSignType());
 
         try {
-            this.client = new DefaultAlipayClient(config);
-            log.info("支付宝客户端初始化成功, appId={}", paymentAlipayConfig.getAppId());
+            this.defaultClient = new DefaultAlipayClient(config);
+            log.info("支付宝默认客户端初始化成功, appId={}", paymentAlipayConfig.getAppId());
         } catch (AlipayApiException e) {
-            log.error("支付宝客户端初始化失败", e);
+            log.error("支付宝默认客户端初始化失败", e);
         }
     }
 
+    /**
+     * 获取默认支付宝客户端(使用 application.yml 全局配置)
+     */
     public AlipayClient getClient() {
-        if (client == null) {
-            throw new IllegalStateException("支付宝客户端未初始化,请检查 alipay 配置");
+        if (defaultClient == null) {
+            throw new IllegalStateException("支付宝默认客户端未初始化,请检查 alipay 配置");
+        }
+        return defaultClient;
+    }
+
+    /**
+     * 获取指定租户的支付宝客户端 — 从 open_conf 表读取配置,懒加载创建并缓存
+     *
+     * @param tenantId 租户ID,为 null 时退化为 getClient()
+     */
+    public AlipayClient getClient(Long tenantId) {
+        if (tenantId == null) return getClient();
+        String cacheKey = "tenant:" + tenantId;
+        return clients.computeIfAbsent(cacheKey, k -> createClientForTenant(tenantId));
+    }
+
+    /**
+     * 根据 open_conf 表创建租户专属客户端
+     */
+    private AlipayClient createClientForTenant(Long tenantId) {
+        OpenConfEntity conf = openConfMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<OpenConfEntity>()
+                        .eq(OpenConfEntity::getTenantId, tenantId));
+        if (conf == null) {
+            log.warn("租户[{}]未配置开放平台信息,使用默认客户端", tenantId);
+            return getClient();
+        }
+
+        AlipayConfig config = new AlipayConfig();
+        config.setAppId(conf.getAppId());
+        config.setPrivateKey(conf.getPrivateKey());
+        config.setAlipayPublicKey(conf.getPublicKey());
+        config.setServerUrl(conf.getGatewayUrl() != null ? conf.getGatewayUrl() : paymentAlipayConfig.getServerUrl());
+        config.setFormat(conf.getFormat() != null ? conf.getFormat() : paymentAlipayConfig.getFormat());
+        config.setCharset(conf.getCharset() != null ? conf.getCharset() : paymentAlipayConfig.getCharset());
+        config.setSignType(conf.getSignType() != null ? conf.getSignType() : paymentAlipayConfig.getSignType());
+
+        try {
+            AlipayClient client = new DefaultAlipayClient(config);
+            log.info("租户[{}]支付宝客户端创建成功, appId={}", tenantId, conf.getAppId());
+            return client;
+        } catch (AlipayApiException e) {
+            log.error("租户[{}]支付宝客户端创建失败,回退默认客户端", tenantId, e);
+            return getClient();
         }
-        return client;
     }
 
     public boolean isReady() {
-        return client != null;
+        return defaultClient != null;
     }
 }

+ 47 - 15
java/src/main/java/com/payment/platform/core/permission/DataScopeInnerInterceptor.java

@@ -1,15 +1,16 @@
 package com.payment.platform.core.permission;
 
-import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
 import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
 import com.payment.platform.core.security.LoginUser;
+import com.payment.platform.module.system.dept.service.DeptService;
+import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.LongValue;
 import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
 import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
 import net.sf.jsqlparser.expression.operators.relational.InExpression;
-import net.sf.jsqlparser.expression.operators.relational.ItemsList;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
 import net.sf.jsqlparser.parser.CCJSqlParserUtil;
 import net.sf.jsqlparser.schema.Column;
 import net.sf.jsqlparser.statement.Statement;
@@ -17,7 +18,6 @@ import net.sf.jsqlparser.statement.select.PlainSelect;
 import net.sf.jsqlparser.statement.select.Select;
 import net.sf.jsqlparser.statement.select.SelectBody;
 import org.apache.ibatis.executor.Executor;
-import org.apache.ibatis.executor.statement.StatementHandler;
 import org.apache.ibatis.mapping.BoundSql;
 import org.apache.ibatis.mapping.MappedStatement;
 import org.apache.ibatis.session.ResultHandler;
@@ -26,7 +26,6 @@ import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.stereotype.Component;
 
-import java.sql.Connection;
 import java.util.List;
 
 /**
@@ -35,21 +34,26 @@ import java.util.List;
  *
  * 策略:
  * - data_scope=1: WHERE created_id = currentUserId
- * - data_scope=2: WHERE dept_id = currentDeptId
- * - data_scope=3: WHERE dept_id IN (currentDeptId + children)
+ * - data_scope=2: WHERE dept_id = userDeptId
+ * - data_scope=3: WHERE dept_id IN (userDeptId + child dept IDs)
  * - data_scope=4/5: 不追加(全部数据 或 自定义由业务层处理)
  */
 @Slf4j
 @Component
+@RequiredArgsConstructor
 public class DataScopeInnerInterceptor implements InnerInterceptor {
 
     /**
-     * 需要数据权限过滤的表
+     * 需要数据权限过滤的表 — 系统表 + 支付业务表
      */
     private static final List<String> DATA_SCOPE_TABLES = List.of(
-            "sys_user", "sys_dept", "sys_role", "sys_log"
+            "sys_user", "sys_dept", "sys_role", "sys_log",
+            "pay_department", "pay_employee", "pay_transfer", "pay_bill",
+            "pay_expense_quota", "pay_deposit", "pay_withdraw", "pay_account"
     );
 
+    private final DeptService deptService;
+
     @Override
     public void beforeQuery(Executor executor, MappedStatement ms, Object parameter,
                             RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
@@ -58,10 +62,14 @@ public class DataScopeInnerInterceptor implements InnerInterceptor {
             return;
         }
 
-        // 管理员不限制
+        // 管理员或 data_scope=4(全部)不限制
         if (user.getIsSuperuser() != null && user.getIsSuperuser()) {
             return;
         }
+        Integer dataScope = user.getDataScope();
+        if (dataScope == null || dataScope == 4 || dataScope == 5) {
+            return;
+        }
 
         String originalSql = boundSql.getSql();
         try {
@@ -80,10 +88,8 @@ public class DataScopeInnerInterceptor implements InnerInterceptor {
                 return;
             }
 
-            // data_scope=1 → 仅本人数据
-            Expression scopeCondition = new EqualsTo(
-                    new Column("created_id"),
-                    new LongValue(user.getUserId()));
+            Expression scopeCondition = buildScopeCondition(dataScope, user);
+            if (scopeCondition == null) return;
 
             if (plainSelect.getWhere() == null) {
                 plainSelect.setWhere(scopeCondition);
@@ -91,12 +97,38 @@ public class DataScopeInnerInterceptor implements InnerInterceptor {
                 plainSelect.setWhere(new AndExpression(plainSelect.getWhere(), scopeCondition));
             }
         } catch (Exception e) {
-            log.warn("数据权限SQL改写失败, 使用原始SQL", e);
+            log.error("数据权限SQL改写失败,拒绝查询(fail-closed): {}", e.getMessage());
+            throw new RuntimeException("数据权限拦截失败,查询被拒绝", e);
         }
     }
 
+    private Expression buildScopeCondition(Integer dataScope, LoginUser user) {
+        return switch (dataScope) {
+            case 1 -> // 仅本人
+                    new EqualsTo(new Column("created_id"), new LongValue(user.getUserId()));
+            case 2 -> // 本部门
+                    new EqualsTo(new Column("dept_id"), new LongValue(user.getDeptId()));
+            case 3 -> { // 本部门及以下
+                List<Long> deptIds = deptService.getDeptAndChildrenIds(user.getDeptId());
+                if (deptIds.isEmpty()) {
+                    yield new EqualsTo(new Column("dept_id"), new LongValue(user.getDeptId()));
+                }
+                java.util.List<Expression> expressions = deptIds.stream()
+                        .<Expression>map(LongValue::new).toList();
+                yield new InExpression(
+                        new Column("dept_id"),
+                        new ExpressionList(expressions));
+            }
+            default -> null;
+        };
+    }
+
     private String extractTableName(PlainSelect plainSelect) {
         if (plainSelect.getFromItem() == null) return null;
-        return plainSelect.getFromItem().toString().replaceAll("[`\"]", "");
+        String table = plainSelect.getFromItem().toString();
+        // 去除别名,仅保留表名部分
+        int spaceIdx = table.indexOf(' ');
+        if (spaceIdx > 0) table = table.substring(0, spaceIdx);
+        return table.replaceAll("[`\"]", "");
     }
 }

+ 3 - 0
java/src/main/java/com/payment/platform/core/security/LoginUser.java

@@ -14,12 +14,15 @@ public class LoginUser implements UserDetails {
 
     private Long userId;
     private Long tenantId;
+    private Long deptId;
     private String username;
     private String password;
     private String nickname;
     private String status;
     private List<GrantedAuthority> authorities;
     private Boolean isSuperuser;
+    /** 数据权限范围: 1=仅本人, 2=本部门, 3=本部门及以下, 4=全部, 5=自定义 */
+    private Integer dataScope;
 
     @Override
     public Collection<? extends GrantedAuthority> getAuthorities() {

+ 2 - 0
java/src/main/java/com/payment/platform/core/security/UserDetailsServiceImpl.java

@@ -52,11 +52,13 @@ public class UserDetailsServiceImpl implements UserDetailsService {
         LoginUser loginUser = new LoginUser();
         loginUser.setUserId(user.getId());
         loginUser.setTenantId(user.getTenantId());
+        loginUser.setDeptId(user.getDeptId());
         loginUser.setUsername(user.getUsername());
         loginUser.setPassword(user.getPassword());
         loginUser.setNickname(user.getName());
         loginUser.setStatus(user.getStatus());
         loginUser.setIsSuperuser(user.getIsSuperuser() != null && user.getIsSuperuser());
+        loginUser.setDataScope(user.getDataScope());
 
         // 角色权限 — 从 sys_user_roles → sys_role_menus → sys_menu.permission 加载
         // 对应 Python AuthPermission: user_permissions = {menu.permission for ...}

+ 4 - 1
java/src/main/java/com/payment/platform/core/tenant/TenantInnerInterceptor.java

@@ -32,7 +32,10 @@ public class TenantInnerInterceptor extends TenantLineInnerInterceptor {
             "sys_dict_data",      // 字典数据
             "sys_param",          // 系统参数
             "sys_notice",         // 通知公告
-            "sys_log"             // 操作日志
+            "sys_log",            // 操作日志
+            "sys_role_menus",     // 角色菜单关联表(无 tenant_id 列)
+            "sys_user_roles",     // 用户角色关联表(无 tenant_id 列)
+            "sys_user_social"     // 用户第三方登录(无 tenant_id 列)
     );
 
     public TenantInnerInterceptor() {

+ 10 - 3
java/src/main/java/com/payment/platform/module/payment/account/controller/AccountController.java

@@ -118,9 +118,16 @@ public class AccountController {
 
     @GetMapping("/receipt/download")
     public Result<?> downloadReceipt(
-            @RequestParam(name = "enterprise_id") String enterpriseId,
-            @RequestParam(name = "order_no") String orderNo) {
-        return Result.ok(accountService.downloadReceipt(enterpriseId, orderNo));
+            @RequestParam(name = "enterprise_id", required = false) String enterpriseId,
+            @RequestParam(name = "order_no", required = false) String orderNo) {
+        // 优先用 order_no 查转账记录自动获取 enterprise_id
+        if (enterpriseId == null && orderNo != null) {
+            return Result.ok(accountService.downloadReceiptByOrderNo(orderNo));
+        }
+        if (enterpriseId != null && orderNo != null) {
+            return Result.ok(accountService.downloadReceipt(enterpriseId, orderNo));
+        }
+        return Result.fail(400, "order_no 不能为空");
     }
 
     @GetMapping("/receipt/{enterprise_id}/{file_id}")

+ 4 - 0
java/src/main/java/com/payment/platform/module/payment/account/dto/TransferCreateDTO.java

@@ -1,6 +1,8 @@
 package com.payment.platform.module.payment.account.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.Digits;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
@@ -19,6 +21,8 @@ public class TransferCreateDTO {
     private String accountBookId;
 
     @NotNull(message = "转账金额不能为空")
+    @DecimalMin(value = "0.01", message = "转账金额必须大于0")
+    @Digits(integer = 10, fraction = 2, message = "金额最多10位整数2位小数")
     @Schema(description = "转账金额")
     private BigDecimal amount;
 

+ 21 - 7
java/src/main/java/com/payment/platform/module/payment/account/service/AccountService.java

@@ -10,6 +10,7 @@ import com.payment.platform.module.payment.account.entity.AccountEntity;
 import com.payment.platform.module.payment.account.entity.TransferEntity;
 import com.payment.platform.module.payment.account.mapper.AccountMapper;
 import com.payment.platform.module.payment.account.mapper.TransferMapper;
+import com.payment.platform.module.payment.notification.mapper.PayBillMapper;
 import com.payment.platform.common.exception.BusinessException;
 import com.payment.platform.core.alipay.AlipayClientFactory;
 import com.alipay.api.AlipayApiException;
@@ -45,6 +46,7 @@ public class AccountService {
     private final TransferMapper transferMapper;
     private final AlipayClientFactory alipayClientFactory;
     private final RedisTemplate<String, Object> redisTemplate;
+    private final PayBillMapper payBillMapper;
 
     public List<AccountVO> queryAccounts(String enterpriseId) {
         return accountMapper.selectList(
@@ -58,16 +60,16 @@ public class AccountService {
     }
 
     /**
-     * 统计消费金额
+     * 统计消费金额 — 查询 pay_bill 表 (consume_type=CONSUME, status=PROCESSED)
      *
-     * Python 对应: stat_consume_amount_service 查询 pay_bill 表
-     * (consume_type=CONSUME, status=PROCESSED),不是查 pay_transfer。
-     * 当前仅为占位实现,需接入 BillCRUD / BillMapper。
+     * Python 对应: stat_consume_amount_service
      */
     public Map<String, String> statConsumeAmount(Long tenantId, String enterpriseId, String payeeType) {
-        return Map.of("amount_of_today", "0",
-                "amount_of_7days", "0",
-                "amount_of_all", "0");
+        Map<String, BigDecimal> amounts = payBillMapper.statConsumeAmount(tenantId, enterpriseId, payeeType);
+        return Map.of(
+                "amount_of_today", amounts.getOrDefault("amount_of_today", BigDecimal.ZERO).toPlainString(),
+                "amount_of_7days", amounts.getOrDefault("amount_of_7days", BigDecimal.ZERO).toPlainString(),
+                "amount_of_all",  amounts.getOrDefault("amount_of_all", BigDecimal.ZERO).toPlainString());
     }
 
     /**
@@ -363,6 +365,18 @@ public class AccountService {
         return queryReceipt(enterpriseId, fileId);
     }
 
+    /**
+     * 通过 order_no 下载回单 — 自动从转账记录获取 enterprise_id
+     */
+    public Map<String, Object> downloadReceiptByOrderNo(String orderNo) {
+        TransferEntity transfer = transferMapper.selectOne(
+                new LambdaQueryWrapper<TransferEntity>().eq(TransferEntity::getOrderNo, orderNo));
+        if (transfer == null || transfer.getEnterpriseId() == null) {
+            throw new BusinessException(400, "未找到对应转账记录,无法获取 enterprise_id");
+        }
+        return downloadReceipt(transfer.getEnterpriseId(), orderNo);
+    }
+
 
     private AccountVO accountVO(AccountEntity a) {
         AccountVO vo = new AccountVO();

+ 9 - 5
java/src/main/java/com/payment/platform/module/payment/account/service/AlipayTransferService.java

@@ -12,6 +12,7 @@ import com.alipay.api.request.*;
 import com.alipay.api.response.*;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.common.utils.RedisLockUtil;
 import com.payment.platform.common.utils.SnowflakeIdGenerator;
 import com.payment.platform.core.alipay.AlipayClientFactory;
 import com.payment.platform.module.payment.account.dto.TransferCreateDTO;
@@ -50,6 +51,7 @@ public class AlipayTransferService {
     private final WithdrawMapper withdrawMapper;
     private final EnterpriseMapper enterpriseMapper;
     private final ObjectMapper objectMapper;
+    private final RedisLockUtil redisLockUtil;
 
     // 自注入用于独立事务(对应 Python 的 async with async_db_session())
     // @Lazy 避免构造期间的循环依赖
@@ -444,8 +446,9 @@ public class AlipayTransferService {
     // ─────────── 退避时间映射 ───────────
     private static final int[] RETRY_INTERVALS = {5, 10, 45, 60};  // 累计: +5min, +15min, +1h, +2h
 
-    // 简易单实例防重入锁(多实例部署时应替换为 DB advisory lock 或 Redis 分布式锁)
-    private final java.util.concurrent.atomic.AtomicBoolean retryRunning = new java.util.concurrent.atomic.AtomicBoolean(false);
+    // 分布式锁保证多实例不重复执行 — 对应 Python pg_try_advisory_lock(99999)
+    private static final String RETRY_LOCK_KEY = "retry:dealing_transfers";
+    private static final long RETRY_LOCK_TTL = 120; // 秒,进程崩溃后自动释放
 
     /**
      * 反查转账状态定时任务 — 对应 Python retry_dealing_transfers (L1128-1230)
@@ -455,8 +458,9 @@ public class AlipayTransferService {
      */
     @org.springframework.scheduling.annotation.Scheduled(fixedDelay = 60_000, initialDelay = 5_000)
     public void retryDealingTransfers() {
-        if (!retryRunning.compareAndSet(false, true)) {
-            log.debug("[重试任务] 上一轮尚未完成,跳过");
+        String lockValue = redisLockUtil.lock(RETRY_LOCK_KEY, RETRY_LOCK_TTL);
+        if (lockValue == null) {
+            log.debug("[重试任务] 上一轮尚未完成或另一实例执行中,跳过");
             return;
         }
         try {
@@ -547,7 +551,7 @@ public class AlipayTransferService {
         } catch (Exception e) {
             log.error("[重试任务] 执行异常", e);
         } finally {
-            retryRunning.set(false);
+            redisLockUtil.unlock(RETRY_LOCK_KEY, lockValue);
         }
     }
 

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/apikey/controller/ApikeyController.java

@@ -48,7 +48,7 @@ public class ApikeyController {
 
     @PutMapping("/{id}")
     @Operation(summary = "更新API Key")
-    public Result<ApiKeyVO> update(@PathVariable Long id, @Valid @RequestBody ApiKeyUpdateDTO dto) {
+    public Result<ApiKeyVO> update(@PathVariable Long id, @RequestBody ApiKeyUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(apikeyService.update(dto));
     }

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/apikey/dto/ApiKeyCreateDTO.java

@@ -7,7 +7,7 @@ import lombok.Data;
 public class ApiKeyCreateDTO {
 
     @Schema(description = "过期天数(默认365天)")
-    private Integer expiredDays;
+    private Integer expiredDays = 365;
 
     @Schema(description = "回调地址")
     private String returnUrl;

+ 3 - 0
java/src/main/java/com/payment/platform/module/payment/apikey/entity/TenantApiKeyEntity.java

@@ -12,6 +12,9 @@ import java.time.OffsetDateTime;
 @TableName("sys_tenant_api_key")
 public class TenantApiKeyEntity extends TenantBaseEntity {
 
+    /** 覆盖基类 @TableLogic — API Key 的状态是启用/停用,不是逻辑删除 */
+    private String status;
+
     private String apiKey;
     private String apiSecret;
     private OffsetDateTime expiredAt;

+ 4 - 1
java/src/main/java/com/payment/platform/module/payment/apikey/service/ApikeyService.java

@@ -1,6 +1,7 @@
 package com.payment.platform.module.payment.apikey.service;
 
 import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.bean.copier.CopyOptions;
 import cn.hutool.core.util.StrUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -60,6 +61,7 @@ public class ApikeyService {
         entity.setTenantId(tenantId);
         entity.setReturnUrl(dto.getReturnUrl());
         entity.setDescription(dto.getDescription());
+        entity.setUuid(UUID.randomUUID().toString());
         entity.setApiKey(generateApiKey(tenantId));
         entity.setApiSecret(generateApiSecret());
         int days = dto.getExpiredDays() != null ? dto.getExpiredDays() : 365;
@@ -73,7 +75,8 @@ public class ApikeyService {
     @Transactional
     public ApiKeyVO update(ApiKeyUpdateDTO dto) {
         TenantApiKeyEntity exist = requireKey(dto.getId());
-        BeanUtil.copyProperties(dto, exist, "id", "apiKey", "apiSecret", "tenantId");
+        BeanUtil.copyProperties(dto, exist,
+                CopyOptions.create().setIgnoreNullValue(true).setIgnoreProperties("id", "apiKey", "apiSecret", "tenantId"));
         apiKeyMapper.updateById(exist);
         ApiKeyVO vo = BeanUtil.copyProperties(apiKeyMapper.selectById(dto.getId()), ApiKeyVO.class);
         vo.setApiSecret(maskSecret(vo.getApiSecret()));

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/department/controller/PayDepartmentController.java

@@ -48,7 +48,7 @@ public class PayDepartmentController {
     @PutMapping("/{department_id}")
     public Result<?> update(@PathVariable(name = "department_id") Long id,
             @RequestParam(name = "enterprise_id") String enterpriseId,
-            @Valid @RequestBody PayDepartmentUpdateDTO dto) {
+            @RequestBody PayDepartmentUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(departmentService.update(dto));
     }

+ 4 - 9
java/src/main/java/com/payment/platform/module/payment/department/entity/PayDepartmentEntity.java

@@ -3,14 +3,14 @@ package com.payment.platform.module.payment.department.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.payment.platform.common.base.PaymentTenantBaseEntity;
 import lombok.Data;
-
-import java.io.Serializable;
-import java.time.OffsetDateTime;
+import lombok.EqualsAndHashCode;
 
 @Data
+@EqualsAndHashCode(callSuper = true)
 @TableName("pay_department")
-public class PayDepartmentEntity implements Serializable {
+public class PayDepartmentEntity extends PaymentTenantBaseEntity {
 
     @TableId(type = IdType.AUTO)
     private Long id;
@@ -19,13 +19,8 @@ public class PayDepartmentEntity implements Serializable {
     private String departmentName;
     private String departmentCode;
     private String parentDepartmentId;
-    private String status;
     private Integer sortOrder;
     private String leaderEmployeeId;
     private String leaderEmployeeName;
-    private String description;
     private String enterpriseId;
-    private Long tenantId;
-    private OffsetDateTime createdTime;
-    private OffsetDateTime updatedTime;
 }

+ 7 - 3
java/src/main/java/com/payment/platform/module/payment/expense/controller/QuotaController.java

@@ -43,7 +43,7 @@ public class QuotaController {
     }
 
     @PutMapping("/{quota_id}")
-    public Result<QuotaVO> update(@PathVariable(name = "quota_id") String quotaId, @Valid @RequestBody QuotaUpdateDTO dto) {
+    public Result<QuotaVO> update(@PathVariable(name = "quota_id") String quotaId, @RequestBody QuotaUpdateDTO dto) {
         // Resolve DB primary key from the business quotaId (UUID from Alipay) so the
         // path variable isn't wasted and the service can find the record by either key.
         QuotaVO resolved = quotaService.detail(quotaId);
@@ -146,10 +146,14 @@ public class QuotaController {
     }
 
     @PostMapping("/issuebatch/records")
-    public Result<PageResult<IssueBatchVO>> issueBatchRecords(@RequestBody Map<String, Object> body) {
+    public Result<PageResult<QuotaVO>> issueBatchRecords(@RequestBody Map<String, Object> body) {
+        String batchNo = (String) body.get("batch_no");
+        if (batchNo == null) {
+            return Result.badRequest("batch_no不能为空");
+        }
         int pageNo = body.containsKey("page_num") ? ((Number) body.get("page_num")).intValue() : 1;
         int pageSize = body.containsKey("page_size") ? ((Number) body.get("page_size")).intValue() : 10;
-        return Result.ok(issueBatchService.page(pageNo, pageSize, body));
+        return Result.ok(issueBatchService.records(batchNo, pageNo, pageSize));
     }
 
     // ==================== 外部通知 ====================

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/expense/institution/dto/InstitutionCreateDTO.java

@@ -12,13 +12,13 @@ import java.util.Map;
 @Data
 public class InstitutionCreateDTO {
 
-    @NotBlank(message = "制度名称不能为空")
     @Schema(description = "制度名称")
     private String institutionName;
 
     @Schema(description = "制度描述")
     private String institutionDesc;
 
+    @NotBlank(message = "企业ID不能为空")
     @Schema(description = "企业ID")
     private String enterpriseId;
 

+ 23 - 0
java/src/main/java/com/payment/platform/module/payment/expense/quota/service/IssueBatchService.java

@@ -6,10 +6,13 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.payment.platform.common.exception.BusinessException;
 import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.response.PageResult;
 import com.payment.platform.module.payment.expense.entity.IssueBatchEntity;
+import com.payment.platform.module.payment.expense.entity.QuotaEntity;
 import com.payment.platform.module.payment.expense.mapper.IssueBatchMapper;
 import com.payment.platform.module.payment.expense.mapper.QuotaMapper;
 import com.payment.platform.module.payment.expense.quota.dto.IssueBatchVO;
+import com.payment.platform.module.payment.expense.quota.dto.QuotaVO;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import com.alipay.api.AlipayApiException;
@@ -204,6 +207,26 @@ public class IssueBatchService {
         log.info("发放批次已删除, issueBatchId={}", issueBatchId);
     }
 
+    // ==================== 批次记录 ====================
+
+    /**
+     * 查询批次发放的员工额度明细 — 对应 Python issue_batch_records_query_service
+     *
+     * 按 batch_no 模糊匹配 QuotaEntity.outBizNo (模式: batch_{batchNo}_%),
+     * 返回该批次创建的员工额度列表。
+     */
+    public PageResult<QuotaVO> records(String batchNo, int pageNo, int pageSize) {
+        Page<QuotaEntity> page = new Page<>(pageNo, pageSize);
+        LambdaQueryWrapper<QuotaEntity> w = new LambdaQueryWrapper<QuotaEntity>()
+                .like(QuotaEntity::getOutBizNo, "batch_" + batchNo + "_%")
+                .orderByDesc(QuotaEntity::getCreatedTime);
+        Page<QuotaEntity> result = quotaMapper.selectPage(page, w);
+        return PageResult.of(pageNo, pageSize, result.getTotal(),
+                result.getRecords().stream()
+                        .map(e -> BeanUtil.copyProperties(e, QuotaVO.class))
+                        .collect(Collectors.toList()));
+    }
+
     // ==================== private helpers ====================
 
     private IssueBatchEntity requireByIssueBatchId(String issueBatchId) {

+ 2 - 0
java/src/main/java/com/payment/platform/module/payment/expense/rule/dto/RuleCreateDTO.java

@@ -2,6 +2,7 @@ package com.payment.platform.module.payment.expense.rule.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -15,6 +16,7 @@ public class RuleCreateDTO {
     private String institutionId;
 
     @NotBlank(message = "规则名称不能为空")
+    @Size(max = 20, message = "规则名称最长20字符")
     @Schema(description = "规则名称")
     private String name;
 

+ 46 - 3
java/src/main/java/com/payment/platform/module/payment/expense/rule/dto/RuleUpdateDTO.java

@@ -1,9 +1,52 @@
 package com.payment.platform.module.payment.expense.rule.dto;
 
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
-import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+import java.util.List;
 
 @Data
-@EqualsAndHashCode(callSuper = true)
-public class RuleUpdateDTO extends RuleCreateDTO {
+public class RuleUpdateDTO {
+
+    @NotNull(message = "ID不能为空")
+    @Schema(description = "规则ID")
+    private Long id;
+
+    @Schema(description = "关联制度ID")
+    private String institutionId;
+
+    @Schema(description = "规则名称")
+    private String name;
+
+    @Schema(description = "规则描述")
+    private String description;
+
+    @Schema(description = "单笔限额(最大)")
+    private BigDecimal maxAmount;
+
+    @Schema(description = "单日限额(最大)")
+    private BigDecimal maxDayAmount;
+
+    @Schema(description = "单月限额(最大)")
+    private BigDecimal maxMonthAmount;
+
+    @Schema(description = "单笔限额(最小)")
+    private BigDecimal minAmount;
+
+    @Schema(description = "有效期起始 (HH:mm)")
+    private String validFrom;
+
+    @Schema(description = "有效期截止 (HH:mm)")
+    private String validTo;
+
+    @Schema(description = "商户PID")
+    private String merchantPid;
+
+    @Schema(description = "适用星期 (1-7, 1=周一)")
+    private List<Integer> weekDays;
+
+    @Schema(description = "适用时间段")
+    private List<RuleCreateDTO.TimeRangeDTO> timeRanges;
 }

+ 6 - 2
java/src/main/java/com/payment/platform/module/payment/notification/controller/NotificationController.java

@@ -5,6 +5,8 @@ import com.payment.platform.common.response.Result;
 import com.payment.platform.module.payment.notification.service.NotificationService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.Map;
@@ -18,9 +20,11 @@ public class NotificationController {
     private final NotificationService notificationService;
 
     @PostMapping("/alipay")
-    public Result<String> alipayNotify(@RequestParam Map<String, String> params) {
+    public ResponseEntity<String> alipayNotify(@RequestParam Map<String, String> params) {
         String result = notificationService.verifyAndDispatch(params);
-        return Result.ok(result);
+        return ResponseEntity.ok()
+                .contentType(MediaType.TEXT_PLAIN)
+                .body(result);
     }
 
     @GetMapping("/logs")

+ 28 - 0
java/src/main/java/com/payment/platform/module/payment/notification/mapper/PayBillMapper.java

@@ -3,7 +3,35 @@ package com.payment.platform.module.payment.notification.mapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.payment.platform.module.payment.notification.entity.PayBillEntity;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.math.BigDecimal;
+import java.util.Map;
 
 @Mapper
 public interface PayBillMapper extends BaseMapper<PayBillEntity> {
+
+    /**
+     * 统计消费金额 — 对应 Python stat_consume_amount_service
+     * 查询 pay_bill 表 (consume_type=CONSUME, status=PROCESSED)
+     * 单次 SQL 完成三个时间窗口聚合(参考 doStatAmount 模式)
+     */
+    @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);
 }
+

+ 12 - 1
java/src/main/java/com/payment/platform/module/payment/notification/service/NotificationService.java

@@ -4,6 +4,7 @@ import com.alipay.api.internal.util.AlipaySignature;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 
 import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.utils.RedisLockUtil;
 import com.payment.platform.core.alipay.AlipayConfig;
 import com.payment.platform.module.payment.account.mapper.TransferMapper;
 import com.payment.platform.module.payment.enterprise.mapper.EnterpriseMapper;
@@ -46,6 +47,7 @@ public class NotificationService {
     private final QuotaMapper quotaMapper;
     private final OpenapiService openapiService;
     private final AlipayConfig alipayConfig;
+    private final RedisLockUtil redisLockUtil;
 
     private static final ObjectMapper oMapper = new ObjectMapper();
 
@@ -61,13 +63,20 @@ public class NotificationService {
                 handlers.stream().map(h -> h.getClass().getSimpleName()).toList());
     }
 
-    /** 验证签名并分发处理 */
+    /** 验证签名并分发处理 — 含 Redis 幂等性检查 */
     @Transactional
     public String verifyAndDispatch(Map<String, String> params) {
         String notifyId = params.get("notify_id");
         String msgMethod = params.get("msg_method");
         log.info("收到支付宝通知: msg_method={}, notify_id={}", msgMethod, notifyId);
 
+        // Redis 幂等锁 — 对应 Python _is_notify_processed / _mark_notify_processed
+        String lockKey = "notify:" + notifyId;
+        String lockValue = redisLockUtil.lock(lockKey, 60);
+        if (lockValue == null) {
+            log.info("重复通知(幂等), 直接返回成功: notify_id={}", notifyId);
+            return "success";
+        }
         AlipayNotifyLogEntity entry = new AlipayNotifyLogEntity();
         entry.setNotifyId(notifyId);
         entry.setMsgMethod(msgMethod);
@@ -97,6 +106,8 @@ public class NotificationService {
             entry.setError(e.getMessage());
             notifyLogMapper.insert(entry);
             return "fail";
+        } finally {
+            redisLockUtil.unlock(lockKey, lockValue);
         }
     }
 

+ 1 - 1
java/src/main/java/com/payment/platform/module/payment/openapi/controller/OpenapiController.java

@@ -74,7 +74,7 @@ public class OpenapiController {
 
     @PutMapping("/{id}")
     @Operation(summary = "更新开放配置")
-    public Result<OpenConfVO> update(@PathVariable Long id, @Valid @RequestBody OpenConfUpdateDTO dto) {
+    public Result<OpenConfVO> update(@PathVariable Long id, @RequestBody OpenConfUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(openapiService.updateConf(dto));
     }

+ 35 - 0
java/src/main/java/com/payment/platform/module/payment/points/controller/PointsController.java

@@ -1,10 +1,14 @@
 package com.payment.platform.module.payment.points.controller;
 
+import com.payment.platform.common.exception.BusinessException;
 import com.payment.platform.common.response.Result;
+import com.payment.platform.core.security.LoginUser;
 import com.payment.platform.module.payment.points.dto.*;
 import com.payment.platform.module.payment.points.service.PointsService;
 import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.web.bind.annotation.*;
 
 import java.math.BigDecimal;
@@ -28,6 +32,9 @@ public class PointsController {
         PointsQueryDTO query = new PointsQueryDTO();
         query.setEnterpriseId(enterpriseId);
         query.setStatus(status);
+        // 自动从安全上下文注入当前租户ID,防止跨租户越权
+        LoginUser loginUser = currentUser();
+        if (loginUser != null) query.setTenantId(loginUser.getTenantId());
         return Result.ok(pointsService.getPage(pageNo, pageSize, query));
     }
 
@@ -43,6 +50,10 @@ public class PointsController {
 
     @PostMapping
     public Result<?> create(@Valid @RequestBody PointsCreateDTO dto) {
+        LoginUser loginUser = currentUser();
+        if (loginUser != null && dto.getTenantId() == null) {
+            dto.setTenantId(loginUser.getTenantId());
+        }
         return Result.ok(pointsService.create(dto));
     }
 
@@ -61,6 +72,14 @@ public class PointsController {
 
     @PostMapping("/{id}/add")
     public Result<?> addPoints(@PathVariable Long id, @Valid @RequestBody PointsChangeDTO dto) {
+        // 验证租户归属
+        LoginUser loginUser = currentUser();
+        if (loginUser != null) {
+            PointsVO account = pointsService.getById(id);
+            if (!loginUser.getTenantId().equals(account.getTenantId())) {
+                throw new BusinessException(403, "无权操作其他租户的积分账户");
+            }
+        }
         return Result.ok(pointsService.addPoints(id,
                 dto.getPoints(),
                 dto.getEnterpriseId(),
@@ -72,6 +91,14 @@ public class PointsController {
 
     @PostMapping("/{id}/deduct")
     public Result<?> deductPoints(@PathVariable Long id, @Valid @RequestBody PointsChangeDTO dto) {
+        // 验证租户归属
+        LoginUser loginUser = currentUser();
+        if (loginUser != null) {
+            PointsVO account = pointsService.getById(id);
+            if (!loginUser.getTenantId().equals(account.getTenantId())) {
+                throw new BusinessException(403, "无权操作其他租户的积分账户");
+            }
+        }
         return Result.ok(pointsService.deductPoints(id,
                 dto.getPoints(),
                 dto.getEnterpriseId(),
@@ -104,4 +131,12 @@ public class PointsController {
         query.setBusinessType(businessType);
         return Result.ok(pointsService.getRecordPage(pageNo, pageSize, query));
     }
+
+    // ==================== 工具方法 ====================
+
+    private LoginUser currentUser() {
+        Authentication a = SecurityContextHolder.getContext().getAuthentication();
+        if (a != null && a.getPrincipal() instanceof LoginUser u) return u;
+        return null;
+    }
 }

+ 9 - 4
java/src/main/java/com/payment/platform/module/payment/points/dto/PointsCreateDTO.java

@@ -1,7 +1,8 @@
 package com.payment.platform.module.payment.points.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Digits;
+import jakarta.validation.constraints.NotNull;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -9,12 +10,16 @@ import java.math.BigDecimal;
 @Data
 public class PointsCreateDTO {
 
-    @NotBlank(message = "企业ID不能为空")
     @Schema(description = "企业ID")
     private String enterpriseId;
 
-    @Schema(description = "积分数额")
-    private BigDecimal points;
+    @NotNull(message = "租户ID不能为空")
+    @Schema(description = "租户ID")
+    private Long tenantId;
+
+    @Digits(integer = 10, fraction = 2)
+    @Schema(description = "积分数额", defaultValue = "0.00")
+    private BigDecimal points = BigDecimal.ZERO;
 
     @Schema(description = "规则")
     private String rule;

+ 3 - 0
java/src/main/java/com/payment/platform/module/payment/points/dto/PointsQueryDTO.java

@@ -11,4 +11,7 @@ public class PointsQueryDTO {
 
     @Schema(description = "状态")
     private String status;
+
+    @Schema(description = "租户ID(从安全上下文自动注入)")
+    private Long tenantId;
 }

+ 1 - 0
java/src/main/java/com/payment/platform/module/payment/points/service/PointsService.java

@@ -225,6 +225,7 @@ public class PointsService {
         LambdaQueryWrapper<PointsEntity> w = new LambdaQueryWrapper<>();
         if (q == null) return w;
         if (StrUtil.isNotBlank(q.getStatus())) w.eq(PointsEntity::getStatus, q.getStatus());
+        if (q.getTenantId() != null) w.eq(PointsEntity::getTenantId, q.getTenantId());
         return w;
     }
 

+ 1 - 6
java/src/main/java/com/payment/platform/module/system/auth/controller/AuthController.java

@@ -98,15 +98,10 @@ public class AuthController {
             return Result.fail(400, "验证码不能为空");
         }
 
-        // 验证短信验证码
-        if (!smsCodeService.verifyCode(templateName, mobile, code)) {
-            return Result.fail(400, "验证码已过期或错误");
-        }
-
         String ip = getClientIp(httpRequest);
         String userAgent = httpRequest.getHeader("User-Agent");
 
-        LoginResponse resp = authService.loginSms(mobile, ip, userAgent);
+        LoginResponse resp = authService.loginSms(mobile, code, ip, userAgent);
         log.info("用户{}短信登录成功", mobile);
         return Result.ok(resp);
     }

+ 12 - 0
java/src/main/java/com/payment/platform/module/system/auth/dto/LoginRequest.java

@@ -17,4 +17,16 @@ public class LoginRequest {
 
     @Schema(description = "登录类型", example = "mini")
     private String loginType;
+
+    @Schema(description = "验证码 key — 由 /captcha/get 返回")
+    private String captchaKey;
+
+    @Schema(description = "验证码值")
+    private String captchaCode;
+
+    @Schema(description = "手机号(短信登录使用)")
+    private String mobile;
+
+    @Schema(description = "短信验证码(短信登录使用)")
+    private String smsCode;
 }

+ 28 - 1
java/src/main/java/com/payment/platform/module/system/auth/service/AuthService.java

@@ -6,12 +6,15 @@ import cn.hutool.http.useragent.UserAgentUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.payment.platform.common.enums.RedisInitKeyConfig;
 import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.common.enums.ErrorCode;
 import com.payment.platform.common.utils.JwtUtils;
 import com.payment.platform.core.security.SessionInfo;
 import com.payment.platform.module.system.auth.dto.LoginRequest;
 import com.payment.platform.module.system.auth.dto.LoginResponse;
 import com.payment.platform.module.system.user.entity.UserEntity;
 import com.payment.platform.module.system.user.mapper.UserMapper;
+import com.payment.platform.module.payment.employee.entity.EmployeeEntity;
+import com.payment.platform.module.payment.employee.mapper.EmployeeMapper;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.data.redis.core.RedisTemplate;
@@ -32,6 +35,9 @@ public class AuthService {
     private final JwtUtils jwtUtils;
     private final PasswordEncoder passwordEncoder;
     private final RedisTemplate<String, Object> redisTemplate;
+    private final CaptchaService captchaService;
+    private final SmsCodeService smsCodeService;
+    private final EmployeeMapper employeeMapper;
 
     // ==================== 登录 ====================
 
@@ -41,6 +47,14 @@ public class AuthService {
     public LoginResponse login(LoginRequest request,
                                String ip,
                                String userAgentHeader) {
+        // 验证码验证 — 对应 Python check_captcha_service (L119-127)
+        if (!StrUtil.isBlank(request.getCaptchaKey())) {
+            String captchaResult = captchaService.verify(request.getCaptchaKey(), request.getCaptchaCode());
+            if (captchaResult != null) {
+                throw new BusinessException(ErrorCode.BAD_REQUEST.getCode(), captchaResult);
+            }
+        }
+
         UserEntity user = findUserByUsername(request.getUsername());
         if (user == null) {
             throw new BusinessException(400, "用户不存在");
@@ -99,8 +113,14 @@ public class AuthService {
 
     /**
      * 短信验证码登录 — 对应 Python authenticate_sms_user_service
+     * 含 SMS 验证码校验 + 员工激活状态检查
      */
-    public LoginResponse loginSms(String mobile, String ip, String userAgentHeader) {
+    public LoginResponse loginSms(String mobile, String smsCode, String ip, String userAgentHeader) {
+        // SMS 验证码校验 — 对应 Python 的 sms_code 验证
+        if (!smsCodeService.verifyCode("login", mobile, smsCode)) {
+            throw new BusinessException(ErrorCode.BAD_REQUEST.getCode(), "短信验证码错误或已过期");
+        }
+
         // 根据手机号查找用户
         UserEntity user = userMapper.selectOne(
                 new LambdaQueryWrapper<UserEntity>().eq(UserEntity::getMobile, mobile));
@@ -112,6 +132,13 @@ public class AuthService {
             throw new BusinessException(403, "用户已被停用");
         }
 
+        // 员工激活状态检查 — 对应 Python 的 employee 激活检查
+        EmployeeEntity employee = employeeMapper.selectOne(
+                new LambdaQueryWrapper<EmployeeEntity>().eq(EmployeeEntity::getEmployeeMobile, mobile));
+        if (employee != null && !"EMPLOYEE_ACTIVATED".equals(employee.getStatus())) {
+            throw new BusinessException(403, "员工未激活或被禁用,无法登录");
+        }
+
         // 更新最后登录时间(非阻塞)
         try {
             user.setLastLogin(OffsetDateTime.now());

+ 8 - 4
java/src/main/java/com/payment/platform/module/system/dept/controller/DeptController.java

@@ -1,7 +1,10 @@
 package com.payment.platform.module.system.dept.controller;
 
 import com.payment.platform.common.response.Result;
+import com.payment.platform.module.system.dept.dto.DeptCreateDTO;
+import com.payment.platform.module.system.dept.dto.DeptUpdateDTO;
 import com.payment.platform.module.system.dept.service.DeptService;
+import jakarta.validation.Valid;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 
@@ -28,13 +31,14 @@ public class DeptController {
     }
 
     @PostMapping("/create")
-    public Result<Map<String, Object>> create(@RequestBody Map<String, Object> body) {
-        return Result.ok(deptService.create(body));
+    public Result<Map<String, Object>> create(@Valid @RequestBody DeptCreateDTO dto) {
+        return Result.ok(deptService.createFromDTO(dto));
     }
 
     @PutMapping("/update/{id}")
-    public Result<Map<String, Object>> update(@PathVariable Long id, @RequestBody Map<String, Object> body) {
-        return Result.ok(deptService.update(id, body));
+    public Result<Map<String, Object>> update(@PathVariable Long id, @RequestBody DeptUpdateDTO dto) {
+        dto.setId(id);
+        return Result.ok(deptService.updateFromDTO(dto));
     }
 
     @DeleteMapping("/delete")

+ 48 - 0
java/src/main/java/com/payment/platform/module/system/dept/dto/DeptCreateDTO.java

@@ -0,0 +1,48 @@
+package com.payment.platform.module.system.dept.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
+import lombok.Data;
+
+@Data
+public class DeptCreateDTO {
+
+    @NotBlank(message = "部门名称不能为空")
+    @Size(max = 64, message = "部门名称最长64字符")
+    @Schema(description = "部门名称")
+    private String name;
+
+    @NotBlank(message = "部门编码不能为空")
+    @Size(max = 16, message = "部门编码最长16字符")
+    @Schema(description = "部门编码")
+    private String code;
+
+    @Min(value = 0, message = "排序最小为0")
+    @Schema(description = "显示排序", defaultValue = "1")
+    private Integer order = 1;
+
+    @Size(max = 32)
+    @Schema(description = "负责人")
+    private String leader;
+
+    @Size(max = 11)
+    @Schema(description = "联系电话")
+    private String phone;
+
+    @Size(max = 64)
+    @Schema(description = "邮箱")
+    private String email;
+
+    @Min(value = 0)
+    @Schema(description = "上级部门ID")
+    private Long parentId;
+
+    @Schema(description = "状态(0正常 1停用)", defaultValue = "0")
+    private String status = "0";
+
+    @Size(max = 255)
+    @Schema(description = "描述")
+    private String description;
+}

+ 40 - 0
java/src/main/java/com/payment/platform/module/system/dept/dto/DeptUpdateDTO.java

@@ -0,0 +1,40 @@
+package com.payment.platform.module.system.dept.dto;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Data
+public class DeptUpdateDTO {
+
+    @NotNull(message = "ID不能为空")
+    @Schema(description = "部门ID")
+    private Long id;
+
+    @Schema(description = "部门名称")
+    private String name;
+
+    @Schema(description = "部门编码")
+    private String code;
+
+    @Schema(description = "显示排序")
+    private Integer order;
+
+    @Schema(description = "负责人")
+    private String leader;
+
+    @Schema(description = "联系电话")
+    private String phone;
+
+    @Schema(description = "邮箱")
+    private String email;
+
+    @Schema(description = "上级部门ID")
+    private Long parentId;
+
+    @Schema(description = "状态(0正常 1停用)")
+    private String status;
+
+    @Schema(description = "描述")
+    private String description;
+}

+ 72 - 0
java/src/main/java/com/payment/platform/module/system/dept/service/DeptService.java

@@ -2,6 +2,8 @@ package com.payment.platform.module.system.dept.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.module.system.dept.dto.DeptCreateDTO;
+import com.payment.platform.module.system.dept.dto.DeptUpdateDTO;
 import com.payment.platform.module.system.dept.entity.DeptEntity;
 import com.payment.platform.module.system.dept.mapper.DeptMapper;
 import lombok.RequiredArgsConstructor;
@@ -58,6 +60,66 @@ public class DeptService {
         return toDict(e);
     }
 
+    /**
+     * 创建部门(DTO版本)
+     */
+    public Map<String, Object> createFromDTO(DeptCreateDTO dto) {
+        DeptEntity e = new DeptEntity();
+        e.setName(dto.getName());
+        e.setCode(dto.getCode());
+        e.setOrder(dto.getOrder());
+        e.setLeader(dto.getLeader());
+        e.setPhone(dto.getPhone());
+        e.setEmail(dto.getEmail());
+        e.setParentId(dto.getParentId());
+        e.setStatus(dto.getStatus());
+        e.setDescription(dto.getDescription());
+
+        String name = dto.getName();
+        String code = dto.getCode();
+        if (deptMapper.exists(new LambdaQueryWrapper<DeptEntity>().eq(DeptEntity::getName, name)))
+            throw new BusinessException(400, "创建失败,该部门已存在");
+        if (code != null && deptMapper.exists(new LambdaQueryWrapper<DeptEntity>().eq(DeptEntity::getCode, code)))
+            throw new BusinessException(400, "创建失败,编码已存在");
+
+        deptMapper.insert(e);
+        return toDict(e);
+    }
+
+    /**
+     * 更新部门(DTO版本)
+     */
+    public Map<String, Object> updateFromDTO(DeptUpdateDTO dto) {
+        DeptEntity dept = deptMapper.selectById(dto.getId());
+        if (dept == null) throw new BusinessException(404, "更新失败,该部门不存在");
+
+        if (dto.getName() != null) dept.setName(dto.getName());
+        if (dto.getCode() != null) dept.setCode(dto.getCode());
+        if (dto.getOrder() != null) dept.setOrder(dto.getOrder());
+        if (dto.getLeader() != null) dept.setLeader(dto.getLeader());
+        if (dto.getPhone() != null) dept.setPhone(dto.getPhone());
+        if (dto.getEmail() != null) dept.setEmail(dto.getEmail());
+        if (dto.getParentId() != null) dept.setParentId(dto.getParentId());
+        if (dto.getStatus() != null) dept.setStatus(dto.getStatus());
+        if (dto.getDescription() != null) dept.setDescription(dto.getDescription());
+
+        String name = dto.getName();
+        if (name != null && !name.equals(dept.getName())) {
+            DeptEntity exist = deptMapper.selectOne(new LambdaQueryWrapper<DeptEntity>().eq(DeptEntity::getName, name));
+            if (exist != null && !exist.getId().equals(dto.getId()))
+                throw new BusinessException(400, "更新失败,部门名称重复");
+        }
+        String code = dto.getCode();
+        if (code != null && !code.equals(dept.getCode())) {
+            DeptEntity exist = deptMapper.selectOne(new LambdaQueryWrapper<DeptEntity>().eq(DeptEntity::getCode, code));
+            if (exist != null && !exist.getId().equals(dto.getId()))
+                throw new BusinessException(400, "更新失败,部门编码已存在");
+        }
+
+        deptMapper.updateById(dept);
+        return toDict(deptMapper.selectById(dto.getId()));
+    }
+
     /**
      * 更新部门 — 对应 Python update_dept_service
      */
@@ -113,6 +175,16 @@ public class DeptService {
         }
     }
 
+    /**
+     * 递归获取部门及其所有子部门的 ID 列表 — 用于数据权限过滤
+     */
+    public List<Long> getDeptAndChildrenIds(Long deptId) {
+        if (deptId == null) return List.of();
+        List<DeptEntity> all = deptMapper.selectList(null);
+        Map<Long, List<Long>> childMap = buildChildIdMap(all);
+        return getChildRecursion(deptId, childMap);
+    }
+
     /**
      * 删除部门(含子部门) — 对应 Python delete_dept_service
      */

+ 1 - 1
java/src/main/java/com/payment/platform/module/system/dict/controller/DictController.java

@@ -57,7 +57,7 @@ public class DictController {
     }
 
     @PutMapping("/type/update/{id}")
-    public Result<DictTypeVO> updateType(@PathVariable Long id, @Valid @RequestBody DictTypeUpdateDTO dto) {
+    public Result<DictTypeVO> updateType(@PathVariable Long id, @RequestBody DictTypeUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(dictService.updateType(dto));
     }

+ 7 - 1
java/src/main/java/com/payment/platform/module/system/dict/dto/DictTypeCreateDTO.java

@@ -2,22 +2,28 @@ package com.payment.platform.module.system.dict.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
 public class DictTypeCreateDTO {
 
     @NotBlank(message = "字典名称不能为空")
+    @Size(max = 64, message = "字典名称最长64字符")
     @Schema(description = "字典名称")
     private String dictName;
 
     @NotBlank(message = "字典类型不能为空")
+    @Size(max = 64, message = "字典类型最长64字符")
+    @Pattern(regexp = "^[a-z][a-z0-9_]*$", message = "字典类型必须以小写字母开头,仅含小写字母/数字/下划线")
     @Schema(description = "字典类型", example = "sys_user_status")
     private String dictType;
 
     @Schema(description = "状态(0正常 1停用)", defaultValue = "0")
-    private String status;
+    private String status = "0";
 
+    @Size(max = 255)
     @Schema(description = "描述")
     private String description;
 }

+ 1 - 1
java/src/main/java/com/payment/platform/module/system/menu/controller/MenuController.java

@@ -37,7 +37,7 @@ public class MenuController {
     }
 
     @PutMapping("/update/{id}")
-    public Result<MenuVO> update(@PathVariable Long id, @Valid @RequestBody MenuUpdateDTO dto) {
+    public Result<MenuVO> update(@PathVariable Long id, @RequestBody MenuUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(menuService.updateMenu(id, dto));
     }

+ 20 - 1
java/src/main/java/com/payment/platform/module/system/menu/dto/MenuCreateDTO.java

@@ -1,36 +1,53 @@
 package com.payment.platform.module.system.menu.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Max;
+import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
 public class MenuCreateDTO {
 
+    @NotBlank(message = "菜单名称不能为空")
+    @Size(max = 50, message = "菜单名称最长50字符")
     @Schema(description = "菜单名称")
     private String name;
 
+    @NotNull(message = "菜单类型不能为空")
+    @Min(value = 1, message = "菜单类型范围1-4")
+    @Max(value = 4, message = "菜单类型范围1-4")
     @Schema(description = "菜单类型(1=目录 2=菜单 3=按钮 4=外链)")
     private Integer type;
 
+    @NotNull(message = "排序不能为空")
+    @Min(value = 1, message = "排序最小为1")
     @Schema(description = "排序")
     private Integer order;
 
+    @Size(max = 100)
     @Schema(description = "权限标识")
     private String permission;
 
+    @Size(max = 100)
     @Schema(description = "图标")
     private String icon;
 
+    @Size(max = 100)
     @Schema(description = "路由名称")
     private String routeName;
 
+    @Size(max = 200)
     @Schema(description = "路由路径")
     private String routePath;
 
+    @Size(max = 255)
     @Schema(description = "组件路径")
     private String componentPath;
 
+    @Size(max = 200)
     @Schema(description = "重定向")
     private String redirect;
 
@@ -43,7 +60,7 @@ public class MenuCreateDTO {
     @Schema(description = "是否总是显示")
     private Boolean alwaysShow;
 
-    @NotBlank(message = "菜单标题不能为空")
+    @Size(max = 50)
     @Schema(description = "菜单标题")
     private String title;
 
@@ -53,12 +70,14 @@ public class MenuCreateDTO {
     @Schema(description = "是否固定")
     private Boolean affix;
 
+    @Min(value = 1)
     @Schema(description = "父级菜单ID")
     private Long parentId;
 
     @Schema(description = "状态(0正常 1停用)", defaultValue = "0")
     private String status = "0";
 
+    @Size(max = 255)
     @Schema(description = "描述")
     private String description;
 }

+ 1 - 1
java/src/main/java/com/payment/platform/module/system/notice/controller/NoticeController.java

@@ -48,7 +48,7 @@ public class NoticeController {
     }
 
     @PutMapping("/update/{id}")
-    public Result<NoticeVO> update(@PathVariable Long id, @Valid @RequestBody NoticeUpdateDTO dto) {
+    public Result<NoticeVO> update(@PathVariable Long id, @RequestBody NoticeUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(noticeService.updateNotice(id, dto));
     }

+ 2 - 0
java/src/main/java/com/payment/platform/module/system/notice/dto/NoticeCreateDTO.java

@@ -3,12 +3,14 @@ package com.payment.platform.module.system.notice.dto;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
 public class NoticeCreateDTO {
 
     @NotBlank(message = "公告标题不能为空")
+    @Size(max = 50, message = "公告标题最长50字符")
     @Schema(description = "公告标题")
     private String noticeTitle;
 

+ 1 - 1
java/src/main/java/com/payment/platform/module/system/params/controller/ParamsController.java

@@ -75,7 +75,7 @@ public class ParamsController {
     }
 
     @PutMapping("/update/{id}")
-    public Result<ParamsVO> update(@PathVariable Long id, @Valid @RequestBody ParamsUpdateDTO dto) {
+    public Result<ParamsVO> update(@PathVariable Long id, @RequestBody ParamsUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(paramsService.update(dto));
     }

+ 2 - 0
java/src/main/java/com/payment/platform/module/system/params/dto/ParamsCreateDTO.java

@@ -3,12 +3,14 @@ package com.payment.platform.module.system.params.dto;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
 public class ParamsCreateDTO {
 
     @NotBlank(message = "参数名称不能为空")
+    @Size(max = 64, message = "参数名称最长64字符")
     @Schema(description = "参数名称", example = "演示模式")
     private String configName;
 

+ 1 - 1
java/src/main/java/com/payment/platform/module/system/position/controller/PositionController.java

@@ -38,7 +38,7 @@ public class PositionController {
     }
 
     @PutMapping("/update/{id}")
-    public Result<PositionVO> update(@PathVariable Long id, @Valid @RequestBody PositionUpdateDTO dto) {
+    public Result<PositionVO> update(@PathVariable Long id, @RequestBody PositionUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(positionService.update(dto));
     }

+ 7 - 2
java/src/main/java/com/payment/platform/module/system/position/dto/PositionCreateDTO.java

@@ -1,22 +1,27 @@
 package com.payment.platform.module.system.position.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
 public class PositionCreateDTO {
 
     @NotBlank(message = "岗位名称不能为空")
+    @Size(max = 64, message = "岗位名称最长64字符")
     @Schema(description = "岗位名称")
     private String name;
 
+    @Min(value = 1, message = "排序最小为1")
     @Schema(description = "显示排序", defaultValue = "1")
-    private Integer order;
+    private Integer order = 1;
 
     @Schema(description = "状态(0正常 1停用)", defaultValue = "0")
-    private String status;
+    private String status = "0";
 
+    @Size(max = 255)
     @Schema(description = "描述")
     private String description;
 }

+ 1 - 1
java/src/main/java/com/payment/platform/module/system/role/controller/RoleController.java

@@ -39,7 +39,7 @@ public class RoleController {
     }
 
     @PutMapping("/update/{id}")
-    public Result<RoleVO> update(@PathVariable Long id, @Valid @RequestBody RoleUpdateDTO dto) {
+    public Result<RoleVO> update(@PathVariable Long id, @RequestBody RoleUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(roleService.update(id, dto));
     }

+ 6 - 0
java/src/main/java/com/payment/platform/module/system/role/dto/RoleCreateDTO.java

@@ -1,20 +1,25 @@
 package com.payment.platform.module.system.role.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
 public class RoleCreateDTO {
 
     @NotBlank(message = "角色名称不能为空")
+    @Size(max = 64, message = "角色名称最长64字符")
     @Schema(description = "角色名称")
     private String name;
 
     @NotBlank(message = "角色编码不能为空")
+    @Size(max = 16, message = "角色编码最长16字符")
     @Schema(description = "角色编码")
     private String code;
 
+    @Min(value = 1, message = "排序最小为1")
     @Schema(description = "排序")
     private Integer order;
 
@@ -24,6 +29,7 @@ public class RoleCreateDTO {
     @Schema(description = "状态(0正常 1停用)", defaultValue = "0")
     private String status = "0";
 
+    @Size(max = 255)
     @Schema(description = "描述")
     private String description;
 }

+ 1 - 1
java/src/main/java/com/payment/platform/module/system/tenant/controller/TenantController.java

@@ -37,7 +37,7 @@ public class TenantController {
     }
 
     @PutMapping("/update/{id}")
-    public Result<TenantVO> update(@PathVariable Long id, @Valid @RequestBody TenantUpdateDTO dto) {
+    public Result<TenantVO> update(@PathVariable Long id, @RequestBody TenantUpdateDTO dto) {
         dto.setId(id);
         return Result.ok(tenantService.update(dto));
     }

+ 7 - 1
java/src/main/java/com/payment/platform/module/system/tenant/dto/TenantCreateDTO.java

@@ -2,6 +2,8 @@ package com.payment.platform.module.system.tenant.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 import java.time.OffsetDateTime;
@@ -10,16 +12,20 @@ import java.time.OffsetDateTime;
 public class TenantCreateDTO {
 
     @NotBlank(message = "租户名称不能为空")
+    @Size(max = 100, message = "租户名称最长100字符")
     @Schema(description = "租户名称")
     private String name;
 
     @NotBlank(message = "租户编码不能为空")
+    @Size(max = 100, message = "租户编码最长100字符")
+    @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "租户编码仅支持字母数字")
     @Schema(description = "租户编码(字母数字)")
     private String code;
 
     @Schema(description = "状态(0启用 1停用)", defaultValue = "0")
-    private String status;
+    private String status = "0";
 
+    @Size(max = 255)
     @Schema(description = "描述")
     private String description;
 

+ 3 - 0
java/src/main/java/com/payment/platform/module/system/user/dto/ChangePasswordDTO.java

@@ -2,16 +2,19 @@ package com.payment.platform.module.system.user.dto;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
 public class ChangePasswordDTO {
 
     @NotBlank(message = "原密码不能为空")
+    @Size(max = 128, message = "密码最长128字符")
     @Schema(description = "原密码")
     private String oldPwd;
 
     @NotBlank(message = "新密码不能为空")
+    @Size(min = 6, max = 128, message = "密码长度6-128字符")
     @Schema(description = "新密码")
     private String newPwd;
 }

+ 2 - 0
java/src/main/java/com/payment/platform/module/system/user/dto/ResetPasswordDTO.java

@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import io.swagger.v3.oas.annotations.media.Schema;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 
 @Data
@@ -14,6 +15,7 @@ public class ResetPasswordDTO {
     private Long id;
 
     @NotBlank(message = "新密码不能为空")
+    @Size(min = 6, max = 128, message = "密码长度6-128字符")
     @Schema(description = "新密码")
     @JsonProperty("password")
     private String newPwd;

+ 7 - 2
java/src/main/java/com/payment/platform/module/system/user/dto/UserCreateDTO.java

@@ -4,7 +4,7 @@ import com.payment.platform.common.dto.BaseCreateDTO;
 import com.payment.platform.common.validator.EmailValid;
 import com.payment.platform.common.validator.Phone;
 import io.swagger.v3.oas.annotations.media.Schema;
-import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Size;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
@@ -14,21 +14,25 @@ import java.util.List;
 @EqualsAndHashCode(callSuper = true)
 public class UserCreateDTO extends BaseCreateDTO {
 
-    @NotBlank(message = "用户名不能为空")
+    @Size(max = 32, message = "用户名最长32字符")
     @Schema(description = "用户名")
     private String username;
 
+    @Size(max = 128, message = "密码最长128字符")
     @Schema(description = "密码")
     private String password;
 
+    @Size(max = 32, message = "姓名最长32字符")
     @Schema(description = "姓名")
     private String name;
 
     @Phone
+    @Size(max = 11)
     @Schema(description = "手机号")
     private String mobile;
 
     @EmailValid
+    @Size(max = 64)
     @Schema(description = "邮箱")
     private String email;
 
@@ -47,6 +51,7 @@ public class UserCreateDTO extends BaseCreateDTO {
     @Schema(description = "状态(0正常 1停用)", defaultValue = "0")
     private String status = "0";
 
+    @Size(max = 255)
     @Schema(description = "描述")
     private String description;
 

+ 4 - 0
java/src/main/java/com/payment/platform/module/system/user/entity/UserEntity.java

@@ -32,6 +32,10 @@ public class UserEntity extends TenantBaseEntity {
     private OffsetDateTime lastLogin;
     private Long deptId;
 
+    /** 数据权限范围: 1=仅本人, 2=本部门, 3=本部门及以下, 4=全部, 5=自定义 */
+    @TableField(exist = false)
+    private Integer dataScope;
+
     // 第三方登录
     private String giteeLogin;
     private String githubLogin;

+ 1 - 1
java/src/main/resources/application.yml

@@ -50,7 +50,7 @@ mybatis-plus:
 
 # JWT
 jwt:
-  secret: vgb0tnl9d58+6n-6h-ea&u^1#0sccp!794=krylxcjq75vzps$
+  secret: vgb0tnl9d58+6n-6h-ea&u^1#1sccp!794=krylxcjq75vzps$
   algorithm: HmacSHA256
   access-token-expire: 3600
   refresh-token-expire: 7200