init: UserBaseService (backend + deploy + scripts)
ci-userbase-service / build-and-push (push) Successful in 40s
ci-userbase-service / build-and-push (push) Successful in 40s
This commit is contained in:
@@ -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 <-> 枚举)。<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 <-> 实现 IEnum<Integer> 的枚举」JPA 转换器抽象基类。<br>
|
||||
* <p>
|
||||
* <b>绑定机制(通用做法):</b>为每个枚举写一个极薄的子类,在构造器里传入枚举 class 即可完成绑定,
|
||||
* 然后实体字段用 {@code @Convert(converter = XxxEnumConverter.class)} 引用该子类。例如:
|
||||
* <pre>
|
||||
* public class GroupTypeEnumConverter extends CommonIntEnumConverter<GroupTypeEnum> {
|
||||
* public GroupTypeEnumConverter() { super(GroupTypeEnum.class); }
|
||||
* }
|
||||
* </pre>
|
||||
* 这样同一套逻辑可服务任意枚举;又因为每个枚举拥有<b>独立的转换器实例</b>,
|
||||
* 彼此之间不会串类型(标准 JPA 的 @Convert 是按 converter 类创建共享单例的,
|
||||
* 若让多个不同枚举共用同一个泛型实例,则无法区分各自的枚举 class,故采用「每枚举一子类」的绑定方式)。
|
||||
* </p>
|
||||
*
|
||||
* @param <E> 必须实现 IEnum<Integer> 的枚举类型
|
||||
* @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 <-> 枚举)。<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 <-> 枚举)。<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 <-> 枚举)。<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 <-> 枚举)。<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 <-> 枚举)。<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 <-> 枚举)。<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 {}
|
||||
}
|
||||
Reference in New Issue
Block a user