feat(user): 初始化用户服务模块

update(gateway):更新网关相关路由配置

- 添加用户服务基础架构,包括 API 和 Biz 模块
- 配置用户服务的 Spring Boot 启动类及 MyBatis Plus 配置- 实现用户信息更新接口,支持头像、昵称、小憨书号等字段校验
- 添加全局异常处理器,统一处理业务异常和参数校验错误
- 集成 Nacos 服务发现与配置中心
- 添加日志配置文件,支持异步日志写入
- 新增用户相关枚举类,如性别、响应码等
- 添加参数校验工具类,用于昵称、小憨书号等格式校验
- 配置网关路由,将 /user/** 路径转发至用户服务
- 在 GitIgnore 中忽略用户服务的本地开发配置文件
- 更新认证服务中的用户相关字段命名与接口路径
- 添加用户数据对象 UserDO 及对应的 Mapper 和 XML 配置
- 实现 UserService 接口及默认实现类 UserServiceImpl
- 添加用户信息更新请求 VO 类 UpdateUserInfoReqVO
- 添加用户 Mapper 接口 UserDOMapper 继承 BaseMapper
- 添加用户模块的 Maven 配置 pom.xml 文件
- 添加用户模块的编码配置,确保使用 UTF-8 编码
- 添加用户模块的启动日志配置 logback-spring.xml
- 添加用户模块的 bootstrap.yml 配置文件
- 添加用户模块的 application.yml 配置文件
- 添加用户模块的异常处理类 GlobalExceptionHandler
- 添加用户模块的枚举类 ResponseCodeEnum 和 SexEnum
- 添加用户模块的工具类 ParamUtils用于参数校验
- 添加用户模块的控制器 UserController 处理用户信息更新请求
- 添加用户模块的服务接口 UserService 及其实现类 UserServiceImpl
- 添加用户模块的数据访问对象 UserDO 及其映射文件 UserDOMapper.xml
- 添加用户模块的请求视图对象 UpdateUserInfoReqVO
- 添加用户模块的 API 模块 pom.xml 配置文件
- 添加用户模块的 Biz 模块 pom.xml 配置文件
- 添加用户模块的根 pom.xml 配置文件
- 在主 pom.xml 中添加用户模块 han-note-user作为子模块
- 修改 SaToken 配置,调整登录和登出接口路径白名单
- 移除 UserController 中的 @RequestMapping("/user") 注解- 修改 UserDO 中“小哈书号”为“小憨书号”以保持命名一致性
This commit is contained in:
Hanserwei
2025-10-04 14:56:33 +08:00
parent 0d71d8e209
commit 91e36d5a84
25 changed files with 805 additions and 5 deletions

View File

@@ -0,0 +1,80 @@
<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">
<modelVersion>4.0.0</modelVersion>
<!-- 指定父项目 -->
<parent>
<groupId>com.hanserwei</groupId>
<artifactId>han-note-user</artifactId>
<version>${revision}</version>
</parent>
<!-- 打包方式 -->
<packaging>jar</packaging>
<artifactId>han-note-user-biz</artifactId>
<name>${project.artifactId}</name>
<description>用户服务业务模块</description>
<dependencies>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-common</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!-- 服务发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
<dependency>
<groupId>com.hanserwei</groupId>
<artifactId>hanserwei-spring-boot-starter-biz-context</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package com.hanserwei.hannote.user.biz;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.hanserwei.hannote.user.biz.domain.mapper")
public class HannoteUserBizApplication {
public static void main(String[] args) {
SpringApplication.run(HannoteUserBizApplication.class, args);
}
}

View File

@@ -0,0 +1,33 @@
package com.hanserwei.hannote.user.biz.controller;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.biz.model.vo.UpdateUserInfoReqVO;
import com.hanserwei.hannote.user.biz.service.UserService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
@Resource
private UserService userService;
/**
* 用户信息修改
*
* @param updateUserInfoReqVO 修改信息请求
* @return 响应
*/
@PostMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Response<?> updateUserInfo(@Validated UpdateUserInfoReqVO updateUserInfoReqVO) {
return userService.updateUserInfo(updateUserInfoReqVO);
}
}

View File

@@ -0,0 +1,107 @@
package com.hanserwei.hannote.user.biz.domain.dataobject;
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 lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 用户表
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "t_user")
public class UserDO {
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
/**
* 小憨书号(唯一凭证)
*/
@TableField(value = "han_note_id")
private String hanNoteId;
/**
* 密码
*/
@TableField(value = "`password`")
private String password;
/**
* 昵称
*/
@TableField(value = "nickname")
private String nickname;
/**
* 头像
*/
@TableField(value = "avatar")
private String avatar;
/**
* 生日
*/
@TableField(value = "birthday")
private LocalDate birthday;
/**
* 背景图
*/
@TableField(value = "background_img")
private String backgroundImg;
/**
* 邮箱
*/
@TableField(value = "email")
private String email;
/**
* 性别(0女 1男)
*/
@TableField(value = "sex")
private Integer sex;
/**
* 状态(0启用 1禁用)
*/
@TableField(value = "`status`")
private Integer status;
/**
* 个人简介
*/
@TableField(value = "introduction")
private String introduction;
/**
* 创建时间
*/
@TableField(value = "create_time")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time")
private LocalDateTime updateTime;
/**
* 逻辑删除(0未删除 1已删除)
*/
@TableField(value = "is_deleted")
private Boolean isDeleted;
}

View File

@@ -0,0 +1,7 @@
package com.hanserwei.hannote.user.biz.domain.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.hanserwei.hannote.user.biz.domain.dataobject.UserDO;
public interface UserDOMapper extends BaseMapper<UserDO> {
}

View File

@@ -0,0 +1,28 @@
package com.hanserwei.hannote.user.biz.enums;
import com.hanserwei.framework.common.exception.BaseExceptionInterface;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum ResponseCodeEnum implements BaseExceptionInterface {
// ----------- 通用异常状态码 -----------
SYSTEM_ERROR("USER-10000", "出错啦,后台小维正在努力修复中..."),
PARAM_NOT_VALID("USER-10001", "参数错误!!!"),
// ----------- 业务异常状态码 -----------
NICK_NAME_VALID_FAIL("USER-20001", "昵称请设置2-24个字符不能使用@《/等特殊字符"),
HAN_NOTE_ID_VALID_FAIL("USER-20002", "小憨书号请设置6-15个字符仅可使用英文必须、数字、下划线"),
SEX_VALID_FAIL("USER-20003", "性别错误"),
INTRODUCTION_VALID_FAIL("USER-20004", "个人简介请设置1-100个字符"),
;
;
// 异常码
private final String errorCode;
// 错误信息
private final String errorMsg;
}

View File

@@ -0,0 +1,26 @@
package com.hanserwei.hannote.user.biz.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Objects;
@Getter
@AllArgsConstructor
public enum SexEnum {
WOMAN(0),
MAN(1);
private final Integer value;
public static boolean isValid(Integer value) {
for (SexEnum loginTypeEnum : SexEnum.values()) {
if (Objects.equals(value, loginTypeEnum.getValue())) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,103 @@
package com.hanserwei.hannote.user.biz.exception;
import com.hanserwei.framework.common.exception.ApiException;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.biz.enums.ResponseCodeEnum;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Optional;
@SuppressWarnings("LoggingSimilarMessage")
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 捕获自定义业务异常
*
* @return Response.fail(e)
*/
@ExceptionHandler({ApiException.class})
@ResponseBody
public Response<Object> handleApiException(HttpServletRequest request, ApiException e) {
log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMsg());
return Response.fail(e);
}
/**
* 捕获参数校验异常
*
* @return Response.fail(errorCode, errorMessage)
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseBody
public Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 获取 BindingResult
BindingResult bindingResult = e.getBindingResult();
StringBuilder sb = new StringBuilder();
// 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com';
Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> {
errors.forEach(error ->
sb.append(error.getField())
.append(" ")
.append(error.getDefaultMessage())
.append(", 当前值: '")
.append(error.getRejectedValue())
.append("'; ")
);
});
// 错误信息
String errorMessage = sb.toString();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 捕获 guava 参数校验异常
*
* @return Response.fail(ResponseCodeEnum.PARAM_NOT_VALID)
*/
@ExceptionHandler({IllegalArgumentException.class})
@ResponseBody
public Response<Object> handleIllegalArgumentException(HttpServletRequest request, IllegalArgumentException e) {
// 参数错误异常码
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
// 错误信息
String errorMessage = e.getMessage();
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
return Response.fail(errorCode, errorMessage);
}
/**
* 其他类型异常
*
* @param request 请求
* @param e 异常
* @return Response.fail(ResponseCodeEnum.SYSTEM_ERROR)
*/
@ExceptionHandler({Exception.class})
@ResponseBody
public Response<Object> handleOtherException(HttpServletRequest request, Exception e) {
log.error("{} request error, ", request.getRequestURI(), e);
return Response.fail(ResponseCodeEnum.SYSTEM_ERROR);
}
}

View File

@@ -0,0 +1,52 @@
package com.hanserwei.hannote.user.biz.model.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class UpdateUserInfoReqVO {
/**
* 头像
*/
private MultipartFile avatar;
/**
* 昵称
*/
private String nickname;
/**
* 小憨书 ID
*/
private String hanNoteId;
/**
* 性别
*/
private Integer sex;
/**
* 生日
*/
private LocalDate birthday;
/**
* 个人介绍
*/
private String introduction;
/**
* 背景图
*/
private MultipartFile backgroundImg;
}

View File

@@ -0,0 +1,17 @@
package com.hanserwei.hannote.user.biz.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.hannote.user.biz.domain.dataobject.UserDO;
import com.hanserwei.hannote.user.biz.model.vo.UpdateUserInfoReqVO;
public interface UserService extends IService<UserDO> {
/**
* 更新用户信息
*
* @param updateUserInfoReqVO 更新用户信息请求参数
* @return 响应结果
*/
Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO);
}

View File

@@ -0,0 +1,92 @@
package com.hanserwei.hannote.user.biz.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.google.common.base.Preconditions;
import com.hanserwei.framework.biz.context.holder.LoginUserContextHolder;
import com.hanserwei.framework.common.response.Response;
import com.hanserwei.framework.common.utils.ParamUtils;
import com.hanserwei.hannote.user.biz.domain.dataobject.UserDO;
import com.hanserwei.hannote.user.biz.domain.mapper.UserDOMapper;
import com.hanserwei.hannote.user.biz.enums.ResponseCodeEnum;
import com.hanserwei.hannote.user.biz.enums.SexEnum;
import com.hanserwei.hannote.user.biz.model.vo.UpdateUserInfoReqVO;
import com.hanserwei.hannote.user.biz.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Objects;
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserDOMapper, UserDO> implements UserService {
@Override
public Response<?> updateUserInfo(UpdateUserInfoReqVO updateUserInfoReqVO) {
UserDO userDO = new UserDO();
// 设置当前需要更新的用户 ID
userDO.setId(LoginUserContextHolder.getUserId());
// 标识位:是否需要更新
boolean needUpdate = false;
// 头像
MultipartFile avatar = updateUserInfoReqVO.getAvatar();
if (Objects.nonNull(avatar)) {
// TODO: 上传头像,调用服务
}
// 昵称
String nickname = updateUserInfoReqVO.getNickname();
if (StringUtils.isNotBlank(nickname)) {
Preconditions.checkArgument(ParamUtils.checkNickname(nickname), ResponseCodeEnum.NICK_NAME_VALID_FAIL.getErrorMsg());
userDO.setNickname(nickname);
needUpdate = true;
}
// 小憨书 ID
String hanNoteId = updateUserInfoReqVO.getHanNoteId();
if (StringUtils.isNotBlank(hanNoteId)) {
Preconditions.checkArgument(ParamUtils.checkHannoteId(hanNoteId), ResponseCodeEnum.HAN_NOTE_ID_VALID_FAIL.getErrorMsg());
userDO.setHanNoteId(hanNoteId);
needUpdate = true;
}
// 性别
Integer sex = updateUserInfoReqVO.getSex();
if (Objects.nonNull(sex)) {
Preconditions.checkArgument(SexEnum.isValid(sex), ResponseCodeEnum.SEX_VALID_FAIL.getErrorMsg());
userDO.setSex(sex);
needUpdate = true;
}
// 生日
LocalDate birthday = updateUserInfoReqVO.getBirthday();
if (Objects.nonNull(birthday)) {
userDO.setBirthday(birthday);
needUpdate = true;
}
// 个人介绍
String introduction = updateUserInfoReqVO.getIntroduction();
if (StringUtils.isNotBlank(introduction)) {
Preconditions.checkArgument(ParamUtils.checkLength(introduction, 100), ResponseCodeEnum.INTRODUCTION_VALID_FAIL.getErrorMsg());
userDO.setIntroduction(introduction);
needUpdate = true;
}
// 背景图片
MultipartFile backgroundImg = updateUserInfoReqVO.getBackgroundImg();
if (Objects.nonNull(backgroundImg)) {
// TODO: 上传背景图片,调用服务
}
if (needUpdate) {
userDO.setUpdateTime(LocalDateTime.now());
return updateById(userDO) ? Response.success() : Response.fail();
}
return Response.success();
}
}

View File

@@ -0,0 +1,13 @@
server:
port: 8082 # 项目启动的端口
spring:
profiles:
active: dev # 默认激活 dev 本地开发环境
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl
global-config:
banner: false
mapper-locations: classpath*:/mapperxml/*.xml

View File

@@ -0,0 +1,12 @@
spring:
application:
name: han-note-user # 应用名称
profiles:
active: dev # 默认激活 dev 本地开发环境
cloud:
nacos:
discovery:
enabled: true # 启用服务发现
group: DEFAULT_GROUP # 所属组
namespace: han-note # 命名空间
server-addr: 127.0.0.1:8848 # 指定 Nacos 配置中心的服务器地址

View File

@@ -0,0 +1,58 @@
<configuration>
<!-- 引用 Spring Boot 的 logback 基础配置 -->
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<!-- 应用名称 -->
<property scope="context" name="appName" value="user"/>
<!-- 自定义日志输出路径,以及日志名称前缀 -->
<property name="LOG_FILE" value="./logs/${appName}.%d{yyyy-MM-dd}"/>
<!-- 每行日志输出的格式 -->
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"/>
<!-- 文件输出 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- 日志文件的命名格式 -->
<fileNamePattern>${LOG_FILE}-%i.log</fileNamePattern>
<!-- 保留 30 天的日志文件 -->
<maxHistory>30</maxHistory>
<!-- 单个日志文件最大大小 -->
<maxFileSize>10MB</maxFileSize>
<!-- 日志文件的总大小0 表示不限制 -->
<totalSizeCap>0</totalSizeCap>
<!-- 重启服务时,是否清除历史日志,不推荐清理 -->
<cleanHistoryOnStart>false</cleanHistoryOnStart>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>${LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- 异步写入日志,提升性能 -->
<appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
<!-- 是否丢弃日志, 0 表示不丢弃。默认情况下,如果队列满 80%, 会丢弃 TRACE、DEBUG、INFO 级别的日志 -->
<discardingThreshold>0</discardingThreshold>
<!-- 队列大小。默认值为 256 -->
<queueSize>256</queueSize>
<appender-ref ref="FILE"/>
</appender>
<!-- 本地 dev 开发环境 -->
<springProfile name="dev">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/> <!-- 输出控制台日志 -->
<appender-ref ref="ASYNC_FILE"/> <!-- 打印日志到文件中。PS: 本地环境下,如果不想打印日志到文件,可注释掉此行 -->
</root>
</springProfile>
<!-- 其它环境 -->
<springProfile name="prod">
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="ASYNC_FILE"/> <!-- 生产环境下,仅打印日志到文件中 -->
</root>
</springProfile>
</configuration>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hanserwei.hannote.user.biz.domain.mapper.UserDOMapper">
<resultMap id="BaseResultMap" type="com.hanserwei.hannote.user.biz.domain.dataobject.UserDO">
<!--@mbg.generated-->
<!--@Table t_user-->
<id column="id" jdbcType="BIGINT" property="id" />
<result column="han_note_id" jdbcType="VARCHAR" property="hanNoteId" />
<result column="password" jdbcType="VARCHAR" property="password" />
<result column="nickname" jdbcType="VARCHAR" property="nickname" />
<result column="avatar" jdbcType="VARCHAR" property="avatar" />
<result column="birthday" jdbcType="DATE" property="birthday" />
<result column="background_img" jdbcType="VARCHAR" property="backgroundImg" />
<result column="email" jdbcType="VARCHAR" property="email" />
<result column="sex" jdbcType="TINYINT" property="sex" />
<result column="status" jdbcType="TINYINT" property="status" />
<result column="introduction" jdbcType="VARCHAR" property="introduction" />
<result column="create_time" jdbcType="TIMESTAMP" property="createTime" />
<result column="update_time" jdbcType="TIMESTAMP" property="updateTime" />
<result column="is_deleted" jdbcType="BIT" property="isDeleted" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, han_note_id, `password`, nickname, avatar, birthday, background_img, email, sex,
`status`, introduction, create_time, update_time, is_deleted
</sql>
</mapper>