Просмотр исходного кода

修改redis冲突问题,实现app端登录,app医生端的添加,修改,重置密码功能

zhouzeyu 1 день назад
Родитель
Сommit
6cd97830c7
33 измененных файлов с 1144 добавлено и 155 удалено
  1. BIN
      logs/2026-01/hdis-2026-01-26-1.log.gz
  2. 8 4
      tr-framework/pom.xml
  3. 40 0
      tr-framework/src/main/java/cn/tr/core/annotation/Phone.java
  4. 43 12
      tr-framework/src/main/java/cn/tr/core/config/RedisConfig.java
  5. 9 6
      tr-framework/src/main/java/cn/tr/core/enums/GrantTypeEnum.java
  6. 1 0
      tr-modules-api/pom.xml
  7. 17 0
      tr-modules-api/tr-module-appDoctor-api/pom.xml
  8. 52 52
      tr-modules/tr-module-system/src/main/java/cn/tr/module/common/redis/RedisConfig.java
  9. 1 4
      tr-modules/tr-module-system/src/main/java/cn/tr/module/common/redis/RedissonClientAutoConfiguration.java
  10. 91 0
      tr-modules/tr-module-system/src/main/java/cn/tr/module/common/utils/PasswordErrorLimitUtil.java
  11. 1 1
      tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/oauth2/granter/TokenParameter.java
  12. 9 9
      tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/controller/SysRoleController.java
  13. 1 1
      tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/controller/SysUserController.java
  14. 10 2
      tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/dto/SysUserAddDTO.java
  15. 3 3
      tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/po/SysUserPO.java
  16. 0 9
      tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/service/impl/SysUserServiceImpl.java
  17. 101 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/auth/AppDoctorAuthGranter.java
  18. 99 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/controller/AppDoctorUserController.java
  19. 65 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/dto/AppDoctorUserAddDTO.java
  20. 54 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/dto/AppDoctorUserDTO.java
  21. 40 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/dto/AppDoctorUserQueryDTO.java
  22. 23 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/mapper/AppDoctorUserAddMapper.java
  23. 27 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/mapper/AppDoctorUserMapper.java
  24. 76 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/po/AppDoctorUserPO.java
  25. 15 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/repository/AppDoctorUserRepository.java
  26. 71 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/service/IAppDoctorUserService.java
  27. 188 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/service/impl/AppDoctorUserServiceImpl.java
  28. 1 8
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/controller/BusClinicController.java
  29. 29 10
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/po/BusHospitalPO.java
  30. 5 0
      tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/web/auth/WebAuthGranter.java
  31. 22 16
      tr-plugins/tr-spring-boot-starter-plugin-satoken/src/main/java/cn/tr/plugin/security/config/SaTokenRedisConfig.java
  32. 35 11
      tr-plugins/tr-spring-boot-starter-plugin-web/src/main/java/cn/tr/plugin/web/config/jackson/mapper/serializer/EnumDeserializer.java
  33. 7 7
      tr-test/.flattened-pom.xml

BIN
logs/2026-01/hdis-2026-01-26-1.log.gz


+ 8 - 4
tr-framework/pom.xml

@@ -38,10 +38,14 @@
             <artifactId>hutool-crypto</artifactId>
         </dependency>
 
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-data-redis</artifactId>
-            <scope>provided</scope>
+<!--        <dependency>-->
+<!--            <groupId>org.springframework.boot</groupId>-->
+<!--            <artifactId>spring-boot-starter-data-redis</artifactId>-->
+<!--            <scope>provided</scope>-->
+<!--        </dependency>-->
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
         </dependency>
         <!-- 参数校验 -->
         <dependency>

+ 40 - 0
tr-framework/src/main/java/cn/tr/core/annotation/Phone.java

@@ -0,0 +1,40 @@
+package cn.tr.core.annotation;
+
+import org.hibernate.validator.constraints.CompositionType;
+import org.hibernate.validator.constraints.ConstraintComposition;
+import org.hibernate.validator.constraints.Length;
+
+import jakarta.validation.Constraint;
+import jakarta.validation.Payload;
+import jakarta.validation.ReportAsSingleViolation;
+import jakarta.validation.constraints.Null;
+import jakarta.validation.constraints.Pattern;
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import static java.lang.annotation.ElementType.*;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+/**
+ * 验证手机号,空和正确的手机号都能验证通过<br/>
+ * 正确的手机号由11位数字组成,第一位为1
+ * 第二位为 3、4、5、7、8
+ * 
+ */
+@ConstraintComposition(CompositionType.OR)
+@Pattern(regexp = "^1\\d{10}$")
+@Null
+@Length(min = 0, max = 0)
+@Documented
+@Constraint(validatedBy = {})
+@Target({ METHOD, FIELD, CONSTRUCTOR, PARAMETER })
+@Retention(RUNTIME)
+@ReportAsSingleViolation
+public @interface Phone {
+    String message() default "手机号校验错误";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+}

+ 43 - 12
tr-framework/src/main/java/cn/tr/core/config/RedisConfig.java

@@ -1,22 +1,53 @@
 package cn.tr.core.config;
 
-import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
 import org.springframework.data.redis.connection.RedisConnectionFactory;
 import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
-@ConditionalOnBean(RedisConnectionFactory.class)
-public class RedisConfig {
+
+/**
+ * redis基础配置
+ *
+ * @author Kevin
+ */
+@Configuration
+@Slf4j
+@AllArgsConstructor
+public class RedisConfig  {
+
+    private  final ObjectMapper objectMapper;
+
     @Bean
-    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
-        RedisTemplate<String, Object> template = new RedisTemplate<>();
-        template.setConnectionFactory(connectionFactory);
-        template.setKeySerializer(new StringRedisSerializer());
-        template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
-        template.setHashKeySerializer(new StringRedisSerializer());
-        template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
-        return template;
+    @Primary
+    public RedisTemplate<String, Object> defaultRedisTemplate(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+
+        // Key/HashKey 用String序列化(关键修正)
+        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
+        // Value/HashValue 用Jackson序列化
+        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
+                new Jackson2JsonRedisSerializer<>(objectMapper, Object.class);
+
+        // 核心修正:区分Key和Value的序列化规则
+        redisTemplate.setKeySerializer(stringRedisSerializer);
+        redisTemplate.setHashKeySerializer(stringRedisSerializer);
+        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
+        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
+
+        redisTemplate.setDefaultSerializer(jackson2JsonRedisSerializer);
+        redisTemplate.setEnableDefaultSerializer(true);
+
+        redisTemplate.setConnectionFactory(connectionFactory);
+        // 必须调用afterPropertiesSet,否则序列化配置不生效
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
     }
 }

+ 9 - 6
tr-framework/src/main/java/cn/tr/core/enums/GrantTypeEnum.java

@@ -19,20 +19,23 @@ public enum GrantTypeEnum{
      * 网页端用户名密码模式
      */
     WEB_USERNAME_PASSWORD("1", "用户名密码模式"),
+
+
     /**
-     * 手机号短信模式
+     * app医生登录
      */
-    MOBILE_CODE("2", "手机号短信模式"),
-
+    APP_DOCTOR("2", "app医生登录"),
     /**
-     * appkey、appSecret模式
+     * 手机号短信模式
      */
-    APPKEY_APPSECRET("3", "第三方应用登录"),
+    MOBILE_CODE("3", "手机号短信模式"),
 
     /**
      * appkey、appSecret模式
      */
-    APP_DOCTOR("4", "app医生登录");
+    APPKEY_APPSECRET("4", "第三方应用登录");
+
+
 
 
     private String code;

+ 1 - 0
tr-modules-api/pom.xml

@@ -15,6 +15,7 @@
     <modules>
         <module>tr-module-system-api</module>
         <module>tr-module-export-api</module>
+        <module>tr-module-appDoctor-api</module>
 <!--        <module>cn-module-quartz-api</module>-->
     </modules>
 

+ 17 - 0
tr-modules-api/tr-module-appDoctor-api/pom.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <groupId>cn.tr</groupId>
+        <artifactId>tr-modules-api</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>tr-module-appDoctor-api</artifactId>
+    <version>${revision}</version>
+
+
+</project>

+ 52 - 52
tr-modules/tr-module-system/src/main/java/cn/tr/module/common/redis/RedisConfig.java

@@ -1,52 +1,52 @@
-package cn.tr.module.common.redis;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.AllArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.data.redis.connection.RedisConnectionFactory;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
-import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
-import org.springframework.data.redis.serializer.RedisSerializationContext;
-import org.springframework.data.redis.serializer.StringRedisSerializer;
-import org.springframework.data.redis.cache.RedisCacheConfiguration;
-
-
-/**
- * redis基础配置
- *
- * @author Kevin
- */
-@Configuration
-@Slf4j
-@AllArgsConstructor
-@ConditionalOnProperty(name = "spring.data.redis.host")
-public class RedisConfig  {
-
-
-    @Bean(name = "redisTemplate")
-    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
-//        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
-//
-//        // 设置序列化方式
-//        redisTemplate.setKeySerializer(new StringRedisSerializer());
-//        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
-//
-//        // 设置连接工厂
-//        redisTemplate.setConnectionFactory(connectionFactory);
-//
-//        return redisTemplate;
-
-        RedisTemplate<String, Object> template = new RedisTemplate<>();
-        template.setConnectionFactory(connectionFactory);
-
-        // 使用JSON序列化,避免循环引用
-        template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
-
-        return template;
-    }
-    
-}
+//package cn.tr.module.common.redis;
+//
+//import com.fasterxml.jackson.databind.ObjectMapper;
+//import lombok.AllArgsConstructor;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+//import org.springframework.context.annotation.Bean;
+//import org.springframework.context.annotation.Configuration;
+//import org.springframework.data.redis.connection.RedisConnectionFactory;
+//import org.springframework.data.redis.core.RedisTemplate;
+//import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
+//import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+//import org.springframework.data.redis.serializer.RedisSerializationContext;
+//import org.springframework.data.redis.serializer.StringRedisSerializer;
+//import org.springframework.data.redis.cache.RedisCacheConfiguration;
+//
+//
+///**
+// * redis基础配置
+// *
+// * @author Kevin
+// */
+//@Configuration
+//@Slf4j
+//@AllArgsConstructor
+//@ConditionalOnProperty(name = "spring.data.redis.host")
+//public class RedisConfig  {
+//
+//
+//    @Bean(name = "redisTemplate")
+//    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
+////        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+////
+////        // 设置序列化方式
+////        redisTemplate.setKeySerializer(new StringRedisSerializer());
+////        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
+////
+////        // 设置连接工厂
+////        redisTemplate.setConnectionFactory(connectionFactory);
+////
+////        return redisTemplate;
+//
+//        RedisTemplate<String, Object> template = new RedisTemplate<>();
+//        template.setConnectionFactory(connectionFactory);
+//
+//        // 使用JSON序列化,避免循环引用
+//        template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
+//
+//        return template;
+//    }
+//
+//}

+ 1 - 4
tr-modules/tr-module-system/src/main/java/cn/tr/module/common/redis/RedissonClientAutoConfiguration.java

@@ -5,6 +5,7 @@
 
 package cn.tr.module.common.redis;
 
+import cn.tr.core.config.RedisConfig;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.extern.slf4j.Slf4j;
 import org.redisson.Redisson;
@@ -49,10 +50,6 @@ public class RedissonClientAutoConfiguration {
     @Autowired
     private RedisProperties redisProperties;
 
-    // 注入Spring的ObjectMapper,统一序列化
-    @Autowired(required = false)
-    private ObjectMapper objectMapper;
-
     public RedissonClientAutoConfiguration() {
     }
 

+ 91 - 0
tr-modules/tr-module-system/src/main/java/cn/tr/module/common/utils/PasswordErrorLimitUtil.java

@@ -0,0 +1,91 @@
+package cn.tr.module.common.utils;
+
+import cn.hutool.core.util.StrUtil;
+import cn.tr.module.sys.exception.CustomException;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RAtomicLong;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * 基于 Redisson 的密码错误次数限制工具类
+ */
+@Component
+@Slf4j
+public class PasswordErrorLimitUtil {
+
+    private static final String PASSWORD_ERROR_PREFIX = "password_error:";
+    private static final int MAX_ERROR_COUNT = 5;
+    private static final int ERROR_WINDOW_MINUTES = 10;
+
+    @Resource
+    private RedissonClient redissonClient;
+
+    /**
+     * 记录密码错误次数
+     * @param username 用户名
+     */
+    public void recordPasswordError(String username) {
+        if (StrUtil.isBlank(username)) {
+            return;
+        }
+
+        String key = PASSWORD_ERROR_PREFIX + username;
+        RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
+
+        // 设置过期时间(10分钟)
+        atomicLong.expire(ERROR_WINDOW_MINUTES, java.util.concurrent.TimeUnit.MINUTES);
+
+        long currentCount = atomicLong.incrementAndGet();
+
+        if (currentCount >= MAX_ERROR_COUNT) {
+            throw new CustomException("密码错误次数过多,请10分钟后重试");
+        }
+    }
+
+    /**
+     * 检查密码错误次数是否超限
+     * @param username 用户名
+     * @return true表示未超限,false表示超限
+     */
+    public boolean isPasswordErrorCountExceeded(String username) {
+        if (StrUtil.isBlank(username)) {
+            return false;
+        }
+
+        String key = PASSWORD_ERROR_PREFIX + username;
+        RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
+        long currentCount = atomicLong.get();
+        return currentCount >= MAX_ERROR_COUNT;
+    }
+
+    /**
+     * 重置密码错误次数
+     * @param username 用户名
+     */
+    public void resetPasswordErrorCount(String username) {
+        if (StrUtil.isBlank(username)) {
+            return;
+        }
+
+        String key = PASSWORD_ERROR_PREFIX + username;
+        redissonClient.getAtomicLong(key).delete();
+    }
+
+    /**
+     * 获取当前密码错误次数
+     * @param username 用户名
+     * @return 错误次数
+     */
+    public int getCurrentPasswordErrorCount(String username) {
+        if (StrUtil.isBlank(username)) {
+            return 0;
+        }
+
+        String key = PASSWORD_ERROR_PREFIX + username;
+        RAtomicLong atomicLong = redissonClient.getAtomicLong(key);
+        return (int) atomicLong.get();
+    }
+}

+ 1 - 1
tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/oauth2/granter/TokenParameter.java

@@ -15,7 +15,7 @@ import java.io.Serializable;
 @Data
 public class TokenParameter implements Serializable {
 
-    @Schema(description = "授权方式 ",allowableValues = "1(web账密登录), 2 (手机短信登录),3(第三方应用登陆), 4(app医生登录), 5(疼痛小管家手机账密登录)6(疼痛小管家手机一键登录)")
+    @Schema(description = "授权方式 ",allowableValues = "1(web账密登录), 2(app医生登录)")
     GrantTypeEnum grantType;
 
     @Schema(description = "用户名 账密登录、app医生登录、疼痛小管家账密(此处用户名即为用户的手机号)登录时使用")

+ 9 - 9
tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/controller/SysRoleController.java

@@ -42,9 +42,9 @@ public class SysRoleController extends BaseController {
      * 分页查询
      */
     @Operation(summary = "角色管理分页查询")
-    @GetMapping("/page")
-    @SaCheckPermission("system:sysRole:page")
-    public TableDataInfo<SysRolePO> page( SysRoleQueryDTO req) {
+    @PostMapping("/page")
+    @SaCheckPermission("sys:role:page")
+    public TableDataInfo<SysRolePO> page(@RequestBody SysRoleQueryDTO req) {
         startPage();
         return getDataTable(sysRoleService.selectRoleList(req));
     }
@@ -54,7 +54,7 @@ public class SysRoleController extends BaseController {
      */
     @Operation(summary = "角色管理新增")
     @PostMapping("/add")
-    @SaCheckPermission("system:sysRole:add")
+    @SaCheckPermission("sys:role:add")
     public CommonResult add(@Validated @RequestBody SysRoleAddDTO req) {
         sysRoleService.add(req);
         return CommonResult.success();
@@ -65,7 +65,7 @@ public class SysRoleController extends BaseController {
      */
     @Operation(summary = "角色管理修改")
     @PostMapping("/edit")
-    @SaCheckPermission("system:sysRole:edit")
+    @SaCheckPermission("sys:role:edit")
     public CommonResult edit(@Validated @RequestBody SysRoleEditDTO req) {
         sysRoleService.edit(req);
         return CommonResult.success();
@@ -76,7 +76,7 @@ public class SysRoleController extends BaseController {
      */
     @Operation(summary = "角色管理删除")
     @PostMapping("/remove")
-    @SaCheckPermission("system:sysRole:remove")
+    @SaCheckPermission("sys:role:remove")
     public CommonResult remove(@RequestParam("ids") Long id) {
         sysRoleService.remove(id);
         return CommonResult.success();
@@ -87,7 +87,7 @@ public class SysRoleController extends BaseController {
      */
     @Operation(summary = "角色管理查看")
     @GetMapping("/view")
-    @SaCheckPermission("system:sysRole:view")
+    @SaCheckPermission("sys:role:view")
     public CommonResult view(@RequestParam String id) {
         return CommonResult.success(sysRoleService.view(id));
     }
@@ -97,7 +97,7 @@ public class SysRoleController extends BaseController {
      */
     @Operation(summary = "角色管理导出")
     @GetMapping("/export")
-    @SaCheckPermission("system:sysRole:export")
+    @SaCheckPermission("sys:role:export")
     public CommonResult export(SysRoleQueryDTO req) {
         String filepath = ExcelUtil.export("角色列表", SysRolePO.class, sysRoleService.list(req));
         return CommonResult.success(filepath);
@@ -117,7 +117,7 @@ public class SysRoleController extends BaseController {
      */
     @Operation(summary = "角色管理分配菜单")
     @PostMapping("/assignMenu")
-    @SaCheckPermission("system:sysRole:assignMenu")
+    @SaCheckPermission("sys:role:assignMenu")
     public CommonResult assignMenu(@Validated @RequestBody SysRoleAssignMenuDTO req) {
         sysRoleService.assignMenu(req);
         return CommonResult.success();

+ 1 - 1
tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/controller/SysUserController.java

@@ -45,7 +45,7 @@ public class SysUserController extends BaseController {
      * 分页查询
      */
     @Operation(summary = "用户管理分页查询")
-    @GetMapping("/page")
+    @PostMapping("/page")
     @SaCheckPermission("system:sysUser:page")
     public TableDataInfo<SysUserPO> page(SysUserQueryDTO req) {
         startPage();

+ 10 - 2
tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/dto/SysUserAddDTO.java

@@ -1,6 +1,7 @@
 package cn.tr.module.sys.user.dto;
 
 import cn.tr.module.common.menus.SexEnum;
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import lombok.Data;
 import org.springframework.format.annotation.DateTimeFormat;
 
@@ -97,7 +98,7 @@ public class SysUserAddDTO implements Serializable {
      * 性别 1男;2女;3未知
      */
     @NotNull(message = "性别不能为空")
-    private SexEnum sex;
+    private Integer sex;
 
     /**
      * 部门ID
@@ -134,5 +135,12 @@ public class SysUserAddDTO implements Serializable {
 
     private Boolean isSys;
 
-    private Long tenantId;
+    private String tenantId;
+
+    /**
+     * 删除标记 0存在;1删除
+     */
+    @TableLogic(value = "0",delval = "1")
+    private Integer delFlag;
+
 }

+ 3 - 3
tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/po/SysUserPO.java

@@ -144,14 +144,14 @@ public class SysUserPO extends TenantGenericEntity<Long,Long> {
      */
     @ExcelProperty(value = "状态", index = 12, converter = ExcelDictConverter.class)
     @ExcelDict("sys_status")
-    @TableField(typeHandler = IntegerStringTypeHandler.class)
-    private String status;
+//    @TableField(typeHandler = IntegerStringTypeHandler.class)
+    private Integer status;
 
     /**
      * 删除标记 0存在;1删除
      */
     @TableLogic(value = "0",delval = "1")
-    private String delFlag;
+    private Integer delFlag;
 
     /**
      * 创建人

+ 0 - 9
tr-modules/tr-module-system/src/main/java/cn/tr/module/sys/user/service/impl/SysUserServiceImpl.java

@@ -210,15 +210,6 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserRepository, SysUserPO
         if (!this.checkUniqueAccount(req.getAccount(), null)) {
             throw new CustomException("账号已存在");
         }
-//        if (!this.checkUniqueEmail(req.getEmail(), null)) {
-//            throw new CustomException("邮箱已存在");
-//        }
-//        if (!this.checkUniquePhone(req.getPhone(), null)) {
-//            throw new CustomException("手机号已存在");
-//        }
-//        if (!this.checkUniqueStaffNumber(req.getStaffNumber(), null)) {
-//            throw new CustomException("工号已存在");
-//        }
         LoginUser loginUser = SecurityUtil.getLoginUser();
         if(!Boolean.TRUE.equals(loginUser.isSys())&&Boolean.TRUE.equals(req.getIsSys())){
             throw new NotPermissionException("无权设置系统用户");

+ 101 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/auth/AppDoctorAuthGranter.java

@@ -0,0 +1,101 @@
+package cn.tr.module.phototherapy.app.auth;
+
+import cn.dev33.satoken.stp.StpLogic;
+import cn.hutool.core.util.StrUtil;
+import cn.tr.core.enums.GrantTypeEnum;
+import cn.tr.core.enums.StpTypeEnum;
+import cn.tr.core.pojo.LoginUser;
+import cn.tr.module.common.menus.StatusEnum;
+import cn.tr.module.common.menus.UserPlatformEnum;
+import cn.tr.module.common.utils.PasswordErrorLimitUtil;
+import cn.tr.module.phototherapy.app.po.AppDoctorUserPO;
+import cn.tr.module.phototherapy.app.service.IAppDoctorUserService;
+import cn.tr.module.phototherapy.app.service.impl.AppDoctorUserServiceImpl;
+import cn.tr.module.sys.exception.CustomException;
+import cn.tr.module.sys.oauth2.granter.IAuthGranter;
+import cn.tr.module.sys.oauth2.granter.TokenParameter;
+import cn.tr.module.sys.oauth2.utils.SecurityUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import jakarta.annotation.Resource;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+import static cn.tr.module.sys.oauth2.utils.SecurityUtil.LOGIN_USER_KEY;
+
+/**
+ * @author zzy
+ * @version 1.0
+ * @description: TODO
+ * @date 2026/1/26 9:49
+ */
+@Component
+@AllArgsConstructor
+@Slf4j
+public class AppDoctorAuthGranter implements IAuthGranter {
+    @Resource
+    private PasswordErrorLimitUtil passwordErrorLimitUtil;
+    @Resource
+    private AppDoctorUserServiceImpl appDoctorUserServiceImpl;
+
+    @Override
+    public GrantTypeEnum getType() {
+        return GrantTypeEnum.APP_DOCTOR;
+    }
+
+    @Override
+    public LoginUser grant(TokenParameter source) {
+        if (StrUtil.isBlank(source.getUsername())){
+            throw new CustomException("用户名不能为空");
+        }
+        if(StrUtil.isBlank(source.getPassword())){
+            throw new CustomException("密码不能为空");
+        }
+        // 检查密码错误次数是否超限
+        if (passwordErrorLimitUtil.isPasswordErrorCountExceeded(source.getUsername())) {
+            throw new CustomException("密码错误次数过多,请10分钟后重试");
+        }
+        AppDoctorUserPO appUser = appDoctorUserServiceImpl.getOne(Wrappers.lambdaQuery(AppDoctorUserPO.class)
+                .eq(AppDoctorUserPO::getUsername, source.getUsername())
+                .last("limit 1"));
+        if (Objects.isNull(appUser)){
+            log.info("登录用户:{}不存在", source.getUsername());
+            passwordErrorLimitUtil.recordPasswordError(source.getUsername());
+            throw new CustomException("账号或密码不正确");
+        }
+        if (!SecurityUtil.matchesPassword(source.getPassword(), appUser.getPassword())){
+            passwordErrorLimitUtil.recordPasswordError(source.getUsername());
+            throw new CustomException("账号或密码不正确");
+        }
+        if (StatusEnum.NO.equals(appUser.getStatus())){
+            log.info("登录用户:{}已被禁用", source.getUsername());
+            throw new CustomException("对不起,您的账号已被禁用");
+        }
+        log.info("登录用户:{}", source.getUsername());
+        StpLogic stpLogic = SecurityUtil.getStpLogic(StpTypeEnum.APP_DOCTOR.getText());
+        // 检查该用户是否已有登录态,如有则先注销旧token
+        if (stpLogic.isLogin(appUser.getId())) {
+            log.info("用户{}存在旧登录态,先注销旧token", source.getUsername());
+            stpLogic.logout(appUser.getId());
+        }
+
+        stpLogic.login(appUser.getId());
+        LoginUser<String> loginUser = new LoginUser<>();
+        loginUser.setToken(stpLogic.getTokenValue());
+        loginUser.setGrantType(source.getGrantType());
+        loginUser.setUserPlatform(UserPlatformEnum.APP_DOCTOR.getCode());
+        loginUser.setUsername(appUser.getUsername());
+        loginUser.setNickName(appUser.getRealName());
+        loginUser.setTenantId(appUser.getTenantId());
+        loginUser.setId(appUser.getId());
+        loginUser.setLoginType(StpTypeEnum.APP_DOCTOR.getText());
+        fillUserAgentInfo(loginUser);
+        //设置用户信息
+        stpLogic.getTokenSessionByToken(loginUser.getToken(),true).set(LOGIN_USER_KEY,loginUser);
+
+        return loginUser;
+    }
+
+}

+ 99 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/controller/AppDoctorUserController.java

@@ -0,0 +1,99 @@
+package cn.tr.module.phototherapy.app.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import cn.tr.core.validation.Insert;
+import cn.tr.core.validation.Update;
+import cn.tr.core.pojo.CommonResult;
+import cn.tr.module.common.annotation.Log;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserAddDTO;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserDTO;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserQueryDTO;
+import cn.tr.module.phototherapy.app.po.AppDoctorUserPO;
+import cn.tr.module.phototherapy.app.service.IAppDoctorUserService;
+import cn.tr.module.sys.exception.CustomException;
+import cn.tr.module.sys.oauth2.utils.SecurityUtil;
+import cn.tr.module.sys.user.dto.SysUserResetPwdDTO;
+import lombok.AllArgsConstructor;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.*;
+
+import cn.tr.core.context.BaseController;
+import org.springframework.web.bind.annotation.*;
+import cn.tr.core.pojo.TableDataInfo;
+
+/**
+ * app医生用户控制器
+ *
+ * @author 
+ * @date 2026-01-26
+ */
+@Tag(name = "app医生用户")
+@RestController
+@RequestMapping("/sys/doctor")
+@AllArgsConstructor
+public class AppDoctorUserController extends BaseController{
+
+    private final IAppDoctorUserService appDoctorUserService;
+
+    @Operation(summary = "根据条件查询app医生用户(分页)", description = "权限: 无")
+    @PostMapping("/query/page")
+    public TableDataInfo<AppDoctorUserDTO> selectPage(@RequestBody AppDoctorUserQueryDTO query) {
+        startPage();
+        return getDataTable(appDoctorUserService.selectAppDoctorUserList(query));
+    }
+
+    @Operation(summary = "根据条件查询app医生用户(不分页)", description = "权限: 无")
+    @PostMapping("/query/list")
+    public CommonResult<List<AppDoctorUserDTO>> selectList(@RequestBody AppDoctorUserQueryDTO query) {
+        return CommonResult.success(appDoctorUserService.selectAppDoctorUserList(query));
+    }
+
+    @Operation(summary = "根据id查询app医生用户", description = "权限: sys:doctor:query")
+    @GetMapping("/detail/{id}")
+    @SaCheckPermission("system:doctor:query")
+    public CommonResult<AppDoctorUserDTO> findById(@PathVariable("id") String id){
+        return CommonResult.success(appDoctorUserService.selectAppDoctorUserById(id));
+    }
+
+    @Operation(summary = "添加app医生用户", description = "权限: sys:doctor:add")
+    @PostMapping("/add")
+    @SaCheckPermission("system:doctor:add")
+    public CommonResult<Boolean> saveBatch(@RequestBody@Validated(Insert.class) List<AppDoctorUserPO> source) {
+        return CommonResult.success(appDoctorUserService.insertAppDoctorUser(source));
+    }
+
+    @Operation(summary = "编辑app医生信息", description = "权限: sys:doctor:edit")
+    @PostMapping("/edit")
+    @SaCheckPermission("sys:doctor:edit")
+    @Log(title = "编辑app医生信息")
+    public CommonResult<Boolean> update(@RequestBody@Validated(Update.class) AppDoctorUserPO source) {
+        return CommonResult.success(appDoctorUserService.updateAppDoctorUserById(source));
+    }
+
+    @Operation(summary = "删除app医生用户", description = "权限: sys:doctor:remove")
+    @PostMapping("/removeByIds")
+    @SaCheckPermission("system:doctor:remove")
+    public CommonResult<Boolean> delete(@RequestBody Collection<String> ids) {
+        return CommonResult.success(appDoctorUserService.removeAppDoctorUserByIds(ids));
+    }
+
+    @Operation(summary = "重置密码", description = "权限: sys:doctor:edit")
+    @PostMapping("/resetPassword")
+    @SaCheckPermission("system:doctor:edit")
+    public CommonResult<Boolean> resetPassword(@RequestBody@Validated SysUserResetPwdDTO<String> source) {
+        if (SecurityUtil.getId().equals(source.getId())) {
+            throw new CustomException("不可重置当前用户密码,请前往【账户设置】中修改");
+        }
+        appDoctorUserService.resetPassword(source.getId(),source.getPassword());
+        return CommonResult.success();
+    }
+
+
+}

+ 65 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/dto/AppDoctorUserAddDTO.java

@@ -0,0 +1,65 @@
+package cn.tr.module.phototherapy.app.dto;
+
+import cn.tr.core.pojo.BaseDTO;
+import cn.tr.module.common.mybatisplus.handler.TenantNameHandler;
+import com.baomidou.mybatisplus.annotation.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+
+/**
+ * app医生用户添加传输对象
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+@Data
+@Schema(description = "app医生用户添加传输对象")
+@EqualsAndHashCode(callSuper = true)
+@ToString
+public class AppDoctorUserAddDTO extends BaseDTO  {
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "主键")
+    @TableId
+    private String id;
+
+    @Schema(description = "用户名")
+    private String username;
+
+    @Schema(description = "密码")
+    private String password;
+
+    @Schema(description = "性别")
+    private String sex;
+
+    @Schema(description = "医生名称")
+    private String realName;
+
+    @Schema(description = "状态 0正常;1停用")
+    private String status;
+
+    @Schema(description = "用户头像")
+    private String avatar;
+
+    @TableField(fill = FieldFill.INSERT)
+    @Schema(hidden = true)
+    @TableLogic(value = "0",delval = "1")
+    private Integer isDelete;
+
+    @Schema(description = "是否修改0-否;1-是")
+    @TableField(fill = FieldFill.INSERT)
+    private Integer isEdit;
+
+    @Schema(description = "医院id")
+    private String tenantId;
+//
+//    @Getter
+//    @TableField(value = "tenant_id",insertStrategy = FieldStrategy.NEVER,updateStrategy = FieldStrategy.NEVER,typeHandler = TenantNameHandler.class)
+//    private String tenantName;
+
+    @Schema(description = "医生手机号")
+    private String doctorPhone;
+}

+ 54 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/dto/AppDoctorUserDTO.java

@@ -0,0 +1,54 @@
+package cn.tr.module.phototherapy.app.dto;
+
+import cn.tr.core.pojo.BaseDTO;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.*;
+
+/**
+ * app医生用户传输对象
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+@Data
+@Schema(description = "app医生用户传输对象")
+@EqualsAndHashCode(callSuper = true)
+@ToString
+public class AppDoctorUserDTO extends BaseDTO  {
+    private static final long serialVersionUID = 1L;
+
+
+    @Schema(description = "用户名")
+    private String username;
+
+    @Schema(description = "密码")
+    private String password;
+
+    @Schema(description = "性别")
+    private String sex;
+
+    @Schema(description = "医生名称")
+    private String realName;
+
+    @Schema(description = "用户头像")
+    private String avatar;
+
+    @Schema(description = "状态 0正常;1停用")
+    private String status;
+
+    @Schema(description = "是否修改0-否;1-是")
+    private Integer isEdit;
+
+    @Schema(description = "是否删除0-否;1-是")
+    private Integer isDelete;
+
+    @Schema(description = "医院id")
+    private String tenantId;
+
+    @Schema(description = "医生手机号")
+    private String doctorPhone;
+}

+ 40 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/dto/AppDoctorUserQueryDTO.java

@@ -0,0 +1,40 @@
+package cn.tr.module.phototherapy.app.dto;
+
+import lombok.ToString;
+import lombok.Data;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * app医生用户查询参数
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+@Data
+@Schema(description = "app医生用户查询参数")
+@ToString
+public class AppDoctorUserQueryDTO  implements Serializable{
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @Schema(description = "用户名")
+    private String username;
+
+    @Schema(description = "性别")
+    private String sex;
+
+    @Schema(description = "医生名称")
+    private String realName;
+
+    @Schema(description = "状态 0正常;1停用")
+    private String status;
+
+    @Schema(description = "医院id")
+    private String tenantId;
+
+    @Schema(description = "医生手机号")
+    private String doctorPhone;
+}

+ 23 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/mapper/AppDoctorUserAddMapper.java

@@ -0,0 +1,23 @@
+package cn.tr.module.phototherapy.app.mapper;
+
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserAddDTO;
+import cn.tr.module.phototherapy.app.po.AppDoctorUserPO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * app医生用户添加映射工具
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+@Mapper
+public interface AppDoctorUserAddMapper {
+    AppDoctorUserAddMapper INSTANCE = Mappers.getMapper(AppDoctorUserAddMapper.class);
+
+    AppDoctorUserPO convertPO(AppDoctorUserAddDTO source);
+
+    List<AppDoctorUserPO> convertPOList(List<AppDoctorUserAddDTO> source);
+}

+ 27 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/mapper/AppDoctorUserMapper.java

@@ -0,0 +1,27 @@
+package cn.tr.module.phototherapy.app.mapper;
+
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserDTO;
+import cn.tr.module.phototherapy.app.po.AppDoctorUserPO;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+
+/**
+ * app医生用户映射工具
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+@Mapper
+public interface AppDoctorUserMapper {
+    AppDoctorUserMapper INSTANCE = Mappers.getMapper(AppDoctorUserMapper.class);
+
+    AppDoctorUserPO convertPO(AppDoctorUserDTO source);
+
+    AppDoctorUserDTO convertDto(AppDoctorUserPO source);
+
+    List<AppDoctorUserDTO> convertDtoList(List<AppDoctorUserPO> source);
+
+    List<AppDoctorUserPO> convertPOList(List<AppDoctorUserDTO> source);
+}

+ 76 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/po/AppDoctorUserPO.java

@@ -0,0 +1,76 @@
+package cn.tr.module.phototherapy.app.po;
+
+import cn.tr.core.annotation.Phone;
+import cn.tr.module.common.entity.TenantGenericEntity;
+import cn.tr.module.common.menus.SexEnum;
+import cn.tr.module.common.menus.StatusEnum;
+import cn.tr.module.common.mybatisplus.handler.TenantNameHandler;
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import org.hibernate.validator.constraints.Length;
+
+/**
+ * @author zzy
+ * @version 1.0
+ * @description: TODO
+ * @date 2026/1/26 13:26
+ */
+@EqualsAndHashCode(callSuper = true)
+@Data
+@TableName(value = "app_doctor_user",autoResultMap = true)
+@Schema(description="app医生用户")
+@ToString
+@NoArgsConstructor
+public class AppDoctorUserPO extends TenantGenericEntity<String,String> {
+
+//    @Schema(description = "主键")
+//    private String id;
+
+    @Schema(description = "用户名,用户名不得超过32个字节")
+    @Length(max = 32,message = "用户名不得超过32个字节",groups = {Insert.class,Update.class})
+    @TableField(updateStrategy = FieldStrategy.NEVER)
+//    @Phone(groups = Insert.class)
+    private String username;
+
+    @Schema(description = "密码,密码不得超过32位")
+    @Length(max = 32,message = "密码不得超过32位",groups = {Insert.class,Update.class})
+    @JsonIgnoreProperties(allowSetters = true)
+    private String password;
+
+    @Schema(description = "性别 1、男 2、女 3、未知",allowableValues = "1,2,3")
+    private SexEnum sex;
+
+    @Schema(description = "医生名称,医生名称不得超过32个字节")
+    @Length(max = 32,message = "医生名称不得超过32个字节",groups = {Insert.class,Update.class})
+    private String realName;
+
+    @Schema(description = "头像")
+    private String avatar;
+
+    @Schema(description = "状态 0、正常 1、停用",allowableValues = "0,1")
+    private Integer status;
+
+    @Schema(description = "医生手机号")
+    @Phone(groups = Insert.class)
+    private String doctorPhone;
+
+    @Getter
+    @TableField(value = "tenant_id",insertStrategy = FieldStrategy.NEVER,updateStrategy = FieldStrategy.NEVER,typeHandler = TenantNameHandler.class)
+    private String tenantName;
+
+
+    @TableField(fill = FieldFill.INSERT)
+    @Schema(hidden = true)
+    @TableLogic(value = "0",delval = "1")
+    private Integer isDelete;
+
+    /**
+     * 是否可编辑
+     */
+    @TableField(fill = FieldFill.INSERT)
+    @Schema(hidden = true)
+    private Integer isEdit;
+
+}

+ 15 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/repository/AppDoctorUserRepository.java

@@ -0,0 +1,15 @@
+package cn.tr.module.phototherapy.app.repository;
+
+import cn.tr.module.phototherapy.app.po.AppDoctorUserPO;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * app医生用户Mapper接口
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+@Mapper
+public interface AppDoctorUserRepository extends BaseMapper<AppDoctorUserPO> {
+}

+ 71 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/service/IAppDoctorUserService.java

@@ -0,0 +1,71 @@
+package cn.tr.module.phototherapy.app.service;
+
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserAddDTO;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserDTO;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserQueryDTO;
+import cn.tr.module.phototherapy.app.po.AppDoctorUserPO;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+import java.util.*;
+
+/**
+ * app医生UserService接口
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+public interface IAppDoctorUserService{
+
+    /**
+     * 根据条件查询app医生用户
+     * @param    query 查询参数
+     * @author   
+     * @date     2026-01-26
+     */
+    List<AppDoctorUserDTO> selectAppDoctorUserList(AppDoctorUserQueryDTO query);
+
+    /**
+     * 根据id查询app医生用户
+     * @param    id 主键id
+     * @author   
+     * @date     2026-01-26
+     */
+    AppDoctorUserDTO selectAppDoctorUserById(String id);
+
+    /**
+     * 编辑app医生用户
+     * @param   source 编辑实体类
+     * @author  
+     * @date    2026-01-26
+     */
+    boolean updateAppDoctorUserById(AppDoctorUserPO source);
+
+    /**
+     * 新增app医生用户
+     * @param   source 新增实体类
+     * @author 
+     * @date 2026-01-26
+     */
+    boolean insertAppDoctorUser(List<AppDoctorUserPO> source);
+
+    /**
+     * 删除app医生用户详情
+     * @param  ids 删除主键集合
+     * @author 
+     * @date   2026-01-26
+     */
+    boolean removeAppDoctorUserByIds(Collection<String> ids);
+
+
+    /**
+     * 重置密码
+     * @param  id 主键
+     * @param  password 密码
+     * @author
+     * @date   2026-01-26
+     */
+    boolean resetPassword(String id, String password);
+
+}

+ 188 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/app/service/impl/AppDoctorUserServiceImpl.java

@@ -0,0 +1,188 @@
+package cn.tr.module.phototherapy.app.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserAddDTO;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserDTO;
+import cn.tr.module.phototherapy.app.dto.AppDoctorUserQueryDTO;
+import cn.tr.module.phototherapy.app.mapper.AppDoctorUserMapper;
+import cn.tr.module.phototherapy.app.po.AppDoctorUserPO;
+import cn.tr.module.phototherapy.app.repository.AppDoctorUserRepository;
+import cn.tr.module.phototherapy.app.service.IAppDoctorUserService;
+import cn.tr.module.phototherapy.common.po.BusClinicPO;
+import cn.tr.module.phototherapy.common.repository.BusClinicRepository;
+import cn.tr.module.sys.exception.CustomException;
+import cn.tr.module.sys.oauth2.utils.SecurityUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.enums.SqlMethod;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.checkerframework.checker.units.qual.A;
+import org.springframework.stereotype.Service;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.util.*;
+import org.springframework.transaction.annotation.Transactional;
+import cn.tr.core.exception.ServiceException;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import jakarta.annotation.Resource;
+import cn.tr.core.exception.TRExcCode;
+
+/**
+ * app医生UserService接口实现类
+ *
+ * @author 
+ * @date 2026-01-26
+ **/
+@Service
+public class AppDoctorUserServiceImpl  extends ServiceImpl<AppDoctorUserRepository, AppDoctorUserPO> implements IAppDoctorUserService {
+    @Resource
+    private AppDoctorUserRepository baseRepository;
+
+
+    /**
+    * 根据条件查询app医生用户
+    * @param    query 查询参数
+    * @author   
+    * @date     2026-01-26
+    */
+    @Override
+    public List<AppDoctorUserDTO> selectAppDoctorUserList(AppDoctorUserQueryDTO query){
+        return AppDoctorUserMapper.INSTANCE.convertDtoList(
+                baseRepository.selectList(new LambdaQueryWrapper<AppDoctorUserPO>()
+                    .like(Objects.nonNull(query.getUsername()),AppDoctorUserPO::getUsername, query.getUsername())
+                    .like(Objects.nonNull(query.getSex()),AppDoctorUserPO::getSex, query.getSex())
+                    .like(Objects.nonNull(query.getRealName()),AppDoctorUserPO::getRealName, query.getRealName())
+                    .eq(Objects.nonNull(query.getStatus()),AppDoctorUserPO::getStatus, query.getStatus())
+                    .eq(Objects.nonNull(query.getTenantId()),AppDoctorUserPO::getTenantId, query.getTenantId())
+                    .like(Objects.nonNull(query.getDoctorPhone()),AppDoctorUserPO::getDoctorPhone, query.getDoctorPhone())
+                )
+        );
+    };
+
+    /**
+    * 根据id查询app医生用户
+    * @param    id 主键id
+    * @author   
+    * @date     2026-01-26
+    */
+    @Override
+    public AppDoctorUserDTO selectAppDoctorUserById(String id){
+        return AppDoctorUserMapper.INSTANCE.convertDto(baseRepository.selectById(id));
+    };
+
+    /**
+    * 编辑app医生用户
+    * @param   source 编辑实体类
+    * @author  
+    * @date    2026-01-26
+    */
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public boolean updateAppDoctorUserById(AppDoctorUserPO source){
+        if(StrUtil.isNullOrUndefined(source.getId())){
+            throw new CustomException("用户id不能为空");
+        }
+        if(StrUtil.isNotBlank(source.getRealName())&&StrUtil.length(source.getRealName())>32){
+            throw new CustomException("医生名称不得超过32个字节");
+        }
+        return this.updateById(source);
+
+    };
+
+    /**
+    * 新增app医生用户
+    * @param   source 新增实体类
+    * @author 
+    * @date 2026-01-26
+    */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean insertAppDoctorUser(List<AppDoctorUserPO> source){
+//        return baseRepository.insert(AppDoctorUserMapper.INSTANCE.convertPO(source))!=0;
+        if (CollectionUtil.isEmpty(source)){
+            return true;
+        }
+        if (source.size() > 100){
+            throw new ServiceException(TRExcCode.SYSTEM_ERROR_B0001,"一次最多只能新增100条数据");
+        }
+        for (AppDoctorUserPO appDoctorUserAddPO : source) {
+            if (StrUtil.isEmpty(appDoctorUserAddPO.getRealName())){
+                throw new CustomException(String.format("用户名:{%s},姓名不能为空",appDoctorUserAddPO.getUsername()));
+            }
+            appDoctorUserAddPO.setIsEdit(0);
+        }
+        List<String> userNames = source.stream()
+                .peek(appDoctorUserAddPO ->{
+                    if (StrUtil.isEmpty(appDoctorUserAddPO.getPassword())){
+                        appDoctorUserAddPO.setPassword("123456");
+                    }
+                })
+                .peek(appDoctorUserAddPO -> {
+                    appDoctorUserAddPO.setPassword(SecurityUtil.encryptPassword(appDoctorUserAddPO.getPassword()));
+                })
+                .map(AppDoctorUserPO::getPassword).toList();
+
+        Set<String> userNameDistinct = new HashSet<>(userNames);
+        if(CollectionUtil.size(userNames)!=CollectionUtil.size(userNameDistinct)){
+            throw new CustomException("请检查新增的医生账户中是否存在重复用户名");
+        }
+        // 检查数据库中是否已存在相同用户名
+        List<AppDoctorUserPO> existUsers = baseRepository.selectList(
+                new QueryWrapper<AppDoctorUserPO>()
+                        .lambda()
+                        .select(AppDoctorUserPO::getUsername)
+                        .in(AppDoctorUserPO::getUsername, userNameDistinct)
+        );
+        if (CollectionUtil.isNotEmpty(existUsers)) {
+            Set<String> existUsername = existUsers.stream()
+                    .map(AppDoctorUserPO::getUsername)
+                    .collect(Collectors.toSet());
+            Collection<String> intersection = CollectionUtil.intersection(existUsername, userNameDistinct);
+            throw new CustomException(String.format("用户名%s已存在,不可重复添加", JSONUtil.toJsonStr(intersection)));
+        }
+        String sqlStatement = super.getSqlStatement(SqlMethod.INSERT_ONE);
+
+        return executeBatch(source, DEFAULT_BATCH_SIZE, (sqlSession, entity) -> sqlSession.insert(sqlStatement, entity));
+
+
+    };
+
+    /**
+    * 删除app医生用户详情
+    * @param  ids 删除主键集合
+    * @author 
+    * @date   2026-01-26
+    */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean removeAppDoctorUserByIds(Collection<String> ids){
+        if(CollectionUtil.isEmpty(ids)){
+            throw new ServiceException(TRExcCode.SYSTEM_ERROR_B0001,"请选择要删除的数据");
+        }
+        boolean result = this.removeBatchByIds(ids);
+        if (!result){
+            throw new ServiceException(TRExcCode.SYSTEM_ERROR_B0001,"删除失败");
+        }
+        SecurityUtil.getStpLogic().logout(ids);
+        return true;
+    };
+
+    /**
+    * 重置密码
+    * @param id
+    * @param password
+    * @return
+    */
+    @Override
+    public boolean resetPassword(String id, String password) {
+        if(StrUtil.isNullOrUndefined(id)){
+            throw new CustomException("用户id不能为空");
+        }
+        AppDoctorUserPO source = new AppDoctorUserPO();
+        source.setId(id);
+        // 密码加密
+        source.setPassword(SecurityUtil.encryptPassword(password));
+        return this.updateById(source);
+    }
+}

+ 1 - 8
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/controller/BusClinicController.java

@@ -36,7 +36,7 @@ public class BusClinicController extends BaseController{
 
     private final IBusClinicService busClinicService;
 
-    @Operation(summary = "根据条件查询临床表", description = "权限: 无")
+    @Operation(summary = "根据条件查询临床表(分页)", description = "权限: 无")
     @PostMapping("/query/page")
     public TableDataInfo<PatientTherapyRecordVO> selectList(@RequestBody BusPatientListQueryDTO query) {
         startPage();
@@ -50,13 +50,6 @@ public class BusClinicController extends BaseController{
         return CommonResult.success(busClinicService.selectBusClinicById(id));
     }
 
-//    @Operation(summary = "添加临床表", description = "权限: phototherapy:clinic:add")
-//    @PostMapping("/add")
-//    @SaCheckPermission("phototherapy:clinic:add")
-//    public CommonResult<Boolean> add(@RequestBody@Validated(Insert.class) BusClinicDTO source) {
-//        return CommonResult.success(busClinicService.insertBusClinic(source));
-//    }
-
     @Operation(summary = "新增患者",description = "权限: phototherapy:clinic:add")
     @PostMapping("/addPatient")
     @SaCheckPermission("phototherapy:clinic:add")

+ 29 - 10
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/common/po/BusHospitalPO.java

@@ -1,16 +1,17 @@
 package cn.tr.module.phototherapy.common.po;
 
 import cn.tr.core.pojo.TenantPO;
+import cn.tr.module.common.entity.RecordCreationEntity;
+import cn.tr.module.common.entity.RecordModifierEntity;
 import cn.tr.module.common.entity.TenantGenericEntity;
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableLogic;
-import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.ToString;
 
+import java.util.Date;
+
 /**
  * 医院信息表实体
  *
@@ -19,11 +20,11 @@ import lombok.ToString;
  **/
 @Data
 @TableName(value = "bus_hospital", autoResultMap = true)
-@EqualsAndHashCode(callSuper = true)
-@ToString
-public class BusHospitalPO extends TenantGenericEntity<String,String> {
+@Schema(description = "医院信息表")
+public class BusHospitalPO implements RecordModifierEntity, RecordCreationEntity {
 
     /** 医院名称 */
+    @TableId(type = IdType.ASSIGN_ID,value = "tenant_id")
     @Schema(description = "医院id")
     private String tenantId;
 
@@ -35,8 +36,26 @@ public class BusHospitalPO extends TenantGenericEntity<String,String> {
     @Schema(description = "logo文件")
     private String logoPath;
 
-    /** 删除标志(0代表存在 1代表删除) */
-    @TableLogic
-    @Schema(description = "删除标志(0代表存在 1代表删除)")
+    /** 创建者 */
+    @TableField(fill = FieldFill.INSERT)
+    private String createBy;
+
+    /** 更新者 */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updateBy;
+
+    @TableField(fill = FieldFill.INSERT)
+    private Date createTime;
+
+    @TableField(fill = FieldFill.UPDATE)
+    private Date updateTime;
+
+    @TableField(fill = FieldFill.INSERT)
+    @TableLogic(value = "0",delval = "1")
     private Integer isDelete;
+
+    @Override
+    public void setCreateTimeNow() {
+        RecordCreationEntity.super.setCreateTimeNow();
+    }
 }

+ 5 - 0
tr-modules/tr-modules-phototherapy/src/main/java/cn/tr/module/phototherapy/web/auth/WebAuthGranter.java

@@ -116,6 +116,11 @@ public class WebAuthGranter implements IAuthGranter {
 
         //登录
         StpLogic stpLogic = SecurityUtil.getStpLogic(StpTypeEnum.DEFAULT.getText());
+        // 检查该用户是否已有登录态,如有则先注销旧token
+        if (stpLogic.isLogin(sysUser.getId())) {
+            log.info("用户{}存在旧登录态,先注销旧token", source.getUsername());
+            stpLogic.logout(sysUser.getId());
+        }
         stpLogic.login(sysUser.getId());
         LoginUser loginUser = new LoginUser();
         //获取到token

+ 22 - 16
tr-plugins/tr-spring-boot-starter-plugin-satoken/src/main/java/cn/tr/plugin/security/config/SaTokenRedisConfig.java

@@ -49,14 +49,17 @@ public class SaTokenRedisConfig implements SaTokenDao {
     @Override
     public void update(String key, String value) {
         try {
-            // 获取当前过期时间
-            long currentTimeout = getTimeout(key);
-            if (currentTimeout > 0) {
-                // 如果原键有过期时间,则使用相同的过期时间
-                set(key, value, currentTimeout);
-            } else {
-                // 如果原键没有过期时间(永不过期),则设置为永不过期
-                set(key, value, 0);
+            // 获取当前过期时间 - 直接使用opsForValue().get()获取值而不是调用this.get()
+            Object currentValue = redisTemplate.opsForValue().get(key);
+            if(currentValue != null){
+                long currentTimeout = redisTemplate.getExpire(key, TimeUnit.SECONDS);
+                if (currentTimeout > 0) {
+                    // 如果原键有过期时间,则使用相同的过期时间
+                    set(key, value, currentTimeout);
+                } else {
+                    // 如果原键没有过期时间(永不过期),则设置为永不过期
+                    set(key, value, 0);
+                }
             }
         } catch (Exception e) {
             log.error("Update key error", e);
@@ -75,7 +78,8 @@ public class SaTokenRedisConfig implements SaTokenDao {
     @Override
     public long getTimeout(String key) {
         try {
-            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+            Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
+            return expire != null ? expire : 0;
         } catch (Exception e) {
             log.error("Get timeout error", e);
             return 0;
@@ -132,8 +136,8 @@ public class SaTokenRedisConfig implements SaTokenDao {
     @Override
     public void updateObject(String key, Object object) {
         try {
-            // 获取当前过期时间
-            long currentTimeout = getObjectTimeout(key);
+            // 获取当前过期时间 - 直接使用RedisTemplate API而不是调用this.getObjectTimeout()
+            long currentTimeout = redisTemplate.getExpire(key, TimeUnit.SECONDS);
             if (currentTimeout > 0) {
                 setObject(key, object, currentTimeout);
             } else {
@@ -165,7 +169,8 @@ public class SaTokenRedisConfig implements SaTokenDao {
     @Override
     public long getObjectTimeout(String key) {
         try {
-            return redisTemplate.getExpire(key, TimeUnit.SECONDS);
+            Long expire = redisTemplate.getExpire(key, TimeUnit.SECONDS);
+            return expire != null ? expire : 0;
         } catch (Exception e) {
             log.error("Get object timeout error", e);
             return 0;
@@ -199,8 +204,8 @@ public class SaTokenRedisConfig implements SaTokenDao {
     @Override
     public void updateSession(SaSession session) {
         try {
-            // 更新会话但保持原有过期时间
-            long currentTimeout = getTimeout(session.getId());
+            // 更新会话但保持原有过期时间 - 直接使用RedisTemplate API而不是调用this.getTimeout()
+            long currentTimeout = redisTemplate.getExpire(session.getId(), TimeUnit.SECONDS);
             if (currentTimeout > 0) {
                 setSession(session, currentTimeout);
             } else {
@@ -223,7 +228,8 @@ public class SaTokenRedisConfig implements SaTokenDao {
     @Override
     public long getSessionTimeout(String sessionId) {
         try {
-            return redisTemplate.getExpire(sessionId, TimeUnit.SECONDS);
+            Long expire = redisTemplate.getExpire(sessionId, TimeUnit.SECONDS);
+            return expire != null ? expire : 0;
         } catch (Exception e) {
             log.error("Get session timeout error", e);
             return 0;
@@ -268,4 +274,4 @@ public class SaTokenRedisConfig implements SaTokenDao {
             return List.of();
         }
     }
-}
+}

+ 35 - 11
tr-plugins/tr-spring-boot-starter-plugin-web/src/main/java/cn/tr/plugin/web/config/jackson/mapper/serializer/EnumDeserializer.java

@@ -26,21 +26,17 @@ public class EnumDeserializer extends JsonDeserializer<Enum<?>> implements Conte
         if (!StringUtils.hasText(jsonParser.getText())) {
             return null;
         }
+        // 保留原有IEnum接口的处理逻辑(兼容业务自定义枚举)
         if (IEnum.class.isAssignableFrom(target)) {
             IEnum anEnum = EnumConvertFactory.getEnum((Class) target, jsonParser.getText());
             if(anEnum!=null){
                 return (Enum<?>) anEnum;
             }
         }
-        return defaultEnumTransform(target,jsonParser.getText());
+        // 调用修改后的枚举转换方法(移除Hutool的likeValueOf)
+        return defaultEnumTransform(target, jsonParser.getText());
     }
 
-    /**
-     * @param ctx      ctx
-     * @param property property
-     * @return 1
-     * @throws JsonMappingException
-     */
     @Override
     public JsonDeserializer<?> createContextual(DeserializationContext ctx, BeanProperty property) throws JsonMappingException {
         Class<?> rawCls =  ctx.getContextualType().getRawClass();
@@ -49,16 +45,44 @@ public class EnumDeserializer extends JsonDeserializer<Enum<?>> implements Conte
         return enumDeserializer;
     }
 
+    /**
+     * 重构后的枚举转换方法:避免反射访问Enum私有字段
+     * 匹配规则:优先按枚举name()匹配,再按ordinal()数字匹配
+     * @param type 枚举类型
+     * @param input 前端传入的枚举值(字符串/数字)
+     * @return 匹配的枚举常量,无匹配则返回null
+     */
+    public static Enum<?> defaultEnumTransform(Class<?> type, String input) {
+        // 非枚举类型直接返回null
+        if (!type.isEnum()) {
+            return null;
+        }
 
-    public static Enum<?> defaultEnumTransform(Class<?> type, String indexString) {
         Enum<?>[] enumConstants = (Enum<?>[]) type.getEnumConstants();
-        if(enumConstants==null||enumConstants.length==0){
+        if (enumConstants == null || enumConstants.length == 0) {
             return null;
         }
+
+        // 1. 先尝试按枚举名称(name())精确匹配(字符串匹配)
+        for (Enum<?> enumConstant : enumConstants) {
+            if (enumConstant.name().equals(input)) {
+                return enumConstant;
+            }
+        }
+
+        // 2. 再尝试按枚举序号(ordinal())匹配(数字字符串转int匹配)
         try {
-            return EnumUtil.likeValueOf(enumConstants[0].getClass(),indexString);
+            int ordinal = Integer.parseInt(input);
+            for (Enum<?> enumConstant : enumConstants) {
+                if (enumConstant.ordinal() == ordinal) {
+                    return enumConstant;
+                }
+            }
         } catch (NumberFormatException e) {
-            return null;
+            // 不是数字,无需处理,直接返回null
         }
+
+        // 3. 无匹配项返回null
+        return null;
     }
 }

+ 7 - 7
tr-test/.flattened-pom.xml

@@ -20,16 +20,16 @@
       <artifactId>spring-boot-starter-data-redis</artifactId>
       <exclusions>
         <exclusion>
-          <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-logging</artifactId>
+          <groupId>org.springframework.boot</groupId>
         </exclusion>
         <exclusion>
-          <groupId>ch.qos.logback</groupId>
           <artifactId>logback-classic</artifactId>
+          <groupId>ch.qos.logback</groupId>
         </exclusion>
         <exclusion>
-          <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-to-slf4j</artifactId>
+          <groupId>org.apache.logging.log4j</groupId>
         </exclusion>
       </exclusions>
     </dependency>
@@ -38,16 +38,16 @@
       <artifactId>spring-boot-starter</artifactId>
       <exclusions>
         <exclusion>
-          <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-logging</artifactId>
+          <groupId>org.springframework.boot</groupId>
         </exclusion>
         <exclusion>
-          <groupId>ch.qos.logback</groupId>
           <artifactId>logback-classic</artifactId>
+          <groupId>ch.qos.logback</groupId>
         </exclusion>
         <exclusion>
-          <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-to-slf4j</artifactId>
+          <groupId>org.apache.logging.log4j</groupId>
         </exclusion>
       </exclusions>
     </dependency>
@@ -56,8 +56,8 @@
       <artifactId>spring-boot-starter-log4j2</artifactId>
       <exclusions>
         <exclusion>
-          <groupId>org.apache.logging.log4j</groupId>
           <artifactId>log4j-to-slf4j</artifactId>
+          <groupId>org.apache.logging.log4j</groupId>
         </exclusion>
       </exclusions>
     </dependency>