1
zj
2025-08-19 0b10f87268bd511c7d8dddad30060abefa49aab2
1
7 files modified
2 files renamed
13 files added
3 files deleted
955 ■■■■ changed files
pom.xml 1 ●●●● patch | view | raw | blame | history
ruoyi-admin/pom.xml 20 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java 1 ●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/ImApiController.java 35 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/comm/ImBusinessException.java 11 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/comm/ImConstant.java 15 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/comm/Result.java 2 ●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/config/AppAuthConfig.java 39 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/dto/RegisterDto.java 4 ●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/service/ImApiServcie.java 8 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/service/impl/ImApiServcieImpl.java 180 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/im/util/SymmetricCryptoUtil.java 116 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/ImApiController.java 31 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/entity/User.java 74 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/util/MD5Util.java 69 ●●●●● patch | view | raw | blame | history
ruoyi-admin/src/main/resources/application.yml 26 ●●●● patch | view | raw | blame | history
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java 106 ●●●● patch | view | raw | blame | history
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyMetaObjectHandler.java 37 ●●●●● patch | view | raw | blame | history
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusConfig.java 36 ●●●●● patch | view | raw | blame | history
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java 1 ●●●● patch | view | raw | blame | history
ruoyi-system/pom.xml 25 ●●●●● patch | view | raw | blame | history
ruoyi-system/src/main/java/com/ruoyi/system/domain/UserAccount.java 81 ●●●●● patch | view | raw | blame | history
ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserAccountMapper.java 14 ●●●●● patch | view | raw | blame | history
ruoyi-system/src/main/java/com/ruoyi/system/service/UserAccountService.java 7 ●●●●● patch | view | raw | blame | history
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserAccountServiceImpl.java 16 ●●●●● patch | view | raw | blame | history
pom.xml
@@ -217,7 +217,6 @@
                <artifactId>ruoyi-common</artifactId>
                <version>${ruoyi.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
ruoyi-admin/pom.xml
@@ -64,6 +64,26 @@
            <artifactId>ruoyi-generator</artifactId>
        </dependency>
        <dependency>
            <groupId>com.netease.nim</groupId>
            <artifactId>yunxin-server-sdk</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version> <!-- 使用最新版本 -->
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.12</version>
        </dependency>
    </dependencies>
    <build>
ruoyi-admin/src/main/java/com/ruoyi/RuoYiApplication.java
@@ -1,5 +1,6 @@
package com.ruoyi;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
ruoyi-admin/src/main/java/com/ruoyi/im/ImApiController.java
New file
@@ -0,0 +1,35 @@
package com.ruoyi.im;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.im.comm.Result;
import com.ruoyi.im.service.ImApiServcie;
import com.ruoyi.im.dto.RegisterDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/im/api")
@Slf4j
public class ImApiController {
    @Autowired
    private ImApiServcie imApiServcie;
    /**
     * 获取参数配置列表
     */
    @PostMapping("/register")
    public Result register(@Validated RegisterDto dto){
        try {
            return imApiServcie.register(dto);
        }catch (Exception e){
            log.error("注册报错:",e);
            return Result.error("注册失败,请稍后再试!");
        }
    }
}
ruoyi-admin/src/main/java/com/ruoyi/im/comm/ImBusinessException.java
New file
@@ -0,0 +1,11 @@
package com.ruoyi.im.comm;
public class ImBusinessException extends RuntimeException {
    public ImBusinessException(String message) {
        super(message);
    }
    public ImBusinessException(String message, Throwable cause) {
        super(message, cause);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/im/comm/ImConstant.java
New file
@@ -0,0 +1,15 @@
package com.ruoyi.im.comm;
public class ImConstant {
    // 云信API路径
    public static final String YUNXIN_CREATE_USER_PATH = "/user/create.action";
    // 云信响应码
    public static final int YUNXIN_SUCCESS_CODE = 200;
    public static final int YUNXIN_ACCOUNT_EXISTS_CODE = 414;
    // 业务常量
    public static final int MAX_GENERATE_ACCOUNT_ATTEMPTS = 10;
    public static final int MIN_GENERATE_DELAY_MS = 5;
    public static final int MAX_GENERATE_DELAY_MS = 30;
}
ruoyi-admin/src/main/java/com/ruoyi/im/comm/Result.java
File was renamed from ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/comm/Result.java
@@ -1,4 +1,4 @@
package com.ruoyi.web.controller.api.comm;
package com.ruoyi.im.comm;
import java.io.Serializable;
ruoyi-admin/src/main/java/com/ruoyi/im/config/AppAuthConfig.java
New file
@@ -0,0 +1,39 @@
package com.ruoyi.im.config;
public enum AppAuthConfig {
    /**
     * 应用认证配置
     */
    DEFAULT_CONFIG("cd693c66bb6992a7cc2b13379e981d48", "720c0b98f7a3");
    private final String appKey;
    private final String appSecret;
    AppAuthConfig(String appKey, String appSecret) {
        this.appKey = appKey;
        this.appSecret = appSecret;
    }
    public String getAppKey() {
        return appKey;
    }
    public String getAppSecret() {
        return appSecret;
    }
    /**
     * 根据key查找配置
     * @param key 应用key
     * @return 对应的配置,找不到返回null
     */
    public static AppAuthConfig getByKey(String key) {
        for (AppAuthConfig config : values()) {
            if (config.getAppKey().equals(key)) {
                return config;
            }
        }
        return null;
    }
}
ruoyi-admin/src/main/java/com/ruoyi/im/dto/RegisterDto.java
File was renamed from ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/dto/RegisterDto.java
@@ -1,4 +1,4 @@
package com.ruoyi.web.controller.api.dto;
package com.ruoyi.im.dto;
import lombok.Data;
@@ -9,7 +9,7 @@
public class RegisterDto {
    @NotNull(message = "账号不能为空")
    private Integer account; // 账号
    private String account; // 账号
    @NotEmpty(message = "密码不能为空")
    private String password; // 密码
ruoyi-admin/src/main/java/com/ruoyi/im/service/ImApiServcie.java
New file
@@ -0,0 +1,8 @@
package com.ruoyi.im.service;
import com.ruoyi.im.comm.Result;
import com.ruoyi.im.dto.RegisterDto;
public interface ImApiServcie {
    Result register(RegisterDto dto);
}
ruoyi-admin/src/main/java/com/ruoyi/im/service/impl/ImApiServcieImpl.java
New file
@@ -0,0 +1,180 @@
package com.ruoyi.im.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.netease.nim.server.sdk.core.BizName;
import com.netease.nim.server.sdk.core.YunxinApiHttpClient;
import com.netease.nim.server.sdk.core.YunxinApiResponse;
import com.netease.nim.server.sdk.core.exception.YunxinSdkException;
import com.ruoyi.im.comm.Result;
import com.ruoyi.im.config.AppAuthConfig;
import com.ruoyi.system.domain.UserAccount;
import com.ruoyi.im.service.ImApiServcie;
import com.ruoyi.im.dto.RegisterDto;
import com.ruoyi.system.service.UserAccountService;
import com.ruoyi.im.util.SymmetricCryptoUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.PostConstruct;
import java.time.Instant;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicLong;
@Service
public class ImApiServcieImpl implements ImApiServcie {
    @Autowired
    private UserAccountService userAccountService;
    // 使用单例客户端,避免重复初始化
    private YunxinApiHttpClient yunxinClient;
    // 使用并发安全的集合存储正在生成的账号,防止重复
    private final Set<Long> generatingAccounts = Collections.newSetFromMap(new ConcurrentHashMap<>());
    // 使用原子计数器优化账号生成
    private final AtomicLong lastTimestamp = new AtomicLong(0);
    private final AtomicLong sequence = new AtomicLong(0);
    @PostConstruct
    public void init() {
        // 初始化云信客户端,只执行一次
        String appkey = AppAuthConfig.DEFAULT_CONFIG.getAppKey();
        String appsecret = AppAuthConfig.DEFAULT_CONFIG.getAppSecret();
        int timeoutMillis = 5000;
        this.yunxinClient = new YunxinApiHttpClient.Builder(BizName.IM, appkey, appsecret)
                .timeoutMillis(timeoutMillis)
                .build();
    }
    @Override
    public Result register(RegisterDto dto) {
        // 短信验证 TODO
        // 手机号是否存在 - 添加分布式锁或数据库唯一索引确保并发安全
        List<UserAccount> accounts = userAccountService.list(
                new LambdaQueryWrapper<>(UserAccount.class)
                        .eq(UserAccount::getAccount, dto.getAccount())
        );
        if(!CollectionUtils.isEmpty(accounts)){
            return Result.error("手机号已注册!");
        }
//        // 生成云信账号
//        long cloudMessageAccount;
//        try {
//            cloudMessageAccount = generateUniqueCloudMessageAccount();
//        } catch (RuntimeException e) {
//            return Result.error("系统繁忙,请稍后重试");
//        }
        // 注册云信
        String path = "/user/create.action";
        Map<String, String> paramMap = new HashMap<>();
        paramMap.put("accid", String.valueOf(dto.getAccount()));
        paramMap.put("token", dto.getPassword());
        YunxinApiResponse response;
        try {
            response = yunxinClient.executeV1Api(path, paramMap);
        } catch (YunxinSdkException e) {
            System.err.println("register error, traceId = " + e.getTraceId());
            return Result.error("注册失败,系统异常");
        }
        // 获取结果
        String data = response.getData();
        JSONObject json = JSONObject.parseObject(data);
        int code = json.getIntValue("code");
        if (code == 200) {
            // 注册成功
            UserAccount userAccount = new UserAccount();
            userAccount.setAccount(dto.getAccount());
            userAccount.setPhoneNumber(dto.getAccount());
            userAccount.setCloudMessageAccount(dto.getAccount());
            userAccount.setPassword(SymmetricCryptoUtil.encryptPassword(dto.getPassword()));
            try {
                userAccountService.save(userAccount);
                return Result.success("注册成功");
            } catch (Exception e) {
                // 处理数据库插入异常,可能需要回滚云信账号
                return Result.error("注册失败,请重试");
            }
        } else if (code == 414) {
            return Result.error("账号已被注册!");
        } else {
            System.err.println("register fail, response = " + data + ", traceId=" + response.getTraceId());
            return Result.error("注册失败,错误码: " + code);
        }
    }
    /**
     * 优化的账号生成方法,使用雪花算法变体提高并发性能
     */
    public long generateUniqueCloudMessageAccount() {
        final int maxAttempts = 10; // 减少尝试次数,提高效率
        for (int attempt = 0; attempt < maxAttempts; attempt++) {
            long account = generateSnowflakeLikeId();
            // 使用并发集合检查是否正在生成相同账号
            if (generatingAccounts.add(account)) {
                try {
                    // 检查数据库是否存在
                    boolean exists = userAccountService.lambdaQuery()
                            .eq(UserAccount::getCloudMessageAccount, account)
                            .exists();
                    if (!exists) {
                        return account;
                    }
                } finally {
                    // 无论成功与否,都从集合中移除
                    generatingAccounts.remove(account);
                }
            }
            // 短暂等待后重试
            try {
                Thread.sleep(ThreadLocalRandom.current().nextInt(10, 50));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new RuntimeException("生成账号被中断", e);
            }
        }
        throw new RuntimeException("无法在" + maxAttempts + "次尝试内生成唯一账号");
    }
    /**
     * 基于雪花算法的ID生成器,提高并发性能
     */
    private long generateSnowflakeLikeId() {
        long currentTime = System.currentTimeMillis();
        long timestamp = currentTime % 100_000_000L; // 取时间戳的后8位
        // 使用原子操作确保序列号递增
        long seq;
        long lastTime;
        do {
            lastTime = lastTimestamp.get();
            if (currentTime == lastTime) {
                seq = sequence.incrementAndGet() % 100;
            } else {
                sequence.set(0);
                lastTimestamp.set(currentTime);
                seq = 0;
            }
        } while (!lastTimestamp.compareAndSet(lastTime, currentTime));
        // 组合时间戳和序列号,生成9位ID
        return timestamp * 100 + seq;
    }
}
ruoyi-admin/src/main/java/com/ruoyi/im/util/SymmetricCryptoUtil.java
New file
@@ -0,0 +1,116 @@
package com.ruoyi.im.util;
import cn.hutool.crypto.Mode;
import cn.hutool.crypto.Padding;
import cn.hutool.crypto.symmetric.AES;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
/**
 * @ClassName : SymmetricCryptoUtil  //类名
 * @Description : 对称加密工具类  //描述
 * @Author :  tf //作者
 * @Date: 2022/10/23  23:31
 */
public class SymmetricCryptoUtil {
    /**
     * 16字节的密钥(128位)
     */
    private static final String ENCODE_KEY = "03SsJLwB03SsJLwB"; // 改为16字节
    /**
     * 16字节的初始化向量(128位)
     */
    private static final String IV_KEY = "0000000000000000"; // 改为16个0
    public static void main(String[] args) {
        String originalText = "zdm321123.";
        String encryptData = encryptFromString(originalText, Mode.CBC, Padding.ZeroPadding);
        System.out.println("加密:" + encryptData);
        String decryptData = decryptFromString(encryptData, Mode.CBC, Padding.ZeroPadding);
        System.out.println("解密:" + decryptData);
        // 验证加解密是否一致
        System.out.println("验证结果:" + originalText.equals(decryptData));
    }
    /**
     * 加密字符串
     */
    public static String encryptFromString(String data, Mode mode, Padding padding) {
        AES aes;
        if (Mode.CBC == mode) {
            aes = new AES(mode, padding,
                    new SecretKeySpec(ENCODE_KEY.getBytes(StandardCharsets.UTF_8), "AES"),
                    new IvParameterSpec(IV_KEY.getBytes(StandardCharsets.UTF_8)));
        } else {
            aes = new AES(mode, padding,
                    new SecretKeySpec(ENCODE_KEY.getBytes(StandardCharsets.UTF_8), "AES"));
        }
        return aes.encryptBase64(data, StandardCharsets.UTF_8);
    }
    /**
     * 解密字符串
     */
    public static String decryptFromString(String data, Mode mode, Padding padding) {
        AES aes;
        if (Mode.CBC == mode) {
            aes = new AES(mode, padding,
                    new SecretKeySpec(ENCODE_KEY.getBytes(StandardCharsets.UTF_8), "AES"),
                    new IvParameterSpec(IV_KEY.getBytes(StandardCharsets.UTF_8)));
        } else {
            aes = new AES(mode, padding,
                    new SecretKeySpec(ENCODE_KEY.getBytes(StandardCharsets.UTF_8), "AES"));
        }
        byte[] decryptDataBase64 = aes.decrypt(data);
        return new String(decryptDataBase64, StandardCharsets.UTF_8);
    }
    /**
     * @Description:  加密密码(使用CBC模式和ZeroPadding)
     * @Param:  password 明文密码
     * @return:  加密后的Base64字符串
     * @Author: tf
     * @Date: 2022/10/23
     */
    public static String encryptPassword(String password) {
        return encryptFromString(password, Mode.CBC, Padding.ZeroPadding);
    }
    /**
     * @Description:  解密密码
     * @Param:  encryptedPassword 加密后的密码字符串
     * @return:  解密后的明文密码
     * @Author: tf
     * @Date: 2022/10/23
     */
    public static String decryptPassword(String encryptedPassword) {
        return decryptFromString(encryptedPassword, Mode.CBC, Padding.ZeroPadding);
    }
    /**
     * 生成随机的16字节密钥(用于生产环境)
     */
    public static String generateRandomKey() {
        java.security.SecureRandom random = new java.security.SecureRandom();
        byte[] key = new byte[16];
        random.nextBytes(key);
        return new String(key, StandardCharsets.UTF_8);
    }
    /**
     * 生成随机的16字节IV(用于生产环境)
     */
    public static String generateRandomIV() {
        java.security.SecureRandom random = new java.security.SecureRandom();
        byte[] iv = new byte[16];
        random.nextBytes(iv);
        return new String(iv, StandardCharsets.UTF_8);
    }
}
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/ImApiController.java
File was deleted
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/entity/User.java
File was deleted
ruoyi-admin/src/main/java/com/ruoyi/web/controller/api/util/MD5Util.java
File was deleted
ruoyi-admin/src/main/resources/application.yml
@@ -96,16 +96,30 @@
  secret: abcdefghijklmnopqrstuvwxyz
  # 令牌有效期(默认30分钟)
  expireTime: 30
# MyBatis配置
# 原有 MyBatis 配置保持不变
mybatis:
  # 搜索指定包别名
  typeAliasesPackage: com.ruoyi.**.domain
  # 配置mapper的扫描,找到所有的mapper.xml映射文件
  mapperLocations: classpath*:mapper/**/*Mapper.xml
  # 加载全局的配置文件
  configLocation: classpath:mybatis/mybatis-config.xml
# 新增 MyBatis-Plus 配置
mybatis-plus:
  # 可以指向新的目录,避免与原有XML冲突
  mapper-locations: classpath*:mapper/**/*Mapper.xml
  type-aliases-package: com.ruoyi.**.domain
  global-config:
    db-config:
      id-type: AUTO
      logic-delete-value: 2  # 若依默认逻辑删除值是2
      logic-not-delete-value: 0
      # 字段策略:0:忽略判断,1:非NULL判断,2:非空判断
      insert-strategy: not_null
      update-strategy: not_null
  configuration:
    # 保持与原有配置一致
    map-underscore-to-camel-case: true
    # 开启SQL日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# PageHelper分页插件
pagehelper:
  helperDialect: mysql
@@ -126,4 +140,4 @@
  # 排除链接(多个用逗号分隔)
  excludes: /system/notice
  # 匹配链接
  urlPatterns: /system/*,/monitor/*,/tool/*
  urlPatterns: /system/*,/monitor/*,/tool/*
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyBatisConfig.java
@@ -6,6 +6,8 @@
import java.util.HashSet;
import java.util.List;
import javax.sql.DataSource;
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
import org.apache.ibatis.io.VFS;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
@@ -32,101 +34,19 @@
@Configuration
public class MyBatisConfig
{
    @Autowired
    private Environment env;
    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
    public static String setTypeAliasesPackage(String typeAliasesPackage)
    {
        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
        List<String> allResult = new ArrayList<String>();
        try
        {
            for (String aliasesPackage : typeAliasesPackage.split(","))
            {
                List<String> result = new ArrayList<String>();
                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
                Resource[] resources = resolver.getResources(aliasesPackage);
                if (resources != null && resources.length > 0)
                {
                    MetadataReader metadataReader = null;
                    for (Resource resource : resources)
                    {
                        if (resource.isReadable())
                        {
                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
                            try
                            {
                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
                            }
                            catch (ClassNotFoundException e)
                            {
                                e.printStackTrace();
                            }
                        }
                    }
                }
                if (result.size() > 0)
                {
                    HashSet<String> hashResult = new HashSet<String>(result);
                    allResult.addAll(hashResult);
                }
            }
            if (allResult.size() > 0)
            {
                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
            }
            else
            {
                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
            }
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
        return typeAliasesPackage;
    }
    public Resource[] resolveMapperLocations(String[] mapperLocations)
    {
        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
        List<Resource> resources = new ArrayList<Resource>();
        if (mapperLocations != null)
        {
            for (String mapperLocation : mapperLocations)
            {
                try
                {
                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
                    resources.addAll(Arrays.asList(mappers));
                }
                catch (IOException e)
                {
                    // ignore
                }
            }
        }
        return resources.toArray(new Resource[resources.size()]);
    }
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
    {
        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
        String mapperLocations = env.getProperty("mybatis.mapperLocations");
        String configLocation = env.getProperty("mybatis.configLocation");
        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
        VFS.addImplClass(SpringBootVFS.class);
        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // 使用 MyBatis-Plus 的 SqlSessionFactory
        MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
        // 设置mapper.xml文件的位置
        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
                .getResources("classpath*:mapper/**/*Mapper.xml"));
        // 设置类型别名包
        sessionFactory.setTypeAliasesPackage("com.ruoyi.**.domain");
        return sessionFactory.getObject();
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MyMetaObjectHandler.java
New file
@@ -0,0 +1,37 @@
package com.ruoyi.framework.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
        // 若依特有的字段
        this.strictInsertFill(metaObject, "createBy", String.class, getUsername());
        this.strictInsertFill(metaObject, "updateBy", String.class, getUsername());
    }
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
        this.strictUpdateFill(metaObject, "updateBy", String.class, getUsername());
    }
    private String getUsername() {
        // 从安全上下文中获取当前用户名
        try {
            // 若依原有的获取用户名方式
            // return SecurityUtils.getUsername();
            return "admin"; // 临时写死,实际中需要动态获取
        } catch (Exception e) {
            return "system";
        }
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/config/MybatisPlusConfig.java
New file
@@ -0,0 +1,36 @@
package com.ruoyi.framework.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
    /**
     * 添加插件
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        // 乐观锁插件(如果需要)
        // interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
    /**
     * 自动填充处理器(用于创建时间、更新时间等)
     */
    @Bean
    public MyMetaObjectHandler metaObjectHandler() {
        return new MyMetaObjectHandler();
    }
}
ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java
@@ -115,6 +115,7 @@
                    // 静态资源,可匿名访问
                    .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                    .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
                    .antMatchers("/im/api/**").permitAll()
                    // 除上面外的所有请求全部需要鉴权认证
                    .anyRequest().authenticated();
            })
ruoyi-system/pom.xml
@@ -22,7 +22,30 @@
            <groupId>com.ruoyi</groupId>
            <artifactId>ruoyi-common</artifactId>
        </dependency>
        <dependency>
            <groupId>com.netease.nim</groupId>
            <artifactId>yunxin-server-sdk</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.3.1</version> <!-- 使用最新版本 -->
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>3.5.3.1</version>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.12</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>
ruoyi-system/src/main/java/com/ruoyi/system/domain/UserAccount.java
New file
@@ -0,0 +1,81 @@
package com.ruoyi.system.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("user_account") // 指定表名
public class UserAccount {
    // 自增ID主键
    @TableId(type = IdType.AUTO) // 主键自增
    private Integer id;
    // 账号(唯一)
    private String account;
    // 手机号(唯一)
    private String phoneNumber;
    // 密码
    private String password;
    // 账号类型: 0:真实 1:虚拟
    private Integer accountType = 0;
    // 云信账号
    private String cloudMessageAccount;
    // 昵称
    private String nickname;
    // 账户余额
    private BigDecimal balance = BigDecimal.ZERO;
    // 账号状态: 0:正常 1:禁用
    private Integer status = 0;
    // 最近登录时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date loginTime;
    // 最近登录IP
    private String loginIp;
    // 注册时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date registerTime;
    // 个性签名
    private String signature;
    // 是否支持昵称搜索
    private Boolean supportNicknameSearch = true;
    // 是否支持手机搜索
    private Boolean supportPhoneSearch = true;
    // 是否支持ID搜索
    private Boolean supportIdSearch = true;
    // 是否支持用户名搜索
    private Boolean supportUsernameSearch = true;
    // 是否申请删除账号
    private Boolean requestDelete = true;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT) // 自动填充
    private Date createTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;
    private Boolean deleted = true;
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/UserAccountMapper.java
New file
@@ -0,0 +1,14 @@
package com.ruoyi.system.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.system.domain.UserAccount;
import org.apache.ibatis.annotations.Mapper;
/**
 * @program: ruoyiim
 * @description:
 * @create: 2025-08-19 17:02
 **/
@Mapper
public interface  UserAccountMapper extends BaseMapper<UserAccount> {
}
ruoyi-system/src/main/java/com/ruoyi/system/service/UserAccountService.java
New file
@@ -0,0 +1,7 @@
package com.ruoyi.system.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.system.domain.UserAccount;
public interface UserAccountService extends IService<UserAccount> {
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UserAccountServiceImpl.java
New file
@@ -0,0 +1,16 @@
package com.ruoyi.system.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.system.domain.UserAccount;
import com.ruoyi.system.mapper.UserAccountMapper;
import com.ruoyi.system.service.UserAccountService;
import org.springframework.stereotype.Service;
/**
 * @program: ruoyiim
 * @description:
 * @create: 2025-08-19 16:41
 **/
@Service
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {
}