init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s

This commit is contained in:
chenxing
2026-08-10 14:50:22 +08:00
commit 4ec19486e0
234 changed files with 15604 additions and 0 deletions
@@ -0,0 +1,72 @@
package com.yunhai.userbase.common;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 全局异常处理器:仅处理「参数校验」类异常,把 Spring 默认抛出的冗长消息收敛成「一句话」返回,
* 避免前端收到整段 Validation failed ... 堆栈文本。<br>
* 返回 HTTP 200 + {@code {code:4, msg:..., data:null}} 结构(而非 HTTP 400),
* 与前端 alova errorHandle 的约定一致:status!=200 会被当作网络错误,code=4 才走「参数错误」分支。<br>
* 注意:本处理器【不】处理业务异常(如 {@code DException}),业务异常交由项目框架统一处理
* (返回 code=5、msg=异常消息),此处若拦截会覆盖框架行为。
*
* @author chenx
*/
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
/** 参数校验失败的业务码,前端按「参数错误」处理 */
private static final int CODE_PARAM_ERROR = 4;
/** @RequestBody + @Validated/@Valid 失败:提取每个字段的 defaultMessage。 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
return paramError(fieldErrorsMessage(ex.getBindingResult().getFieldErrors()));
}
/** 普通 Bean 参数(如 SortChildren/Move 的 @Validated 请求体对象)校验失败。 */
@ExceptionHandler(BindException.class)
public Map<String, Object> handleBind(BindException ex) {
return paramError(fieldErrorsMessage(ex.getBindingResult().getFieldErrors()));
}
/** 方法级约束(@PathVariable/@RequestParam 上的 @NotNull 等)校验失败。 */
@ExceptionHandler(ConstraintViolationException.class)
public Map<String, Object> handleConstraintViolation(ConstraintViolationException ex) {
String msg = ex.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(";"));
return paramError(msg);
}
private String fieldErrorsMessage(java.util.List<FieldError> fieldErrors) {
if (fieldErrors == null || fieldErrors.isEmpty()) {
return "参数校验失败";
}
return fieldErrors.stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(";"));
}
/** HTTP 200 + code/msg/data 结构体,前端根据 code 提示 msg */
private Map<String, Object> paramError(String msg) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("code", CODE_PARAM_ERROR);
body.put("msg", msg);
body.put("data", null);
return body;
}
}
@@ -0,0 +1,35 @@
package com.yunhai.userbase.common.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 操作日志注解。<br>
* 标注在 Controller 方法上,AOP 拦截后自动记录操作日志并异步落库。<br>
*
* @author chenx
* @date 2026/7/28
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
/**
* 操作描述,可选。
*/
String value() default "";
/**
* 是否保存响应结果(GZIP 压缩后入库)。
*/
boolean saveResponse() default true;
/**
* 响应结果序列化后的最大字符数,超过则截断。默认 10KB。
*/
int maxResponseSize() default 10240;
}
@@ -0,0 +1,18 @@
package com.yunhai.userbase.common.context;
public class UserContext {
private static final ThreadLocal<Integer> CURRENT_USER = new ThreadLocal<>();
public static void setCurrentUser(Integer userId) {
CURRENT_USER.set(userId);
}
public static Integer getCurrentUser() {
return CURRENT_USER.get();
}
public static void clear() {
CURRENT_USER.remove();
}
}
@@ -0,0 +1,18 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.ClientEnum;
import javax.persistence.AttributeConverter;
/**
* ClientEnum 专用 JPA 转换器(int &lt;-&gt; 枚举)。<br>
* 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可。
*
* @author chenx
*/
public class ClientEnumConverter extends CommonIntEnumConverter<ClientEnum> {
public ClientEnumConverter() {
super(ClientEnum.class);
}
}
@@ -0,0 +1,47 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.IEnum;
import javax.persistence.AttributeConverter;
/**
* 通用「int &lt;-&gt; 实现 IEnum&lt;Integer&gt; 的枚举」JPA 转换器抽象基类。<br>
* <p>
* <b>绑定机制(通用做法):</b>为每个枚举写一个极薄的子类,在构造器里传入枚举 class 即可完成绑定,
* 然后实体字段用 {@code @Convert(converter = XxxEnumConverter.class)} 引用该子类。例如:
* <pre>
* public class GroupTypeEnumConverter extends CommonIntEnumConverter&lt;GroupTypeEnum&gt; {
* public GroupTypeEnumConverter() { super(GroupTypeEnum.class); }
* }
* </pre>
* 这样同一套逻辑可服务任意枚举;又因为每个枚举拥有<b>独立的转换器实例</b>,
* 彼此之间不会串类型(标准 JPA 的 @Convert 是按 converter 类创建共享单例的,
* 若让多个不同枚举共用同一个泛型实例,则无法区分各自的枚举 class,故采用「每枚举一子类」的绑定方式)。
* </p>
*
* @param <E> 必须实现 IEnum&lt;Integer&gt; 的枚举类型
* @author chenx
*/
public abstract class CommonIntEnumConverter<E extends Enum<E> & IEnum<Integer>>
implements AttributeConverter<E, Integer> {
private final Class<E> enumCls;
protected CommonIntEnumConverter(Class<E> enumCls) {
this.enumCls = enumCls;
}
@Override
public Integer convertToDatabaseColumn(E enumVal) {
return enumVal == null ? null : enumVal.getKey();
}
@Override
public E convertToEntityAttribute(Integer dbVal) {
if (dbVal == null) {
return null;
}
// 读库安全:无匹配的枚举值返回 null(不抛异常,避免脏数据导致整棵树加载失败)
return IEnum.getByKey(enumCls, dbVal);
}
}
@@ -0,0 +1,18 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.GenderEnum;
import javax.persistence.AttributeConverter;
/**
* GenderEnum 专用 JPA 转换器(int &lt;-&gt; 枚举)。<br>
* 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可。
*
* @author chenx
*/
public class GenderEnumConverter extends CommonIntEnumConverter<GenderEnum> {
public GenderEnumConverter() {
super(GenderEnum.class);
}
}
@@ -0,0 +1,18 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.GroupTypeEnum;
import javax.persistence.AttributeConverter;
/**
* GroupTypeEnum 专用 JPA 转换器(int &lt;-&gt; 枚举)。<br>
* 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可,无需重复转换逻辑。
*
* @author chenx
*/
public class GroupTypeEnumConverter extends CommonIntEnumConverter<GroupTypeEnum> {
public GroupTypeEnumConverter() {
super(GroupTypeEnum.class);
}
}
@@ -0,0 +1,18 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.IntegEnum;
import javax.persistence.AttributeConverter;
/**
* IntegEnum 专用 JPA 转换器(int &lt;-&gt; 枚举)。<br>
* 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可。
*
* @author chenx
*/
public class IntegEnumConverter extends CommonIntEnumConverter<IntegEnum> {
public IntegEnumConverter() {
super(IntegEnum.class);
}
}
@@ -0,0 +1,50 @@
package com.yunhai.userbase.common.converter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
import java.util.Collections;
import java.util.List;
/**
* @author chenx
* @date 2026/7/14
* @description 将 List<Integer> 与 MySQL JSON 字符串互相转换。
* 使用 Jackson 的 {@link TypeReference} 明确反序列化为 List<Integer>,
* 避免 Hibernate 内置 JSON 类型把数组元素类型读错的问题。
*/
@Converter
public class ListIntegerJsonConverter implements AttributeConverter<List<Integer>, String> {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final TypeReference<List<Integer>> LIST_INTEGER_TYPE = new TypeReference<List<Integer>>() {
};
@Override
public String convertToDatabaseColumn(List<Integer> attribute) {
if (attribute == null) {
return null;
}
try {
return MAPPER.writeValueAsString(attribute);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("角色列表序列化 JSON 失败: " + attribute, e);
}
}
@Override
public List<Integer> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.trim().isEmpty()) {
return null;
}
try {
List<Integer> list = MAPPER.readValue(dbData, LIST_INTEGER_TYPE);
return list == null ? Collections.emptyList() : list;
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("角色 JSON 解析失败: " + dbData, e);
}
}
}
@@ -0,0 +1,18 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.StatusEnum;
import javax.persistence.AttributeConverter;
/**
* StatusEnum 专用 JPA 转换器(int &lt;-&gt; 枚举)。<br>
* 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可。
*
* @author chenx
*/
public class StatusEnumConverter extends CommonIntEnumConverter<StatusEnum> {
public StatusEnumConverter() {
super(StatusEnum.class);
}
}
@@ -0,0 +1,20 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.SysRoleEnum;
import javax.persistence.AttributeConverter;
/**
* SysRoleEnum 专用 JPA 转换器(int &lt;-&gt; 枚举)。<br>
* 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可。<br>
* 注意:sys_user.roles 是「角色 key 的 JSON 列表」,使用 ListIntegerJsonConverter,
* 若某处需要「单角色」字段,再引用本转换器即可。
*
* @author chenx
*/
public class SysRoleEnumConverter extends CommonIntEnumConverter<SysRoleEnum> {
public SysRoleEnumConverter() {
super(SysRoleEnum.class);
}
}
@@ -0,0 +1,18 @@
package com.yunhai.userbase.common.converter;
import com.yunhai.userbase.enums.UserTypeEnum;
import javax.persistence.AttributeConverter;
/**
* UserTypeEnum 专用 JPA 转换器(int &lt;-&gt; 枚举)。<br>
* 继承通用基类 CommonIntEnumConverter,仅在构造器绑定枚举 class 即可。
*
* @author chenx
*/
public class UserTypeEnumConverter extends CommonIntEnumConverter<UserTypeEnum> {
public UserTypeEnumConverter() {
super(UserTypeEnum.class);
}
}
@@ -0,0 +1,48 @@
package com.yunhai.userbase.common.interceptor;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.userbase.common.context.UserContext;
import com.yunhai.userbase.service.UserCenterService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 把当前登录用户写入 {@link UserContext}(供 JPA 审计监听器 @PrePersist/@PreUpdate 与操作日志切面使用)。<br><br>
* 登录名取自 CAS 客户端写入的 {@code request.getRemoteUser()}(cas-client-core 过滤器已建立会话);
* 再经本地 {@link UserCenterService} 组装 {@link BoaUser} 取其 id 写入 ThreadLocal。
* 原 bsch 的 {@code BschBaseOpenApi.getUserInfoWithDept} 与 {@code LocalMemCache} 已移除,
* 统一改用基础平台用户模型与本地服务中心。
*/
@Component
public class BoaUserContextInterceptor implements HandlerInterceptor {
@Autowired
private UserCenterService userCenterService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
String loginName = request.getRemoteUser();
if (StringUtils.isBlank(loginName)) {
return true;
}
BoaUser user = userCenterService.getUserInfoWithDeptByAccount(loginName);
if (user != null && user.getId() != null && user.getId() != 0) {
UserContext.setCurrentUser(user.getId());
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {
UserContext.clear();
}
}
@@ -0,0 +1,33 @@
package com.yunhai.userbase.common.listener;
import com.bestsch.utils.DException;
import com.yunhai.userbase.common.context.UserContext;
import com.yunhai.userbase.entity.BaseEntity;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
public class UserAuditListener {
@PrePersist
public void prePersist(BaseEntity entity) {
System.out.println("=== prePersist 触发 ===");
Integer userId = UserContext.getCurrentUser();
System.out.println("ThreadLocal userId: " + userId);
if (userId != null) {
entity.setCreateUser(userId);
entity.setUpdateUser(userId);
}
}
@PreUpdate
public void preUpdate(BaseEntity entity) {
System.out.println("=== preUpdate 触发 ===");
Integer userId = UserContext.getCurrentUser();
System.out.println("ThreadLocal userId: " + userId);
if (userId != null) {
entity.setUpdateUser(userId);
}
}
}
@@ -0,0 +1,11 @@
package com.yunhai.userbase.common.validation;
/**
* @author chenx
* @date 2026/7/15 22:11
* @description
*/
public interface ValidationGroups {
interface Create {}
interface Update {}
}