Compare commits

..

26 Commits

Author SHA1 Message Date
08aab8d5a9 add TimiSpring.getLanguageRaw 2025-11-07 23:38:50 +08:00
f887079a62 catch multilingualHeader exception 2025-11-07 16:56:40 +08:00
3283c678db fix TimiSpring.getLanguage fail on Chrome 2025-11-07 16:55:59 +08:00
3eb6bd7df5 log for not prod env 2025-11-07 15:15:41 +08:00
6a57d22366 add Multilingual common entity 2025-11-06 17:16:39 +08:00
007253f828 support get URI token for TimiSpring.getToken 2025-11-06 16:59:00 +08:00
d1728955aa fix TimiSpring.getLanguage result null for xx-XX 2025-11-06 16:34:56 +08:00
1a81ac1c54 support template multilingual in GlobalReturnHandler 2025-11-06 16:22:11 +08:00
838c6cd6a4 add log GlobalExceptionHandler.paramsException arg 2025-11-06 16:21:15 +08:00
39dd976820 TimiSpring.getLanguage use url arg(lang) first and header(Accept-Language) 2025-11-06 16:12:07 +08:00
2e67e4086d support extends SQLProvider 2025-11-06 14:10:53 +08:00
4de03cf60a add mybatis GsonHandler 2025-11-06 14:10:23 +08:00
9bcf17a118 add BaseMapper.listAll 2025-11-04 14:58:49 +08:00
e08a50a9b2 AbstractEntityService.update use BaseMapper.updateSelective 2025-11-04 14:58:36 +08:00
945a2c5e9d fix sql in SQLProvider.listOrder 2025-11-03 16:08:35 +08:00
1688666dca add BaseMapper.updateSelective 2025-11-02 20:59:27 +08:00
278bf7c59a add CaptchaValid 2025-11-02 10:23:31 +08:00
8a7946ce01 add TimiSpring.getRequestArg 2025-11-02 10:22:52 +08:00
f2689ab812 auto sort for BaseMapper.listOrder when entity creatable or updatable 2025-10-29 11:22:57 +08:00
3ae1ccedb7 add GlobalExceptionHandler msg 2025-10-29 11:21:50 +08:00
8de027e0c7 add YamlPropertySourceFactory 2025-10-29 11:20:59 +08:00
69d847f337 support deploy nexus 2025-10-13 10:55:20 +08:00
443757f501 upper Page & PageResult base field to timi-java 2025-10-13 10:54:46 +08:00
2fc06e3851 add BaseMapper.listOrder 2025-10-13 10:53:55 +08:00
831d36e095 support list() and count() in SQLProvider, fix custom column mapper 2025-07-25 10:50:33 +08:00
39f628e71a add RequestRange 2025-07-15 11:44:27 +08:00
17 changed files with 646 additions and 135 deletions

57
pom.xml
View File

@ -28,17 +28,60 @@
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-deploy-plugin</artifactId>
<version>3.11.0</version> <version>3.1.3</version>
<configuration> </plugin>
<source>21</source> <plugin>
<target>21</target> <groupId>org.apache.maven.plugins</groupId>
<encoding>UTF-8</encoding> <artifactId>maven-source-plugin</artifactId>
</configuration> <version>3.3.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>package</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.11.2</version>
<executions>
<execution>
<id>attach-javadocs</id>
<phase>package</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
<distributionManagement>
<repository>
<id>timi_nexus</id>
<url>https://nexus.imyeyu.com/repository/maven-releases/</url>
</repository>
</distributionManagement>
<repositories>
<repository>
<id>timi_nexus</id>
<url>https://nexus.imyeyu.com/repository/maven-public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>

View File

@ -7,6 +7,7 @@ import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.bean.timi.TimiResponse; import com.imyeyu.java.bean.timi.TimiResponse;
import com.imyeyu.java.ref.Ref; import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.bean.RequestRange;
import jakarta.servlet.http.Cookie; import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
@ -16,8 +17,10 @@ import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Locale; import java.util.Locale;
/** /**
@ -117,6 +120,12 @@ public class TimiSpring {
return getRequest().getRequestURI(); return getRequest().getRequestURI();
} }
public static String cutURIStartAt(String flag) {
int indexOf = getURI().indexOf(flag);
TimiException.requiredTrue(-1 < indexOf, "not found flag: %s".formatted(flag));
return getURI().substring(indexOf + flag.length());
}
/** /**
* 获取 HttpServlet 回调 * 获取 HttpServlet 回调
* *
@ -286,6 +295,26 @@ public class TimiSpring {
getRequest().removeAttribute(key); getRequest().removeAttribute(key);
} }
/**
* 获取请求 URL 参数
*
* @param key 键
* @return 参数值
*/
public static String getRequestArg(String key) {
return getRequest().getParameter(key);
}
/**
* 获取请求 URL 参数(多值)
*
* @param key 键
* @return 参数值
*/
public static String[] getRequestArgs(String key) {
return getRequest().getParameterValues(key);
}
public static void addCookie(Cookie cookie) { public static void addCookie(Cookie cookie) {
getResponse().addCookie(cookie); getResponse().addCookie(cookie);
} }
@ -308,12 +337,16 @@ public class TimiSpring {
} }
/** /**
* 获取请求头的令牌,键为 Token * 获取请求令牌,键为 Token 或 token包括请求头和 URI
* *
* @return 令牌 * @return 令牌
*/ */
public static String getToken() { public static String getToken() {
return getHeader("Token"); return TimiJava.firstNotEmpty(getHeader("Token"), getHeader("token"), getRequestArg("token"), getRequestArg("Token"));
}
public static String getLanguageRaw() {
return getHeader("Accept-Language");
} }
/** /**
@ -321,9 +354,18 @@ public class TimiSpring {
* @return 客户端地区语言 * @return 客户端地区语言
*/ */
public static Language getLanguage() { public static Language getLanguage() {
String name = TimiSpring.getHeader("Language"); String name = getRequestArg("lang");
if (TimiJava.isEmpty(name)) { if (TimiJava.isEmpty(name)) {
name = TimiSpring.getLocale().toString(); List<Locale.LanguageRange> rangeList = Locale.LanguageRange.parse(getLanguageRaw());
for (Locale.LanguageRange item : rangeList) {
if (item.getRange().contains("-")) {
name = item.getRange();
break;
}
}
}
if (TimiJava.isNotEmpty(name)) {
name = name.replace("-", "_");
} }
if (TimiJava.isEmpty(name)) { // use for not support if (TimiJava.isEmpty(name)) { // use for not support
return Language.zh_CN; return Language.zh_CN;
@ -357,4 +399,26 @@ public class TimiSpring {
public static boolean isLocalIP() { public static boolean isLocalIP() {
return getRequestIP().startsWith("127"); return getRequestIP().startsWith("127");
} }
public static RequestRange requestRange(long fileLength) throws IOException {
HttpServletResponse resp = getResponse();
String range = getRequestAttrAsString("Range");
if (range == null || !range.startsWith("bytes=")) {
return null;
}
// 处理 bytes=0-999 格式
String rangeValue = range.substring("bytes=".length());
String[] ranges = rangeValue.split("-");
TimiException.requiredTrue(2 == ranges.length, "Invalid Range format");
long start = Long.parseLong(ranges[0]);
long end = ranges[1].isEmpty() ? fileLength - 1 : Long.parseLong(ranges[1]);
// 验证范围有效性
if (start < 0 || fileLength <= end || end < start) {
resp.setHeader("Content-Range", "bytes */" + fileLength);
resp.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
return null;
}
return new RequestRange(start, end);
}
} }

View File

@ -0,0 +1,17 @@
package com.imyeyu.spring.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 图形验证码校验注解
*
* @author 夜雨
* @since 2023-07-15 10:09
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CaptchaValid {
}

View File

@ -0,0 +1,57 @@
package com.imyeyu.spring.annotation;
import com.imyeyu.spring.bean.CaptchaData;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
/**
* 图形验证码校验注解处理器
*
* @author 夜雨
* @since 2023-07-15 10:01
*/
@Aspect
public abstract class CaptchaValidAbstractInterceptor {
private boolean enable = true;
/** 注入注解 */
@Pointcut("@annotation(com.imyeyu.spring.annotation.CaptchaValid)")
public void captchaPointCut() {
}
/**
* 执行前
*
* @param joinPoint 切入点
*/
@Before("captchaPointCut()")
public void doBefore(JoinPoint joinPoint) {
if (!enable) {
return;
}
if (joinPoint.getSignature() instanceof MethodSignature ms) {
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof CaptchaData<?> captchaData) {
// 校验请求参数的验证码
verify(captchaData.getCaptchaId(), captchaData.getCaptcha());
break;
}
}
}
}
protected abstract void verify(String captchaId, String captcha);
public void enable() {
enable = true;
}
public void disable() {
enable = false;
}
}

View File

@ -1,8 +1,6 @@
package com.imyeyu.spring.bean; package com.imyeyu.spring.bean;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/** /**
* 含验证码数据实体 * 含验证码数据实体
@ -14,23 +12,21 @@ public class CaptchaData<T> {
/** 来源 */ /** 来源 */
@NotBlank(message = "timijava.code.request_bad") @NotBlank(message = "timijava.code.request_bad")
protected String from; protected String captchaId;
/** 验证码 */ /** 验证码 */
@NotBlank(message = "captcha.miss") @NotBlank(message = "captcha.miss")
protected String captcha; protected String captcha;
/** 数据体 */ /** 数据体 */
@Valid
@NotNull
protected T data; protected T data;
public String getFrom() { public String getCaptchaId() {
return from; return captchaId;
} }
public void setFrom(String from) { public void setCaptchaId(String captchaId) {
this.from = from; this.captchaId = captchaId;
} }
public String getCaptcha() { public String getCaptcha() {

View File

@ -0,0 +1,105 @@
package com.imyeyu.spring.bean;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.entity.UUIDEntity;
/**
* @author 夜雨
* @since 2025-10-17 15:21
*/
public class Multilingual extends UUIDEntity {
protected String key;
protected String zhCN;
protected String zhTW;
protected String enUS;
protected String ruRU;
protected String koKR;
protected String jaJP;
protected String deDE;
/**
* 获取指定语言值
*
* @param language 指定语言
* @return 值
*/
public String getValue(com.imyeyu.java.bean.Language language) {
try {
return Ref.getFieldValue(this, language.toString().replace("_", ""), String.class);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getZhCN() {
return zhCN;
}
public void setZhCN(String zhCN) {
this.zhCN = zhCN;
}
public String getZhTW() {
return zhTW;
}
public void setZhTW(String zhTW) {
this.zhTW = zhTW;
}
public String getEnUS() {
return enUS;
}
public void setEnUS(String enUS) {
this.enUS = enUS;
}
public String getRuRU() {
return ruRU;
}
public void setRuRU(String ruRU) {
this.ruRU = ruRU;
}
public String getKoKR() {
return koKR;
}
public void setKoKR(String koKR) {
this.koKR = koKR;
}
public String getJaJP() {
return jaJP;
}
public void setJaJP(String jaJP) {
this.jaJP = jaJP;
}
public String getDeDE() {
return deDE;
}
public void setDeDE(String deDE) {
this.deDE = deDE;
}
}

View File

@ -1,8 +1,9 @@
package com.imyeyu.spring.bean; package com.imyeyu.spring.bean;
import jakarta.validation.constraints.Max; import com.imyeyu.java.TimiJava;
import jakarta.validation.constraints.Min; import com.imyeyu.java.bean.BasePage;
import com.imyeyu.spring.mapper.BaseMapper; import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.utils.Text;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
@ -12,18 +13,7 @@ import java.util.LinkedHashMap;
* @author 夜雨 * @author 夜雨
* @version 2023-06-02 14:47 * @version 2023-06-02 14:47
*/ */
public class Page { public class Page extends BasePage {
/** 下标 */
@Min(value = 0, message = "page.min_index")
protected int index = 0;
/** 数据量 */
@Max(value = 64, message = "page.max_size")
protected int size = 16;
/** 关键字 */
protected String keyword;
protected LinkedHashMap<String, BaseMapper.OrderType> orderMap; protected LinkedHashMap<String, BaseMapper.OrderType> orderMap;
@ -31,8 +21,7 @@ public class Page {
} }
public Page(int index, int size) { public Page(int index, int size) {
this.index = index; super(index, size);
this.size = size;
} }
public long getOffset() { public long getOffset() {
@ -43,30 +32,6 @@ public class Page {
return size; return size;
} }
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public int getSize() {
return size;
}
public void setSize(int size) {
this.size = size;
}
public String getKeyword() {
return keyword;
}
public void setKeyword(String keyword) {
this.keyword = keyword;
}
public LinkedHashMap<String, BaseMapper.OrderType> getOrderMap() { public LinkedHashMap<String, BaseMapper.OrderType> getOrderMap() {
return orderMap; return orderMap;
} }
@ -75,8 +40,13 @@ public class Page {
this.orderMap = orderMap; this.orderMap = orderMap;
} }
public void addOrder(String field, BaseMapper.OrderType orderType) {
orderMap = TimiJava.firstNotNull(orderMap, new LinkedHashMap<>());
orderMap.put(Text.camelCase2underscore(field), orderType);
}
public static <T, P extends Page, R extends PageResult<T>> R toResult(BaseMapper<T, ?> pageMapper, P page, R result) { public static <T, P extends Page, R extends PageResult<T>> R toResult(BaseMapper<T, ?> pageMapper, P page, R result) {
result.setList(pageMapper.list(page.getOffset(), page.getLimit())); result.setList(pageMapper.listOrder(page.getOffset(), page.getLimit(), page.getOrderMap()));
result.setTotal(pageMapper.count()); result.setTotal(pageMapper.count());
return result; return result;
} }

View File

@ -1,9 +1,6 @@
package com.imyeyu.spring.bean; package com.imyeyu.spring.bean;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.BasePageResult;
import com.imyeyu.utils.Calc;
import java.util.List;
/** /**
* 抽象页面查询结果 * 抽象页面查询结果
@ -11,49 +8,5 @@ import java.util.List;
* @author 夜雨 * @author 夜雨
* @version 2023-06-02 14:47 * @version 2023-06-02 14:47
*/ */
public class PageResult<T> { public class PageResult<T> extends BasePageResult<T> {
/** 总数据量 */
protected long total;
/** 总页数 */
protected int pages;
protected List<T> list;
/**
* 获取总数据量
*
* @return 总数据量
*/
public long getTotal() {
return total;
}
/**
* 设置总数据量
*
* @param total 总数据量
*/
public void setTotal(long total) {
this.total = total;
if (TimiJava.isNotEmpty(list)) {
pages = Calc.ceil(1D * total / list.size());
}
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
if (TimiJava.isNotEmpty(list)) {
pages = Calc.ceil(1D * total / list.size());
}
}
public int getPages() {
return pages;
}
} }

View File

@ -0,0 +1,39 @@
package com.imyeyu.spring.bean;
/**
* @author 夜雨
* @since 2025-07-14 17:09
*/
public class RequestRange {
private long start;
private long end;
private long length;
public RequestRange(long start, long end) {
this.start = start;
this.end = end;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public long getEnd() {
return end;
}
public void setEnd(long end) {
this.end = end;
}
public long getLength() {
return end - start + 1;
}
}

View File

@ -0,0 +1,57 @@
package com.imyeyu.spring.handler;
import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import com.imyeyu.java.TimiJava;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* MySQL JSON 数据类型处理器
*
* @author 夜雨
* @since 2021-07-04 09:36
*/
public class GsonHandler extends BaseTypeHandler<JsonElement> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, JsonElement parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, parameter.toString());
}
@Override
public JsonElement getNullableResult(ResultSet rs, String columnName) throws SQLException {
return toElement(rs.getString(columnName));
}
@Override
public JsonElement getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return toElement(rs.getString(columnIndex));
}
@Override
public JsonElement getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return toElement(cs.getNString(columnIndex));
}
private JsonElement toElement(String json) {
if (TimiJava.isNotEmpty(json)) {
JsonElement el = JsonParser.parseString(json);
if (el.isJsonObject()) {
return el.getAsJsonObject();
}
if (el.isJsonArray()) {
return el.getAsJsonArray();
}
if (el.isJsonPrimitive()) {
return el.getAsJsonPrimitive();
}
}
return null;
}
}

View File

@ -8,6 +8,7 @@ import org.apache.ibatis.annotations.SelectProvider;
import org.apache.ibatis.annotations.UpdateProvider; import org.apache.ibatis.annotations.UpdateProvider;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 基本 SQL 映射,子接口可以不实现 * 基本 SQL 映射,子接口可以不实现
@ -43,8 +44,13 @@ public interface BaseMapper<T, P> {
* *
* @return 数据量 * @return 数据量
*/ */
@SelectProvider(type = SQLProvider.class, method = "count")
long count(); long count();
default List<T> list(long offset, int limit) {
return listOrder(offset, limit, null);
}
/** /**
* 获取部分数据 * 获取部分数据
* *
@ -52,7 +58,11 @@ public interface BaseMapper<T, P> {
* @param limit 数据量 * @param limit 数据量
* @return 数据列表 * @return 数据列表
*/ */
List<T> list(long offset, int limit); @SelectProvider(type = SQLProvider.class, method = "listOrder")
List<T> listOrder(long offset, int limit, Map<String, OrderType> orderMap);
@SelectProvider(type = SQLProvider.class, method = "listAll")
List<T> listAll();
/** /**
* 创建数据。默认自增主键为 id如需修改请重写此接口 * 创建数据。默认自增主键为 id如需修改请重写此接口
@ -86,6 +96,9 @@ public interface BaseMapper<T, P> {
@UpdateProvider(type = SQLProvider.class, method = "update") @UpdateProvider(type = SQLProvider.class, method = "update")
void update(T t); void update(T t);
@UpdateProvider(type = SQLProvider.class, method = "updateSelective")
void updateSelective(T t);
/** /**
* 软删除 * 软删除
* *

View File

@ -47,7 +47,7 @@ public abstract class AbstractEntityService<T, P> implements BaseService<T, P> {
public void update(T t) { public void update(T t) {
checkMapper(); checkMapper();
baseMapper.update(t); baseMapper.updateSelective(t);
} }
public void delete(P p) { public void delete(P p) {

View File

@ -1,10 +1,10 @@
package com.imyeyu.spring.util; package com.imyeyu.spring.util;
import jakarta.servlet.ServletException;
import jakarta.validation.ValidationException;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException; import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.bean.timi.TimiResponse; import com.imyeyu.java.bean.timi.TimiResponse;
import jakarta.servlet.ServletException;
import jakarta.validation.ValidationException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.TypeMismatchException; import org.springframework.beans.TypeMismatchException;
@ -41,7 +41,7 @@ public class GlobalExceptionHandler {
if (env.contains("dev") || log.isDebugEnabled()) { if (env.contains("dev") || log.isDebugEnabled()) {
log.error("conversion error", e); log.error("conversion error", e);
} }
return new TimiResponse<>(TimiCode.ARG_BAD); return new TimiResponse<>(TimiCode.ARG_BAD).msgKey("invalid.body");
} }
/** /**
@ -56,7 +56,7 @@ public class GlobalExceptionHandler {
if (env.contains("dev") || log.isDebugEnabled()) { if (env.contains("dev") || log.isDebugEnabled()) {
log.error("header error", e); log.error("header error", e);
} }
return new TimiResponse<>(TimiCode.REQUEST_BAD); return new TimiResponse<>(TimiCode.REQUEST_BAD).msgKey("invalid.request");
} }
/** /**
@ -71,13 +71,13 @@ public class GlobalExceptionHandler {
log.warn("request error", e); log.warn("request error", e);
FieldError error = subE.getBindingResult().getFieldError(); FieldError error = subE.getBindingResult().getFieldError();
if (error != null) { if (error != null) {
return new TimiResponse<>(TimiCode.ARG_BAD, error.getDefaultMessage()); return new TimiResponse<>(TimiCode.ARG_BAD, "[%s] %s".formatted(error.getField(), error.getDefaultMessage()));
} }
} }
if (env.startsWith("dev") || log.isDebugEnabled()) { if (env.startsWith("dev") || log.isDebugEnabled()) {
log.error("request error", e); log.error("request error", e);
} }
return new TimiResponse<>(TimiCode.REQUEST_BAD); return new TimiResponse<>(TimiCode.REQUEST_BAD).msgKey("invalid.arg");
} }
/** /**
@ -89,8 +89,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(Throwable.class) @ExceptionHandler(Throwable.class)
public TimiResponse<?> error(Throwable e) { public TimiResponse<?> error(Throwable e) {
if (e instanceof TimiException timiE) { if (e instanceof TimiException timiE) {
// TODO 400 以下即使是开发环境也不算异常 if (!env.startsWith("prod") || log.isDebugEnabled()) {
if (env.startsWith("dev") || log.isDebugEnabled()) {
log.error(timiE.getMessage(), e); log.error(timiE.getMessage(), e);
} }
// 一般异常 // 一般异常
@ -98,6 +97,6 @@ public class GlobalExceptionHandler {
} }
// 致命异常 // 致命异常
log.error("fatal error", e); log.error("fatal error", e);
return new TimiResponse<>(TimiCode.ERROR); return new TimiResponse<>(TimiCode.ERROR).msgKey("service.error");
} }
} }

View File

@ -2,6 +2,7 @@ package com.imyeyu.spring.util;
import com.imyeyu.java.TimiJava; import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArgReturn; import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.java.bean.LanguageMsgMapping;
import com.imyeyu.java.bean.timi.TimiCode; import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiResponse; import com.imyeyu.java.bean.timi.TimiResponse;
import com.imyeyu.spring.TimiSpring; import com.imyeyu.spring.TimiSpring;
@ -31,7 +32,7 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class); private static final Logger log = LoggerFactory.getLogger(GlobalReturnHandler.class);
private CallbackArgReturn<String, String> multilingualHeader; private CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader;
@Override @Override
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) { public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
@ -54,22 +55,27 @@ public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
} else { } else {
result = new TimiResponse<>(TimiCode.SUCCESS, body); result = new TimiResponse<>(TimiCode.SUCCESS, body);
} }
try {
if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) { if (multilingualHeader != null && TimiJava.isNotEmpty(result.getMsgKey())) {
result.setMsg(multilingualHeader.handler(result.getMsgKey())); result.setMsg(multilingualHeader.handler(result));
} else if (TimiJava.isEmpty(result.getMsg())) { } else if (TimiJava.isEmpty(result.getMsg())) {
result.setMsg(TimiCode.fromCode(result.getCode()).toString()); result.setMsg(TimiCode.fromCode(result.getCode()).toString());
} }
} catch (Exception e) {
log.error("multilingual response error", e);
result.setMsg(TimiCode.fromCode(result.getCode()).toString());
}
if (30000 < result.getCode()) { if (30000 < result.getCode()) {
log.warn("ID: {} Response -> Exception.{}.{}", TimiSpring.getSessionAttr(AOPLogInterceptor.REQUEST_ID), result.getCode(), result.getMsg()); log.warn("ID: {} Response -> Exception.{}.{}", TimiSpring.getSessionAttr(AOPLogInterceptor.REQUEST_ID), result.getCode(), result.getMsg());
} }
return result; return result;
} }
public CallbackArgReturn<String, String> getMultilingualHeader() { public CallbackArgReturn<LanguageMsgMapping<?>, String> getMultilingualHeader() {
return multilingualHeader; return multilingualHeader;
} }
public void setMultilingualHeader(CallbackArgReturn<String, String> multilingualHeader) { public void setMultilingualHeader(CallbackArgReturn<LanguageMsgMapping<?>, String> multilingualHeader) {
this.multilingualHeader = multilingualHeader; this.multilingualHeader = multilingualHeader;
} }
} }

View File

@ -9,6 +9,7 @@ import com.imyeyu.spring.annotation.table.Column;
import com.imyeyu.spring.annotation.table.Id; import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.Table; import com.imyeyu.spring.annotation.table.Table;
import com.imyeyu.spring.annotation.table.Transient; import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.BaseEntity;
import com.imyeyu.spring.entity.Creatable; import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable; import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.Destroyable; import com.imyeyu.spring.entity.Destroyable;
@ -40,6 +41,51 @@ public class SQLProvider {
/** 反射缓存 */ /** 反射缓存 */
private static final Map<Class<?>, EntityMeta> ENTITY_META_CACHE = new ConcurrentHashMap<>(); private static final Map<Class<?>, EntityMeta> ENTITY_META_CACHE = new ConcurrentHashMap<>();
public String count(ProviderContext context) {
EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder();
sql.append("SELECT COUNT(*) FROM `%s` WHERE 1 = 1".formatted(meta.table));
if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
return sql.toString();
}
public String listOrder(ProviderContext context, @Param("offset") Long offset, @Param("limit") Integer limit, @Param("orderMap") Map<String, BaseMapper.OrderType> orderMap) {
EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder();
sql.append("SELECT %s FROM `%s` WHERE 1 = 1".formatted(meta.selectAllClause, meta.table));
if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
}
if (TimiJava.isNotEmpty(orderMap)) {
sql.append(" ORDER BY ");
for (Map.Entry<String, BaseMapper.OrderType> item : orderMap.entrySet()) {
sql.append(Text.camelCase2underscore(item.getKey())).append(' ').append(item.getValue().toString());
sql.append(", ");
}
sql.deleteCharAt(sql.length() - 2);
} else {
if (meta.canCreate && !meta.canUpdate) {
sql.append(" ORDER BY created_at DESC");
}
if (meta.canCreate && meta.canUpdate) {
sql.append(" ORDER BY COALESCE(updated_at, created_at) DESC");
}
}
return sql.append(" LIMIT %s, %s".formatted(offset, limit)).toString();
}
public String listAll(ProviderContext context) {
EntityMeta meta = getEntityMeta(context);
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM %s WHERE 1 = 1".formatted(meta.table));
if (meta.canDelete) {
sql.append(BaseMapper.NOT_DELETE);
}
return sql.toString();
}
/** /**
* 插入 * 插入
* <p><i>不实现 {@link Creatable} 也允许调用是合理的,某些数据属于关联数据,不参与主创建过程</i></p> * <p><i>不实现 {@link Creatable} 也允许调用是合理的,某些数据属于关联数据,不参与主创建过程</i></p>
@ -82,7 +128,7 @@ public class SQLProvider {
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass)); TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
StringBuilder sql = new StringBuilder(); StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM `%s` WHERE `%s` = #{%s}".formatted(meta.table, meta.idFieldColumn.columnName, id)); sql.append("SELECT %s FROM `%s` WHERE `%s` = #{%s}".formatted(meta.selectAllClause, meta.table, meta.idFieldColumn.columnName, id));
if (meta.canDelete) { if (meta.canDelete) {
sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now())); sql.append(" AND (`deleted_at` IS NULL OR %s < `deleted_at`)".formatted(Time.now()));
} }
@ -121,7 +167,7 @@ public class SQLProvider {
.collect(Collectors.joining(" AND ")); .collect(Collectors.joining(" AND "));
StringBuilder sql = new StringBuilder(); StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM `%s` WHERE %s".formatted(meta.table, conditionClause)); sql.append("SELECT %s FROM `%s` WHERE %s".formatted(meta.selectAllClause, meta.table, conditionClause));
if (meta.canDelete) { if (meta.canDelete) {
if (TimiJava.isNotEmpty(conditionClause)) { if (TimiJava.isNotEmpty(conditionClause)) {
sql.append(" AND "); sql.append(" AND ");
@ -151,7 +197,43 @@ public class SQLProvider {
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName); return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
}) })
.collect(Collectors.joining(", ")); .collect(Collectors.joining(", "));
return "UPDATE `%s` SET `%s` WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName); return "UPDATE `%s` SET %s WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
}
/**
* 根据 ID 更新,选择性更新非空属性,需要实体实现 {@link Updatable}
*
* @param entity 实体
* @return SQL
*/
public String updateSelective(Object entity) {
EntityMeta meta = getEntityMeta(entity.getClass());
TimiException.required(meta.idFieldColumn, "not found id field in %s".formatted(meta.entityClass));
TimiException.required(meta.canUpdate, "not allow update for %s".formatted(meta.entityClass));
if (entity instanceof BaseEntity baseEntity) {
baseEntity.setCreatedAt(null);
baseEntity.setDeletedAt(null);
}
String setClause = meta.fieldColumnList.stream()
.filter(fc -> {
if (fc.isId) {
return false;
}
try {
return Ref.getFieldValue(entity, fc.field, Object.class) != null;
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
})
.map(fc -> {
if (entity instanceof Updatable updatableEntity) {
updatableEntity.setUpdatedAt(Time.now());
}
return "`%s` = #{%s}".formatted(fc.columnName, fc.fieldName);
})
.collect(Collectors.joining(", "));
return "UPDATE `%s` SET %s WHERE `%s` = #{%s}".formatted(meta.table, setClause, meta.idFieldColumn.columnName, meta.idFieldColumn.fieldName);
} }
/** /**
@ -190,7 +272,7 @@ public class SQLProvider {
* @param context 代理器上下文 * @param context 代理器上下文
* @return 实体类元数据 * @return 实体类元数据
*/ */
private EntityMeta getEntityMeta(ProviderContext context) { protected EntityMeta getEntityMeta(ProviderContext context) {
Type[] types = context.getMapperType().getGenericInterfaces(); Type[] types = context.getMapperType().getGenericInterfaces();
ParameterizedType type = (ParameterizedType) types[0]; ParameterizedType type = (ParameterizedType) types[0];
Class<?> entityClass = (Class<?>) type.getActualTypeArguments()[0]; Class<?> entityClass = (Class<?>) type.getActualTypeArguments()[0];
@ -203,7 +285,7 @@ public class SQLProvider {
* @param entityClass 实体类 * @param entityClass 实体类
* @return 元数据 * @return 元数据
*/ */
private EntityMeta getEntityMeta(Class<?> entityClass) { protected EntityMeta getEntityMeta(Class<?> entityClass) {
return ENTITY_META_CACHE.computeIfAbsent(entityClass, EntityMeta::new); return ENTITY_META_CACHE.computeIfAbsent(entityClass, EntityMeta::new);
} }
@ -213,7 +295,7 @@ public class SQLProvider {
* @author 夜雨 * @author 夜雨
* @since 2025-02-05 23:47 * @since 2025-02-05 23:47
*/ */
private static class EntityMeta { protected static class EntityMeta {
/** 实体类 */ /** 实体类 */
final Class<?> entityClass; final Class<?> entityClass;
@ -221,12 +303,18 @@ public class SQLProvider {
/** 表名 */ /** 表名 */
final String table; final String table;
/** 查询字段映射 */
final String selectAllClause;
/** ID 字段 */ /** ID 字段 */
final FieldColumn idFieldColumn; final FieldColumn idFieldColumn;
/** 只读的列名字段名映射Map&lt;列名,字段名&gt; */ /** 只读的列名字段名映射Map&lt;列名,字段名&gt; */
final List<FieldColumn> fieldColumnList; final List<FieldColumn> fieldColumnList;
/** true 为可创建 */
final boolean canCreate;
/** true 为可更新 */ /** true 为可更新 */
final boolean canUpdate; final boolean canUpdate;
@ -252,6 +340,7 @@ public class SQLProvider {
} }
List<Field> allFieldList = Ref.listAllFields(entityClass); List<Field> allFieldList = Ref.listAllFields(entityClass);
StringBuilder selectAllClause = new StringBuilder();
FieldColumn idFieldColumn = null; FieldColumn idFieldColumn = null;
List<FieldColumn> fieldColumnList = new ArrayList<>(); List<FieldColumn> fieldColumnList = new ArrayList<>();
for (int i = 0; i < allFieldList.size(); i++) { for (int i = 0; i < allFieldList.size(); i++) {
@ -264,14 +353,64 @@ public class SQLProvider {
TimiException.requiredNull(idFieldColumn, String.format("multi id field for %s entity", entityClass.getName())); TimiException.requiredNull(idFieldColumn, String.format("multi id field for %s entity", entityClass.getName()));
idFieldColumn = fieldColumn; idFieldColumn = fieldColumn;
} }
{
Column column = field.getAnnotation(Column.class);
if (column == null) {
selectAllClause.append('`').append(fieldColumn.columnName).append('`');
selectAllClause.append(',');
} else {
// 处理自定义映射列名
selectAllClause.append('`').append(column.value()).append('`');
selectAllClause.append(" AS `").append(fieldColumn.fieldName).append('`');
selectAllClause.append(',');
}
}
fieldColumnList.add(fieldColumn); fieldColumnList.add(fieldColumn);
} }
this.selectAllClause = selectAllClause.substring(0, selectAllClause.length() - 1);
this.idFieldColumn = idFieldColumn; this.idFieldColumn = idFieldColumn;
this.fieldColumnList = List.of(fieldColumnList.toArray(new FieldColumn[0])); // 转为只读 this.fieldColumnList = List.of(fieldColumnList.toArray(new FieldColumn[0])); // 转为只读
canCreate = Creatable.class.isAssignableFrom(entityClass);
canUpdate = Updatable.class.isAssignableFrom(entityClass); canUpdate = Updatable.class.isAssignableFrom(entityClass);
canDelete = Deletable.class.isAssignableFrom(entityClass); canDelete = Deletable.class.isAssignableFrom(entityClass);
canDestroy = Destroyable.class.isAssignableFrom(entityClass); canDestroy = Destroyable.class.isAssignableFrom(entityClass);
} }
public Class<?> getEntityClass() {
return entityClass;
}
public String getTable() {
return table;
}
public String getSelectAllClause() {
return selectAllClause;
}
public FieldColumn getIdFieldColumn() {
return idFieldColumn;
}
public List<FieldColumn> getFieldColumnList() {
return fieldColumnList;
}
public boolean canCreate() {
return canCreate;
}
public boolean canUpdate() {
return canUpdate;
}
public boolean canDelete() {
return canDelete;
}
public boolean canDestroy() {
return canDestroy;
}
} }
/** /**
@ -280,7 +419,7 @@ public class SQLProvider {
* @author 夜雨 * @author 夜雨
* @since 2025-02-07 09:54 * @since 2025-02-07 09:54
*/ */
private static class FieldColumn { protected static class FieldColumn {
/** 字段 */ /** 字段 */
final Field field; final Field field;
@ -318,5 +457,29 @@ public class SQLProvider {
isAutoUpperUUID = false; isAutoUpperUUID = false;
} }
} }
public Field getField() {
return field;
}
public String getFieldName() {
return fieldName;
}
public String getColumnName() {
return columnName;
}
public boolean isId() {
return isId;
}
public boolean isAutoUUID() {
return isAutoUUID;
}
public boolean isAutoUpperUUID() {
return isAutoUpperUUID;
}
} }
} }

View File

@ -0,0 +1,24 @@
package com.imyeyu.spring.util;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import java.io.IOException;
import java.util.List;
/**
*
*
* @author 夜雨
* @since 2025-10-13 16:29
*/
public class YamlPropertySourceFactory implements PropertySourceFactory {
@Override
public @org.springframework.lang.NonNull PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
List<PropertySource<?>> sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());
return sources.get(0);
}
}

View File

@ -0,0 +1,5 @@
invalid.arg=无效的参数
invalid.body=无效的请求体
invalid.header=无效的请求头
invalid.request=无效的请求
service.error=服务错误