一、环境准备
创建表
-- 创建数据库
create database if not exists chaoo;
-- 使用数据库
use chaoo;
-- 创建用户表
create table if not exists `user`
(
`id` bigint not null auto_increment comment '主键id',
`user_name` varchar(64) not null default '无名氏' comment '用户名',
`phone` varchar(512) comment '手机号密文',
`email` varchar(512) comment '邮箱密文',
`id_card` varchar(1024) comment '身份证号密文',
`phone_last4_fuzzy` varchar(64) comment '手机号后4位模糊索引',
`id_card_last6_fuzzy` varchar(64) comment '身份证后6位模糊索引',
`email_prefix_fuzzy` varchar(64) comment '邮箱前缀模糊索引',
`created_at` datetime not null default current_timestamp comment '创建时间',
`updated_at` datetime not null default current_timestamp on update current_timestamp comment '更新时间',
`deleted` tinyint not null default 1 comment '逻辑删除标记(-1:已删除,1:未删除)',
primary key (`id`),
key `idx_phone_last4` (`phone_last4_fuzzy`),
key `idx_id_card_last6` (`id_card_last6_fuzzy`),
key `idx_email_prefix` (`email_prefix_fuzzy`)
) engine = innodb
default charset utf8mb4
collate = utf8mb4_unicode_ci comment '用户表';
完善依赖和配置
在父工程中引入相关依赖,使得子模块都能使用
ext {
mybatisPlusVersion = "3.5.3.1"
druidVersion = "1.2.22"
mysqlVersion = "8.0.33"
}
// 操作数据库的orm框架
implementation "com.baomidou:mybatis-plus-boot-starter:${mybatisPlusVersion}"
// 数据库连接池
implementation "com.alibaba:druid-spring-boot-starter:${druidVersion}"
// mysql驱动
implementation("com.mysql:mysql-connector-j:${mysqlVersion}")
在nacos上创建数据库相关的配置文件 db-mysql.yaml作为各模块的公共数据库配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/chaoo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
# 初始化连接数
initial-size: 5
# 最小空闲连接数
min-idle: 5
# 最大活跃连接数
max-active: 20
mybatis-plus:
configuration:
# 开启日志打印完整sql
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
在各服务的bootstrap.yml中引入db-mysql.yaml
spring:
cloud:
nacos:
config:
extension-configs: # 加载公共配置
- data-id: db-mysql.yaml
group: DEFAULT_GROUP
refresh: true
Mybatisplus配置类
package com.chaoup.provider.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
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));
return interceptor;
}
}
代码生成
使用mybatisx插件,生成entity、xml、mapper、service、serviceImpl,这样基础的增删改查功能就有了
二、功能代码
加密注解
定义一个字段加密注解,用于标记实体类的哪些字段需要加密、怎么加密等
package com.chaoup.provider.annotations;
import java.lang.annotation.*;
/**
* 敏感字段加密注解(用于标记实体类中需要加解密的字段)
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Encrypted {
/**
* 加密算法,默认使用最安全的 AES/GCM 算法
*/
String algorithm() default "AES/GCM/NoPadding";
/**
* 密钥ID
*/
String keyId() default "";
/**
* 是否开启模糊查询(true:自动生成模糊查询辅助字段)
*/
boolean fuzzyQuery() default false;
/**
* 模糊查询类型
*/
FuzzyType fuzzyType() default FuzzyType.NONE;
enum FuzzyType {
/**
* 不支持模糊查询
*/
NONE,
/**
* 手机号后四位
*/
PHONE_LAST4,
/**
* 邮箱前缀(@之前的部分)
*/
EMAIL_PREFIX,
/**
* 身份证后六位
*/
ID_CARD_LAST6
}
}
加密服务
定义一个密钥服务,用于 加载、存储、提供专用密钥
package com.chaoup.provider.components;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 密钥管理服务
* 作用:加载、存储、提供加密或模糊查询专用密钥
*/
@Component
public class KeyService {
/**
* 用于保存所有的密钥(key为密钥id,value为AES密钥对象)
*/
private final Map<String, SecretKey> keyMap = new ConcurrentHashMap<>();
/**
* 密钥id
*/
@Value("${encryption.default-key-id:default}")
private String defaultKeyId;
/**
* 密钥(Base64编码格式)
*/
@Value("${encryption.key.default}")
private String defaultKeyBase64;
/**
* 模糊查询专用密钥ID
*/
@Value("${encryption.fuzzy.deterministic-key:fuzzy-key}")
private String fuzzyKeyId;
/**
* 模糊查询专用密钥
*/
@Value("${encryption.fuzzy.key}")
private String fuzzyKeyBase64;
/**
* 模糊查询固定IV向量(16字节)
*/
@Value("${encryption.fuzzy.fixed-iv}")
private String fixedIvHex;
/**
* 加载所有密钥到map中
*/
@PostConstruct
public void init(){
loadKey(defaultKeyId, defaultKeyBase64);
loadKey(fuzzyKeyId, fuzzyKeyBase64);
}
/**
* 根据密钥id获取密钥对象
*/
public SecretKey getKey(String keyId){
keyId = (keyId == null || keyId.isEmpty()) ? defaultKeyId : keyId;
SecretKey secretKey = keyMap.get(keyId);
if (secretKey == null) {
throw new RuntimeException("未找到对应密钥:" + keyId);
}
return secretKey;
}
/**
* 获取模糊查询专用密钥
*/
public SecretKey getFuzzyKey() {
return getKey(fuzzyKeyId);
}
/**
* 获取模糊查询固定IV向量
* @return 字节数组格式的IV
*/
public byte[] getFixedIv() {
return hexToByte(fixedIvHex);
}
/**
* 十六进制字符串转字节数组(IV转换专用)
*/
private byte[] hexToByte(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i+1), 16));
}
return data;
}
/**
* 加载密钥:Base64字符串 → 密钥对象
*/
private void loadKey(String keyId, String keyBase64) {
// Base64解码为字节数组
byte[] decodedKey = Base64.getDecoder().decode(keyBase64);
// 转换为AES密钥
SecretKey secretKey = new SecretKeySpec(decodedKey, "AES");
// 存入密钥Map
keyMap.put(keyId, secretKey);
}
}
加密配置
对应的nacos配置(在nacos上的db-mysql.yml文件中新增如下配置)
# 加密配置
encryption:
enabled: true
default-key-id: default
# 标准加密密钥
key:
default: 8Z/T8L+QeK2sG9dS7aP5xN3vB0nM6jH1
# 模糊查询专用配置
fuzzy:
key: 9aF7gH2kL5zXcV9bN3mR5tY7uI1oP0sD
fixed-iv: 0123456789ABCDEF0123456789ABCDEF
deterministic-key: fuzzy-key
密钥工具类
创建密钥工具类,用于加解密操作
package com.chaoup.provider.utils;
import com.chaoup.provider.components.KeyService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* 加密工具类
*/
@Slf4j
@Component
public class CryptoUtil {
/**
* 标准加密算法:AES-GCM(高安全)
*/
private static final String GCM_ALG = "AES/GCM/NoPadding";
/**
* 确定性加密算法:AES-CBC(模糊查询专用)
*/
private static final String CBC_ALG = "AES/CBC/PKCS5Padding";
/**
* 密文前缀:标记该字符串是加密后的密文
*/
private static final String ENCRYPTED_PREFIX = "ENC:";
/**
* GCM推荐的12字节IV
*/
private static final int IV_LEN = 12;
/**
* 认证标签长度
*/
private static final int TAG_LEN = 128;
@Resource
private KeyService keyService;
/**
* 加密操作
*
* @param plainText 明文
* @param keyId 密钥标识
* @return 密文
*/
public String encrypt(String plainText, String keyId) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
try {
// 获取密钥
SecretKey secretKey = keyService.getKey(keyId);
// 生成随机IV向量(GCM推荐12字节)
byte[] ivs = new byte[IV_LEN];
new SecureRandom().nextBytes(ivs);
// 创建加密器
Cipher cipher = Cipher.getInstance(GCM_ALG);
// GCM参数
GCMParameterSpec spec = new GCMParameterSpec(TAG_LEN, ivs);
// 初始化加密模式
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
// 执行加密
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// 拼接IV + 密文
byte[] encrypted = new byte[ivs.length + cipherText.length];
System.arraycopy(ivs, 0, encrypted, 0, ivs.length);
System.arraycopy(cipherText, 0, encrypted, ivs.length, cipherText.length);
// 添加前缀并Base64编码返回
return ENCRYPTED_PREFIX + Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
/**
* 解密操作
*
* @param encryptedText 密文
* @param keyId 密钥标识
* @return 明文
*/
public String decrypt(String encryptedText, String keyId) {
if (!isEncrypted(encryptedText)) {
return encryptedText;
}
try {
// 去掉密文前缀
encryptedText = encryptedText.substring(ENCRYPTED_PREFIX.length());
// Base64解码
byte[] encryptedData = Base64.getDecoder().decode(encryptedText);
if (encryptedData.length < IV_LEN) {
throw new RuntimeException("密文不合法");
}
// 获取密钥
SecretKey secretKey = keyService.getKey(keyId);
Cipher cipher = Cipher.getInstance(GCM_ALG);
// 解析IV向量(前12字节)
byte[] ivs = Arrays.copyOfRange(encryptedData, 0, IV_LEN);
byte[] cipherText = Arrays.copyOfRange(encryptedData, IV_LEN, encryptedData.length);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LEN, ivs);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
// 执行解密
byte[] plaintext = cipher.doFinal(cipherText);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
log.error("解密失败", e);
return "DECRYPT_FAILED";
}
}
/**
* 模糊查询专用加密
*/
public String encryptDeterministic(String plainText) {
if (plainText == null || plainText.isEmpty()) {
return plainText;
}
try {
// 获取模糊查询专用密钥
SecretKey key = keyService.getFuzzyKey();
// 获取固定IV向量
byte[] iv = keyService.getFixedIv();
Cipher cipher = Cipher.getInstance(CBC_ALG);
IvParameterSpec spec = new IvParameterSpec(iv);
// 初始化加密
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] encrypted = cipher.doFinal(plainText.getBytes());
// Base64编码返回
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("模糊查询加密失败", e);
}
}
/**
* 判断是否是密文
*/
public boolean isEncrypted(String text) {
return text != null && text.startsWith(ENCRYPTED_PREFIX);
}
}
Mybatis拦截器
mybatis拦截器,拦截sql,进行加解密操作
package com.chaoup.provider.components;
import com.chaoup.provider.annotations.Encrypted;
import com.chaoup.provider.utils.CryptoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
/**
* MyBatis加解密拦截器(拦截所有增删改查SQL,自动加密或解密)
*
* @Intercepts 标注该类是mybatis的拦截器
* @Signature 标注要拦截的方法
*/
@Intercepts({
// 拦截新增或更新(insert或update)操作
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
// 拦截普通查询操作
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
// 拦截复杂查询(比如:分页查询、关联查询、带缓存的查询等)操作
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class,
CacheKey.class, BoundSql.class})
})
@Component
@Slf4j
public class EncryptionInterceptor implements Interceptor {
@Resource
private CryptoUtil cryptoUtil;
/**
* 解密开关,默认开启
*/
@Value("${encryption.enabled:true}")
private boolean encryptionEnabled;
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (!encryptionEnabled) {
return invocation.proceed();
}
Object[] args = invocation.getArgs();
String methodName = invocation.getMethod().getName();
// 新增或修改加密
if ("update".equals(methodName)) {
Object parameter = args[1];
if (parameter != null) {
// true=加密,false=解密
processParameter(parameter, Boolean.TRUE);
}
}
// 执行原方法
Object result = invocation.proceed();
// 2. 查询解密
if ("query".equals(methodName) && Objects.nonNull(result)) {
processQueryResult(result);
}
return result;
}
/**
* 递归处理查询结果
*/
private void processQueryResult(Object result) {
// 集合类型(list或者set)
if (result instanceof Collection) {
((Collection<?>) result).forEach(this::decryptObject);
return;
}
// 数组类型
if (result.getClass().isArray()) {
for (Object obj : ((Object[]) result)) {
decryptObject(obj);
}
return;
}
// 普通实体对象
if (!isBasicType(result.getClass())) {
decryptObject(result);
}
}
/**
* 递归处理参数中的实体对象,进行加密
*/
private void processParameter(Object param, boolean encrypt) {
if (param == null || isBasicType(param.getClass())) {
return;
}
Class<?> clazz = param.getClass();
// 处理集合
if (param instanceof Collection) {
((Collection<?>) param).forEach(item -> processParameter(item, encrypt));
return;
}
// 处理Map
if (param instanceof Map) {
((Map<?, ?>) param).values().forEach(val -> processParameter(val, encrypt));
return;
}
// 处理数组
if (clazz.isArray()) {
for (Object item : (Object[]) param) {
processParameter(item, encrypt);
}
return;
}
// 处理实体类字段加解密
processEncryptedFields(param, encrypt);
}
/**
* 处理带@Encrypted注解的字段
*/
private void processEncryptedFields(Object obj, boolean encrypt) {
if (obj == null) {
return;
}
Class<?> clazz = obj.getClass();
// 递归处理父类字段
while (clazz != null && clazz != Object.class) {
for (Field field : clazz.getDeclaredFields()) {
if (java.lang.reflect.Modifier.isStatic(field.getModifiers()) || java.lang.reflect.Modifier.isFinal(field.getModifiers())) {
// 跳过static或者final修饰的字段
continue;
}
if (field.isAnnotationPresent(Encrypted.class)) {
// 仅处理带 @Encrypted 注解的字段
field.setAccessible(true);
try {
Object fieldValue = field.get(obj);
if (fieldValue instanceof String) {
// 仅支持字符串类型的加解密
String value = (String) fieldValue;
Encrypted annotation = field.getAnnotation(Encrypted.class);
handleStringField(obj, field, value, annotation, encrypt);
}
} catch (IllegalAccessException e) {
log.error("字段{}反射赋值失败", field.getName(), e);
} catch (Exception e) {
log.error("字段{}加解密执行失败", field.getName(), e);
}
}
}
clazz = clazz.getSuperclass();
}
}
/**
* 处理String类型字段的加密/解密
*/
private void handleStringField(Object obj, Field field, String value, Encrypted annotation, boolean encrypt) throws IllegalAccessException {
if (encrypt) {
// 加密:防止重复加密
if (org.springframework.util.StringUtils.hasText(value) && !cryptoUtil.isEncrypted(value)) {
String encryptedValue = cryptoUtil.encrypt(value, annotation.keyId());
field.set(obj, encryptedValue);
// 自动生成模糊查询字段
generateFuzzyField(obj, field, annotation, value);
log.debug("字段加密成功 → 类:{} 字段:{}", obj.getClass().getSimpleName(), field.getName());
}
} else {
// 解密:仅解密密文
if (org.springframework.util.StringUtils.hasText(value) && cryptoUtil.isEncrypted(value)) {
String decryptedValue = cryptoUtil.decrypt(value, annotation.keyId());
field.set(obj, decryptedValue);
log.debug("字段解密成功 → 类:{} 字段:{}", obj.getClass().getSimpleName(), field.getName());
}
}
}
/**
* 自动生成模糊查询辅助字段
*/
private void generateFuzzyField(Object obj, Field field, Encrypted anno, String plainText) {
if (!anno.fuzzyQuery() || anno.fuzzyType() == Encrypted.FuzzyType.NONE) return;
String fragment = switch (anno.fuzzyType()) {
case PHONE_LAST4 -> plainText.length() >= 4 ? plainText.substring(plainText.length() - 4) : plainText;
case ID_CARD_LAST6 -> plainText.length() >= 6 ? plainText.substring(plainText.length() - 6) : plainText;
case EMAIL_PREFIX -> plainText.split("@")[0];
default -> null;
};
if (fragment == null) return;
// 确定性加密
String encryptedFragment = cryptoUtil.encryptDeterministic(fragment);
// 自动赋值辅助字段
try {
String fuzzyFieldName = getFuzzyFieldName(field.getName(), anno.fuzzyType());
Field fuzzyField = obj.getClass().getDeclaredField(fuzzyFieldName);
fuzzyField.setAccessible(true);
fuzzyField.set(obj, encryptedFragment);
} catch (Exception e) {
log.warn("模糊字段赋值失败", e);
}
}
private String getFuzzyFieldName(String fieldName, Encrypted.FuzzyType type) {
return switch (type) {
case PHONE_LAST4 -> fieldName + "Last4Fuzzy";
case ID_CARD_LAST6 -> fieldName + "Last6Fuzzy";
case EMAIL_PREFIX -> fieldName + "PrefixFuzzy";
default -> fieldName + "Fuzzy";
};
}
/**
* 解密对象
*/
private void decryptObject(Object obj) {
processEncryptedFields(obj, Boolean.FALSE);
}
/**
* 判断是否为基础类型数据
*/
private boolean isBasicType(Class<?> clazz) {
return clazz.isPrimitive()
|| clazz == String.class
|| Number.class.isAssignableFrom(clazz)
|| clazz == Boolean.class
|| clazz == Character.class
|| clazz.getClassLoader() == null; // jdk原生类(如:Date、LocalDateTime等)
}
/**
* 拦截器注册
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
三、业务代码
实体类
package com.chaoup.provider.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.chaoup.provider.annotations.Encrypted;
import lombok.Data;
import java.time.LocalDate;
/**
* 用户表
* @TableName user
*/
@TableName(value ="user")
@Data
public class User {
/**
* 主键id
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
@TableField(value = "user_name")
private String userName;
/**
* 手机号(密文)
*/
@Encrypted(fuzzyQuery = true, fuzzyType = Encrypted.FuzzyType.PHONE_LAST4)
@TableField(value = "phone")
private String phone;
/**
* 邮箱(密文)
*/
@Encrypted(fuzzyQuery = true, fuzzyType = Encrypted.FuzzyType.EMAIL_PREFIX)
@TableField(value = "email")
private String email;
/**
* 身份证号(密文)
*/
@Encrypted(fuzzyQuery = true, fuzzyType = Encrypted.FuzzyType.ID_CARD_LAST6)
@TableField(value = "id_card")
private String idCard;
/**
* 手机后4位(密文)
*/
@TableField(value = "phone_last4_fuzzy")
private String phoneLast4Fuzzy;
/**
* 身份证后6位(密文)
*/
@TableField(value = "id_card_last6_fuzzy")
private String idCardLast6Fuzzy;
/**
* 邮箱前缀(密文)
*/
@TableField(value = "email_prefix_fuzzy")
private String emailPrefixFuzzy;
/**
* 创建时间
*/
@TableField(value = "created_at")
private LocalDate createdAt;
/**
* 更新时间
*/
@TableField(value = "updated_at")
private LocalDate updatedAt;
/**
* 逻辑删除标记(-1:已删除,1:未删除)
*/
@TableField(value = "deleted")
private Integer deleted;
}
Mapper接口
package com.chaoup.provider.mapper;
import com.chaoup.provider.domain.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author chaoy
* @description 针对表【user(用户表)】的数据库操作Mapper
* @createDate 2026-03-15 23:16:05
* @Entity com.chaoup.provider.domain.User
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
@Select("select * from user where phone_last4_fuzzy = #{encrypted}")
List<User> selectByPhoneLast4(@Param("encrypted") String encrypted);
@Select("select * from user where id_card_last6_fuzzy = #{encrypted}")
List<User> selectByIdCardLast6(@Param("encrypted") String encrypted);
@Select("select * from user where email_prefix_fuzzy = #{encrypted}")
List<User> selectByEmailPrefix(@Param("encrypted") String encrypted);
}
Service实现类
package com.chaoup.provider.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chaoup.provider.domain.User;
import com.chaoup.provider.domain.dto.UserAddOrUpdateDTO;
import com.chaoup.provider.domain.dto.UserPageQueryDTO;
import com.chaoup.provider.mapper.UserMapper;
import com.chaoup.provider.service.UserService;
import com.chaoup.provider.utils.CryptoUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
/**
* @author chaoy
* @description 针对表【user(用户表)】的数据库操作Service实现
* @createDate 2026-03-15 23:16:05
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
@Resource
private UserMapper userMapper;
@Resource
private CryptoUtil cryptoUtil;
@Override
@Transactional(rollbackFor = Exception.class)
public void save(UserAddOrUpdateDTO addOrUpdateDTO) {
User user = new User();
user.setUserName(addOrUpdateDTO.getUserName());
user.setPhone(addOrUpdateDTO.getPhone());
user.setEmail(addOrUpdateDTO.getEmail());
user.setIdCard(addOrUpdateDTO.getIdCard());
user.setCreatedAt(LocalDate.now());
userMapper.insert(user);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void update(UserAddOrUpdateDTO addOrUpdateDTO) {
Long id = addOrUpdateDTO.getId();
if (id == null) {
throw new RuntimeException("主键id不能为空");
}
User oldUser = userMapper.selectById(id);
if (oldUser == null) {
throw new RuntimeException("用户不存在");
}
Optional.ofNullable(addOrUpdateDTO.getUserName()).ifPresent(oldUser::setUserName);
Optional.ofNullable(addOrUpdateDTO.getEmail()).ifPresent(oldUser::setEmail);
Optional.ofNullable(addOrUpdateDTO.getPhone()).ifPresent(oldUser::setPhone);
Optional.ofNullable(addOrUpdateDTO.getIdCard()).ifPresent(oldUser::setIdCard);
oldUser.setUpdatedAt(LocalDate.now());
userMapper.insert(oldUser);
}
@Override
public List<User> listByPhoneLast4(String last4) {
String encrypted = cryptoUtil.encryptDeterministic(last4);
return userMapper.selectByPhoneLast4(encrypted);
}
@Override
public List<User> findByIdCardLast6(String last6) {
String encrypted = cryptoUtil.encryptDeterministic(last6);
return userMapper.selectByIdCardLast6(encrypted);
}
@Override
public List<User> findByEmailPrefix(String prefix) {
String encrypted = cryptoUtil.encryptDeterministic(prefix);
return userMapper.selectByEmailPrefix(encrypted);
}
@Override
public IPage<User> pageQuery(UserPageQueryDTO pageQueryDTO) {
Page<User> page = new Page<>(pageQueryDTO.getPageNum(), pageQueryDTO.getPageSize());
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
String phoneLast4 = pageQueryDTO.getPhoneLast4();
String idCardLast6 = pageQueryDTO.getIdCardLast6();
String emailPrefix = pageQueryDTO.getEmailPrefix();
wrapper.eq(phoneLast4 != null && !phoneLast4.isEmpty(), User::getPhoneLast4Fuzzy,cryptoUtil.encryptDeterministic(phoneLast4))
.eq(idCardLast6 != null && !idCardLast6.isEmpty(), User::getIdCardLast6Fuzzy,cryptoUtil.encryptDeterministic(idCardLast6))
.eq(emailPrefix != null && !emailPrefix.isEmpty(), User::getEmailPrefixFuzzy,cryptoUtil.encryptDeterministic(emailPrefix));
return userMapper.selectPage(page, wrapper);
}
}
Service接口
package com.chaoup.provider.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.chaoup.provider.domain.User;
import com.chaoup.provider.domain.dto.UserAddOrUpdateDTO;
import com.chaoup.provider.domain.dto.UserPageQueryDTO;
import java.util.List;
/**
* @author chaoy
* @description 针对表【user(用户表)】的数据库操作Service
* @createDate 2026-03-15 23:16:05
*/
public interface UserService extends IService<User> {
void save(UserAddOrUpdateDTO addOrUpdateDTO);
void update(UserAddOrUpdateDTO addOrUpdateDTO);
List<User> listByPhoneLast4(String last4);
List<User> findByIdCardLast6(String last6);
List<User> findByEmailPrefix(String prefix);
IPage<User> pageQuery(UserPageQueryDTO pageQueryDTO);
}
Controller控制器
package com.chaoup.provider.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.chaoup.provider.domain.User;
import com.chaoup.provider.domain.dto.UserAddOrUpdateDTO;
import com.chaoup.provider.domain.dto.UserPageQueryDTO;
import com.chaoup.provider.service.UserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("/users")
public class UserController {
@Resource
private UserService userService;
@PostMapping
public String add(@RequestBody UserAddOrUpdateDTO addOrUpdateDTO){
userService.save(addOrUpdateDTO);
return "保存成功";
}
@PutMapping
public String update(@RequestBody UserAddOrUpdateDTO addOrUpdateDTO){
userService.update(addOrUpdateDTO);
return "保存成功";
}
@GetMapping("phone/last4/{last4}")
public List<User> searchByPhoneLast4(@PathVariable String last4){
return userService.listByPhoneLast4(last4);
}
@GetMapping("/idcard/last6/{last6}")
public List<User> findByIdCardLast6(@PathVariable String last6) {
return userService.findByIdCardLast6(last6);
}
// 3. 邮箱前缀查询(如 zhangsan 对应 zhangsan@xxx.com)
@GetMapping("/email/prefix/{prefix}")
public List<User> findByEmailPrefix(@PathVariable String prefix) {
return userService.findByEmailPrefix(prefix);
}
@GetMapping("/page")
public IPage<User> pageQuery(UserPageQueryDTO pageQueryDTO){
return userService.pageQuery(pageQueryDTO);
}
}
四、测试
新增
插入数据(可以插入多条)
POST http://localhost:8081/users
Content-Type: application/json
{
"userName": "chaoo3",
"phone": "13276528371",
"email": "13276528371@163.com",
"idCard": "965550853939952010"
}
插入了6条数据
查询
按手机号后4位查询
GET http://localhost:8081/users/phone/last4/5678
按身份证后6位查询
GET http://localhost:8081/users/idcard/last6/952010
按邮箱前缀查询
GET http://localhost:8081/users/email/prefix/13276528371
再插入几条数据,手机号后4位、身份证后6位、邮箱前缀其中有相同的
分页查询:手机号后4位为8371,且身份证号后4位为952010的用户有哪些
GET http://localhost:8081/users/page?pageNum=1&pageSize=3&phoneLast4=8371&idCardLast6=952010