Initial project

This commit is contained in:
Timi
2025-07-08 16:31:30 +08:00
parent 1c6a45c8c2
commit ae0f56a6dc
356 changed files with 21123 additions and 109 deletions

View File

@ -0,0 +1,75 @@
package com.imyeyu.server;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.utils.OS;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.Environment;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import java.io.File;
/**
* 夜雨综合数据中心接口
*
* <p>本端所有接口面向用户,不做管理接口,数据管理将使用 JavaFX
*
* @author 夜雨
* @since 2021-02-23 21:35
*/
@Slf4j
@SpringBootApplication(scanBasePackages = {"com.imyeyu.server", "com.imyeyu.spring"})
@EnableTransactionManagement
public class TimiServerAPI implements OS.FileSystem, ApplicationContextAware {
private static final String DEV_LANG_CONFIG = "dev.lang";
public static ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
TimiServerAPI.applicationContext = applicationContext;
}
public static Language getUserLanguage() {
Language userLanguage = TimiSpring.getLanguage();
Environment env = applicationContext.getBean(Environment.class);
if (env.containsProperty(DEV_LANG_CONFIG)) {
String property = env.getProperty(DEV_LANG_CONFIG);
if (TimiJava.isNotEmpty(property)) {
userLanguage = Ref.toType(Language.class, property);
}
}
return userLanguage;
}
public static void main(String[] args) {
try {
{
// 导出配置
String[] files = {"application.yml", "logback.xml"};
for (int i = 0; i < files.length; i++) {
File file = new File("config" + SEP + files[i]);
if (!file.exists() || !file.isFile()) {
log.info("exporting default config at {}", file.getAbsolutePath());
IO.resourceToDisk(TimiServerAPI.class, files[i], file.getAbsolutePath());
}
}
}
// 启动 SpringBoot
SpringApplication.run(TimiServerAPI.class, args);
} catch (Exception e) {
log.error("launch error", e);
}
}
}

View File

@ -0,0 +1,22 @@
package com.imyeyu.server.annotation;
import com.imyeyu.server.bean.CaptchaFrom;
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 {
/** @return 验证码来源 */
CaptchaFrom value();
}

View File

@ -0,0 +1,70 @@
package com.imyeyu.server.annotation;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.util.CaptchaManager;
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;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
/**
* 图形验证码校验注解处理器
*
* @author 夜雨
* @since 2023-07-15 10:01
*/
@Slf4j
@Aspect
@Component
public class CaptchaValidInterceptor {
@Value("${spring.profiles.active}")
private String env;
@Autowired
private CaptchaManager captchaManager;
/** 注入注解 */
@Pointcut("@annotation(com.imyeyu.server.annotation.CaptchaValid)")
public void captchaPointCut() {
}
/**
* 执行前
*
* @param joinPoint 切入点
*/
@Before("captchaPointCut()")
public void doBefore(JoinPoint joinPoint) {
try {
if (env.startsWith("dev")) {
// 开发环境不校验
return;
}
if (joinPoint.getSignature() instanceof MethodSignature ms) {
Method method = joinPoint.getTarget().getClass().getMethod(ms.getName(), ms.getParameterTypes());
CaptchaValid annotation = method.getAnnotation(CaptchaValid.class);
CaptchaFrom from = annotation.value();
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof CaptchaData<?> captchaData) {
// 校验请求参数的验证码
captchaManager.test(captchaData.getCaptcha(), from.toString());
break;
}
}
}
} catch (NoSuchMethodException e) {
throw new RuntimeException("TODO CaptchaValidInterceptor error");
}
}
}

View File

@ -0,0 +1,25 @@
package com.imyeyu.server.annotation;
import com.imyeyu.server.modules.common.bean.SettingKey;
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:00
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EnableSetting {
/** @return 配置键 */
SettingKey value();
/** @return 未启用配置时响应消息语言映射键 */
String message() default "service.offline";
}

View File

@ -0,0 +1,40 @@
package com.imyeyu.server.annotation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.common.service.SettingService;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
/**
* 启用配置注解处理器
*
* @author 夜雨
* @since 2023-07-15 10:01
*/
@Component
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class EnableSettingInterceptor implements HandlerInterceptor {
private final SettingService service;
public boolean preHandle(@NonNull HttpServletRequest req, @NonNull HttpServletResponse resp, @NonNull Object handler) {
if (handler instanceof HandlerMethod handlerMethod) {
EnableSetting annotation = handlerMethod.getMethodAnnotation(EnableSetting.class);
if (annotation == null) {
return true;
}
if (service.is(annotation.value())) {
return true;
}
throw new TimiException(TimiCode.ERROR_SERVICE_OFF, annotation.message());
}
return true;
}
}

View File

@ -0,0 +1,49 @@
package com.imyeyu.server.annotation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.RequestRateLimitAbstractInterceptor;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Time;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
/**
* 请求频率限制处理器
*
* @author 夜雨
* @since 2021-08-16 18:07
*/
@Slf4j
@Component
public class RequestRateLimitInterceptor extends RequestRateLimitAbstractInterceptor {
@Autowired
@Qualifier("redisRateLimit")
private Redis<String, Integer> redisRequestRateLimit;
@Override
public boolean beforeRun(HttpServletRequest req, HttpServletResponse resp, String id, int cycle, int limit) {
// 键
String key = "TimiServerAPI." + TimiSpring.getRequestIP() + "." + id;
if (redisRequestRateLimit.has(key)) {
Integer count = redisRequestRateLimit.get(key);
if (count != null) {
if (count < limit) {
redisRequestRateLimit.setAndKeepTTL(key, ++count);
} else {
log.warn("请求频率过高:[" + key + "].C" + count + "L" + limit);
throw new TimiException(TimiCode.REQUEST_BAD).msgKey("request_rate_limit");
}
}
return true;
}
redisRequestRateLimit.set(key, 0, Time.S * cycle);
return true;
}
}

View File

@ -0,0 +1,39 @@
package com.imyeyu.server.annotation;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.blog.util.UserToken;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.annotation.RequiredTokenAbstractInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 令牌验证注解处理器
*
* @author 夜雨
* @since 2021-08-16 18:07
*/
@Slf4j
@Component
public class RequiredTokenInterceptor extends RequiredTokenAbstractInterceptor<RequiredToken> {
@Autowired
private UserToken userToken;
public RequiredTokenInterceptor() {
super(RequiredToken.class);
}
@Override
protected boolean beforeRun(HttpServletRequest req, HttpServletResponse resp) {
if (userToken.isInvalid(TimiSpring.getToken())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("token.illegal");
}
return true;
}
}

View File

@ -0,0 +1,34 @@
package com.imyeyu.server.bean;
/**
* 验证码来源
*
* @author 夜雨
* @since 2023-07-16 10:12
*/
public enum CaptchaFrom {
/** 注册 */
REGISTER,
/** 登录 */
LOGIN,
/** 忘记密码 */
RESET_PASSWORD,
/** 评论 */
COMMENT,
/** 评论回复 */
COMMENT_REPLY,
/** Git 反馈 */
GIT_ISSUE,
/** Git 合并请求 */
GIT_MERGE,
/** 歌词修正申请 */
LYRIC_CORRECT
}

View File

@ -0,0 +1,18 @@
package com.imyeyu.server.bean;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.yaml.snakeyaml.Yaml;
/**
* @author 夜雨
* @since 2025-01-13 11:42
*/
@Component
public class IOCBeans {
@Bean
public Yaml yaml() {
return new Yaml();
}
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.server.bean;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author 夜雨
* @since 2023-10-25 10:10
*/
public interface MultilingualHandler {
/**
*
*
* @author 夜雨
* @since 2023-10-25 10:25
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@interface MultilingualField {
String[] args() default {};
}
}

View File

@ -0,0 +1,30 @@
package com.imyeyu.server.bean;
import lombok.Data;
import com.imyeyu.utils.OS;
import java.io.InputStream;
/**
* 资源文件
*
* @author 夜雨
* @since 2021-07-31 15:31
*/
@Data
public class ResourceFile implements OS.FileSystem {
/** 服务器文件所在路径 */
private String path;
/** 文件名 */
private String name;
/** 文件数据流 */
private InputStream inputStream;
/** @return 完整绝对路径 */
public String getFullPath() {
return path + SEP + name;
}
}

View File

@ -0,0 +1,35 @@
package com.imyeyu.server.config;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import java.util.Arrays;
/**
* 异步线程池配置
*
* @author 夜雨
* @since 2023-08-21 16:22
*/
@Slf4j
@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public @NotNull AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (e, method, obj) -> {
log.info("Exception message - {}", e.getMessage());
log.info("Method name - {}", method.getName());
log.info("Parameter values - {}", Arrays.toString(obj));
if (e instanceof Exception exception) {
log.info("exception: {}", exception.getMessage());
}
log.error("async uncaught error", e);
};
}
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.config;
import com.google.gson.Gson;
import org.springframework.context.annotation.Configuration;
/**
* @author 夜雨
* @since 2025-05-16 18:53
*/
@Configuration
public class BeanConfig {
public Gson gson() {
return new Gson();
}
}

View File

@ -0,0 +1,55 @@
package com.imyeyu.server.config;
import jakarta.servlet.Filter;
import lombok.Data;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Arrays;
/**
* 跨域控制
*
* @author 夜雨
* @since 2021-05-14 09:21
*/
@Data
@Configuration
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "cors")
public class CORSConfig {
/** 允许跨域的地址 */
private String[] allowOrigin;
/** 是否允许请求带有验证信息 */
private boolean allowCredentials;
/** 允许请求的方法 */
private String allowMethods;
/** 允许服务端访问的客户端请求头 */
private String allowHeaders;
@Bean
public FilterRegistrationBean<Filter> corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedHeader(allowHeaders);
config.addAllowedMethod(allowMethods);
config.setAllowCredentials(allowCredentials);
config.setAllowedOriginPatterns(Arrays.asList(allowOrigin));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
FilterRegistrationBean<Filter> bean = new FilterRegistrationBean<>(new CorsFilter(source));
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.server.config;
import com.mongodb.client.MongoClient;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSBuckets;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author 夜雨
* @since 2024-02-23 10:55
*/
@Configuration
public class MongoConfig {
@Value("${spring.data.mongodb.database}")
private String db;
@Bean
public GridFSBucket gridFSBucket(MongoClient mongoClient) {
return GridFSBuckets.create(mongoClient.getDatabase(db));
}
}

View File

@ -0,0 +1,268 @@
package com.imyeyu.server.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.common.entity.Multilingual;
import com.imyeyu.spring.bean.RedisConfigParams;
import com.imyeyu.spring.config.AbstractRedisConfig;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.spring.util.RedisSerializers;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.lang.Nullable;
import java.time.Duration;
/**
* Redis 配置
*
* @author 夜雨
* @since 2021-02-23 21:36
*/
@Data
@Configuration
@EqualsAndHashCode(callSuper = true)
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "spring.redis")
public class RedisConfig extends AbstractRedisConfig {
// ---------- 连接配置 ----------
/** 地址 */
private String host;
/** 端口 */
private int port;
/** 密码 */
private String password;
/** 超时(毫秒) */
private int timeout;
/** 连接池 */
private Lettuce lettuce;
/** 数据库 */
private Database database;
/**
* 连接池
*
* @author 夜雨
* @since 2023-08-21 16:23
*/
@Data
public static class Lettuce {
/** 配置 */
private Pool pool;
/**
* 配置
*
* @author 夜雨
* @since 2023-08-21 16:23
*/
@Data
public static class Pool {
/** 最大活跃连接 */
private int maxActive;
/** 最小空闲连接 */
private int minIdle;
/** 最大空闲连接 */
private int maxIdle;
/** 最大等待时间(秒) */
private int maxWait;
}
}
/**
* 数据库
*
* @author 夜雨
* @since 2023-08-21 16:25
*/
@Data
public static class Database {
/** 分布式锁 */
private int locker;
/** 多语言环境 */
private int language;
/** 多语言键环境 */
private int languageMap;
/** 文章排位 */
private int articleRanking;
/** 文章阅读记录 */
private int articleRead;
/** 用户登录令牌 */
private int userToken;
/** 用户经验值标记 */
private int userExpFlag;
/** 用户邮箱验证 */
private int userEmailVerify;
/** 用户重置密码验证 */
private int userResetPWVerify;
/** 访问频率控制 */
private int rateLimit;
/** 系统配置 */
private int setting;
/** Minecraft 登录 */
private int fmcPlayerToken;
}
@Override
protected RedisConfigParams configParams() {
return new RedisConfigParams() {{
setHost(host);
setPort(port);
setPassword(password);
setTimeout(timeout);
setMaxActive(lettuce.pool.maxActive);
setMinIdle(lettuce.pool.minIdle);
setMaxIdle(lettuce.pool.maxIdle);
}};
}
/** @return 连接池配置 */
@Bean
@Override
public GenericObjectPoolConfig<?> getPoolConfig() {
GenericObjectPoolConfig<?> config = new GenericObjectPoolConfig<>();
config.setMaxTotal(lettuce.pool.maxActive);
config.setMinIdle(lettuce.pool.minIdle);
config.setMaxIdle(lettuce.pool.maxIdle);
config.setMaxWait(Duration.ofMillis(lettuce.pool.maxWait));
return config;
}
/** @return key 生成策略 */
@Bean
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
};
}
/** @return 分布式锁, ID: 尝试加锁次数 */
@Bean("redisLocker")
public Redis<String, Integer> getLockerRedisTemplate() {
return getRedis(database.locker, RedisSerializers.STRING, RedisSerializers.INTEGER);
}
/** @return 多语言环境ID: {@link Multilingual} */
@Bean("redisLanguage")
public Redis<Long, Multilingual> getLanguageRedisTemplate() {
return getRedis(database.language, RedisSerializers.LONG, new RedisSerializer<>() {
public Multilingual deserialize(@Nullable byte[] bytes) {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
return (Multilingual) new DeserializingConverter().convert(bytes);
} catch (Exception var3) {
throw new SerializationException("Cannot deserialize", var3);
}
}
public byte[] serialize(@Nullable Multilingual multilingual) {
if (multilingual == null) {
return new byte[0];
} else {
try {
return new SerializingConverter().convert(multilingual);
} catch (Exception var3) {
throw new SerializationException("Cannot serialize", var3);
}
}
}
});
}
/** @return 文章访问记录IP: [文章 ID] */
@Bean("redisLanguageMap")
public Redis<String, Long> getLanguageMapRedisTemplate() {
return getRedis(database.languageMap, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 文章访问统计,文章 ID: {@link ArticleRanking}(JSON) */
@Bean("redisArticleRanking")
public Redis<Long, ArticleRanking> getArticleRankingRedisTemplate() {
return getRedis(database.articleRanking, RedisSerializers.LONG, RedisSerializers.gsonSerializer(ArticleRanking.class));
}
/** @return 文章访问记录IP: [文章 ID] */
@Bean("redisArticleRead")
public Redis<String, Long> getArticleReadRedisTemplate() {
return getRedis(database.articleRead, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 用户登录经验标记UID: NULL暂时没有值数据死亡时间为次日零时 */
@Bean("redisUserExpFlag")
public Redis<Long, String> getUserExpFlagRedisTemplate() {
return getRedis(database.userExpFlag, RedisSerializers.LONG, RedisSerializers.STRING);
}
/** @return 用户邮箱验证密钥,密钥: UID */
@Bean("redisUserEmailVerify")
public Redis<String, Long> getUserEmailVerifyRedisTemplate() {
return getRedis(database.userEmailVerify, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 用户重置密码密钥,密钥: UID */
@Bean("redisUserResetPWVerify")
public Redis<String, Long> getUserResetPWVerifyRedisTemplate() {
return getRedis(database.userResetPWVerify, RedisSerializers.STRING, RedisSerializers.LONG);
}
/** @return 接口访问控制IP#方法: 生命周期内访问次数 */
@Bean("redisRateLimit")
public Redis<String, Integer> getRateLimitRedisTemplate() {
return getRedis(database.rateLimit, RedisSerializers.STRING, RedisSerializers.INTEGER);
}
/** @return 系统配置Key 枚举: String 配置值 */
@Bean("redisSetting")
public Redis<String, String> getSettingRedisTemplate() {
return getRedis(database.setting, RedisSerializers.STRING, RedisSerializers.STRING);
}
/** @return Minecraft 登录,令牌: 玩家 ID */
@Bean("redisMCPlayerToken")
public Redis<String, Long> getMCPlayerLoginRedisTemplate() {
return getRedis(database.fmcPlayerToken, RedisSerializers.STRING, RedisSerializers.LONG);
}
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
/**
*
*
* @author 夜雨
* @since 2024-12-19 23:04
*/
@Configuration
public class SchedulerConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(32);
scheduler.initialize();
return scheduler;
}
@Bean
public ScheduledTaskRegistrar scheduleCronTask(TaskScheduler taskScheduler) {
ScheduledTaskRegistrar registrar = new ScheduledTaskRegistrar();
registrar.setTaskScheduler(taskScheduler);
registrar.afterPropertiesSet();
return registrar;
}
}

View File

@ -0,0 +1,57 @@
package com.imyeyu.server.config;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 线程池配置
*
* @author 夜雨
* @since 2023-08-21 16:31
*/
@Data
@Slf4j
@Configuration
@EnableAutoConfiguration
@ConfigurationProperties(prefix = "spring.async.thread-pool")
public class ThreadPoolConfig {
/** 核心数量 */
private int corePoolSize;
/** 最大数量 */
private int maxPoolSize;
/** 等待区容量 */
private int queueCapacity;
/** 最大保持活跃时间(秒) */
private int keepAliveSeconds;
/** 最大等待时间(秒) */
private int awaitTerminationSeconds;
/** 线程名称前缀 */
private String threadNamePrefix;
@Bean(name = "threadPoolTaskExecutor")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
executor.setKeepAliveSeconds(keepAliveSeconds);
executor.setAwaitTerminationSeconds(awaitTerminationSeconds);
executor.setThreadNamePrefix(threadNamePrefix);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
}

View File

@ -0,0 +1,94 @@
package com.imyeyu.server.config;
import com.google.gson.GsonBuilder;
import com.imyeyu.server.annotation.EnableSettingInterceptor;
import com.imyeyu.server.annotation.RequestRateLimitInterceptor;
import com.imyeyu.server.annotation.RequiredTokenInterceptor;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.vo.user.UserProfileView;
import com.imyeyu.server.modules.common.vo.user.UserView;
import com.imyeyu.server.modules.minecraft.annotation.RequiredFMCServerTokenInterceptor;
import com.imyeyu.server.modules.minecraft.entity.MinecraftPlayer;
import com.imyeyu.server.modules.mirror.vo.MirrorView;
import com.imyeyu.server.modules.system.util.SystemAPIInterceptor;
import com.imyeyu.server.util.GsonSerializerAdapter;
import com.imyeyu.spring.annotation.RequestSingleParamResolver;
import jakarta.validation.constraints.NotNull;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.GsonHttpMessageConverter;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.io.Writer;
import java.lang.reflect.Type;
import java.util.List;
/**
* 系统配置
*
* @author 夜雨
* @since 2021-07-20 16:44
*/
@EnableWebMvc
@Configuration
@RequiredArgsConstructor
public class WebConfig implements WebMvcConfigurer {
private final SystemAPIInterceptor systemAPIInterceptor;
private final GsonSerializerAdapter gsonSerializerAdapter;
private final RequiredTokenInterceptor requiredTokenInterceptor;
private final EnableSettingInterceptor enableSettingInterceptor;
private final RequestSingleParamResolver requestSingleParamResolver;
private final RequestRateLimitInterceptor requestRateLimitInterceptor;
private final RequiredFMCServerTokenInterceptor requiredFMCServerTokenInterceptor;
/**
* 过滤器
*
* @param registry 注册表
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(systemAPIInterceptor).addPathPatterns(SystemAPIInterceptor.PATH);
registry.addInterceptor(requiredFMCServerTokenInterceptor).addPathPatterns("/fmc/server/**");
registry.addInterceptor(requiredTokenInterceptor).addPathPatterns("/**");
registry.addInterceptor(enableSettingInterceptor).addPathPatterns("/**");
registry.addInterceptor(requestRateLimitInterceptor).addPathPatterns("/**");
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(requestSingleParamResolver);
}
/**
* 通信消息转换
*
* @param converters 转换器
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
GsonHttpMessageConverter converter = new GsonHttpMessageConverter() {
@Override
protected void writeInternal(@NotNull Object object, Type type, @NonNull Writer writer) {
// 忽略参数类型,因为接口返回对象会被全局返回处理器包装为 TimiResponse否则会序列化转型错误
getGson().toJson(object, writer);
}
};
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Attachment.class, gsonSerializerAdapter);
builder.registerTypeAdapter(UserView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(MirrorView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(UserProfileView.class, gsonSerializerAdapter);
builder.registerTypeAdapter(MinecraftPlayer.class, gsonSerializerAdapter);
converter.setGson(builder.create());
converters.add(converter);
}
}

View File

@ -0,0 +1,76 @@
package com.imyeyu.server.config.dbsource;
import com.zaxxer.hikari.HikariDataSource;
import com.imyeyu.utils.Time;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
/**
* ForeverMC 登录校验数据源
*
* @author 夜雨
* @since 2022-11-29 22:39
*/
@Configuration
@MapperScan(basePackages = "com.imyeyu.server.modules.forevermc.mapper", sqlSessionFactoryRef = "foreverMCSqlSessionFactory")
public class ForeverMCDBConfig {
public static final String ROLLBACKER = "foreverMCTransactionManager";
@Bean(name = "foreverMCDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.forevermc")
public DataSource getPrimaryDateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setAutoCommit(true);
dataSource.setMinimumIdle(10);
dataSource.setMaximumPoolSize(100);
dataSource.setConnectionTestQuery("SELECT 1");
dataSource.setMaxLifetime(Time.S * 180);
dataSource.setIdleTimeout(Time.S * 120);
dataSource.setLoginTimeout(5);
dataSource.setValidationTimeout(Time.S * 3);
dataSource.setConnectionTimeout(Time.S * 8);
dataSource.setLeakDetectionThreshold(Time.S * 180);
return dataSource;
}
@Bean(name = "foreverMCSqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("foreverMCDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true);
config.setMapUnderscoreToCamelCase(true);
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setTypeAliasesPackage("com.imyeyu.server.modules.forevermc.entity");
bean.setConfiguration(config);
return bean.getObject();
}
@Bean("foreverMCSqlSessionTemplate")
@Primary
public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("foreverMCSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
@Bean(name = "foreverMCTransactionManager")
public PlatformTransactionManager txManager(@Qualifier("foreverMCDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

View File

@ -0,0 +1,104 @@
package com.imyeyu.server.config.dbsource;
import com.imyeyu.utils.Time;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.EnumTypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* Gitea 数据源
*
* @author 夜雨
* @since 2022-11-29 22:40
*/
@Configuration
@MapperScan(basePackages = {
"com.imyeyu.server.modules.gitea.mapper",
}, sqlSessionFactoryRef = "giteaSqlSessionFactory")
public class GiteaDBConfig {
public static final String ROLLBACKER = "giteaTransactionManager";
@Bean(name = "giteaDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.gitea")
public DataSource dateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setAutoCommit(true);
dataSource.setMinimumIdle(10);
dataSource.setMaximumPoolSize(100);
dataSource.setConnectionTestQuery("SELECT 1");
dataSource.setMaxLifetime(Time.S * 180);
dataSource.setIdleTimeout(Time.S * 120);
dataSource.setLoginTimeout(5);
dataSource.setValidationTimeout(Time.S * 3);
dataSource.setConnectionTimeout(Time.S * 8);
dataSource.setLeakDetectionThreshold(Time.S * 180);
return dataSource;
}
@Bean(name = "giteaSqlSessionFactory")
@Primary
public SqlSessionFactory sessionFactory(@Qualifier("giteaDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true);
config.setMapUnderscoreToCamelCase(true);
config.setDefaultEnumTypeHandler(EnumTypeHandler.class);
List<Resource> resources = new ArrayList<>();
{
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:mapper/gitea/**/*.xml");
for (int i = 0; i < mapperLocations.size(); i++) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i))));
}
}
String[] typeAliases = {
"com.imyeyu.server.modules.gitea.entity",
};
String[] typeHandlers = {
"com.imyeyu.server.handler"
};
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setConfiguration(config);
bean.setMapperLocations(resources.toArray(new Resource[0]));
bean.setTypeAliasesPackage(String.join(",", typeAliases));
bean.setTypeHandlersPackage(String.join(",", typeHandlers));
return bean.getObject();
}
@Bean("giteaSqlSessionTemplate")
@Primary
public SqlSessionTemplate sessionTemplate(@Qualifier("giteaSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
@Bean(name = "giteaTransactionManager")
public PlatformTransactionManager txManager(@Qualifier("giteaDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

View File

@ -0,0 +1,122 @@
package com.imyeyu.server.config.dbsource;
import com.zaxxer.hikari.HikariDataSource;
import com.imyeyu.utils.Time;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.EnumTypeHandler;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
/**
* TimiServer 数据源
*
* @author 夜雨
* @since 2022-11-29 22:40
*/
@Configuration
@MapperScan(basePackages = {
"com.imyeyu.server.modules.git.mapper",
"com.imyeyu.server.modules.bill.mapper",
"com.imyeyu.server.modules.blog.mapper",
"com.imyeyu.server.modules.lyric.mapper",
"com.imyeyu.server.modules.mirror.mapper",
"com.imyeyu.server.modules.system.mapper",
"com.imyeyu.server.modules.common.mapper",
"com.imyeyu.server.modules.minecraft.mapper"
}, sqlSessionFactoryRef = "timiServerSqlSessionFactory")
public class TimiServerDBConfig {
public static final String ROLLBACKER = "timiServerTransactionManager";
@Bean(name = "timiServerDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.timiserver")
public DataSource getPrimaryDateSource() throws SQLException {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setAutoCommit(true);
dataSource.setMinimumIdle(10);
dataSource.setMaximumPoolSize(100);
dataSource.setConnectionTestQuery("SELECT 1");
dataSource.setMaxLifetime(Time.S * 180);
dataSource.setIdleTimeout(Time.S * 120);
dataSource.setLoginTimeout(5);
dataSource.setValidationTimeout(Time.S * 3);
dataSource.setConnectionTimeout(Time.S * 8);
dataSource.setLeakDetectionThreshold(Time.S * 180);
return dataSource;
}
@Bean(name = "timiServerSqlSessionFactory")
@Primary
public SqlSessionFactory primarySqlSessionFactory(@Qualifier("timiServerDataSource") DataSource datasource) throws Exception {
org.apache.ibatis.session.Configuration config = new org.apache.ibatis.session.Configuration();
config.setUseGeneratedKeys(true);
config.setMapUnderscoreToCamelCase(true);
config.setDefaultEnumTypeHandler(EnumTypeHandler.class);
List<Resource> resources = new ArrayList<>();
{
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List<String> mapperLocations = new ArrayList<>();
mapperLocations.add("classpath:mapper/git/**/*.xml");
mapperLocations.add("classpath:mapper/blog/**/*.xml");
mapperLocations.add("classpath:mapper/common/**/*.xml");
mapperLocations.add("classpath:mapper/system/**/*.xml");
mapperLocations.add("classpath:mapper/minecraft/**/*.xml");
for (int i = 0; i < mapperLocations.size(); i++) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i))));
}
}
String[] typeAliases = {
"com.imyeyu.server.modules.git.entity",
"com.imyeyu.server.modules.bill.entity",
"com.imyeyu.server.modules.blog.entity",
"com.imyeyu.server.modules.lyric.entity",
"com.imyeyu.server.modules.mirror.entity",
"com.imyeyu.server.modules.system.entity",
"com.imyeyu.server.modules.common.entity",
"com.imyeyu.server.modules.minecraft.entity"
};
String[] typeHandlers = {
"com.imyeyu.server.handler"
};
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(datasource);
bean.setConfiguration(config);
bean.setMapperLocations(resources.toArray(new Resource[0]));
bean.setTypeAliasesPackage(String.join(",", typeAliases));
bean.setTypeHandlersPackage(String.join(",", typeHandlers));
return bean.getObject();
}
@Bean("timiServerSqlSessionTemplate")
@Primary
public SqlSessionTemplate primarySqlSessionTemplate(@Qualifier("timiServerSqlSessionFactory") SqlSessionFactory sessionfactory) {
return new SqlSessionTemplate(sessionfactory);
}
@Bean(name = "timiServerTransactionManager")
public PlatformTransactionManager txManager(@Qualifier("timiServerDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}

View File

@ -0,0 +1,57 @@
package com.imyeyu.server.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, String.valueOf(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

@ -0,0 +1,49 @@
package com.imyeyu.server.modules.bill.controller;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.server.modules.bill.service.BillService;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.service.SettingService;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 收支帐单接口
*
* @author 夜雨
* @since 2023-02-04 01:02
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/bill")
public class BillController {
private final BillService service;
private final SettingService settingService;
/**
* 创建收支帐单
*
* @param bill 账单
*/
@AOPLog
@RequestRateLimit
@PostMapping("/create")
public void createREBill(@Valid @RequestBody Bill bill) {
if (!settingService.getAsString(SettingKey.BILL_API_TOKEN).equals(TimiSpring.getToken())) {
throw new TimiException(TimiCode.REQUEST_BAD).msgKey("token.illegal");
}
service.create(bill);
}
}

View File

@ -0,0 +1,120 @@
package com.imyeyu.server.modules.bill.entity;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import com.imyeyu.spring.entity.Entity;
/**
* 收支账单
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Bill extends Entity {
/**
* 类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum Type {
/** 收入 */
REVENUE,
/** 支出 */
EXPENDITURE
}
/**
* 收入类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum RevenueType {
/** 工作 */
WORK,
/** 退款 */
REFUND,
/** 其他 */
OTHER
}
/**
* 支出类型
*
* @author 夜雨
* @since 2022-03-29 11:28
*/
@Getter
public enum ExpenditureType {
/** 饮食 */
FOOD,
/** 生活 */
LIFE,
/** 通信 */
COMMUNICATION,
/** 交通 */
TRAFFIC,
/** 娱乐 */
GAME,
/** 工作 */
WORK,
/** 服饰 */
CLOTHES,
/** 医疗 */
HEALTH,
/** 其他 */
OTHER
}
/** 收入类型 */
private RevenueType revenueType;
/** 支出类型 */
private ExpenditureType expenditureType;
/** 描述 */
@NotBlank(message = "bill.description.empty")
private String description;
/** 金额(未确保计算精度,放大了 100 倍) */
@NotNull(message = "bill.decimal.empty")
@DecimalMin(value = "0", message = "bill.decimal.limit")
private Long decimal;
/** 备注 */
private String remarks;
/** @return true 为收入账单 */
public boolean isRevenue() {
return revenueType != null;
}
/** @return true 为支出账单 */
public boolean isExpenditure() {
return expenditureType != null;
}
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.bill.mapper;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 收支帐单表
*
* @author 夜雨
* @since 2022-04-01 16:26
*/
public interface BillMapper extends BaseMapper<Bill, Long> {
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.bill.service;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.spring.service.CreatableService;
/**
* 收支帐单服务
*
* @author 夜雨
* @since 2022-04-01 16:24
*/
public interface BillService extends CreatableService<Bill> {
}

View File

@ -0,0 +1,27 @@
package com.imyeyu.server.modules.bill.service.implement;
import com.imyeyu.server.modules.bill.entity.Bill;
import com.imyeyu.server.modules.bill.mapper.BillMapper;
import com.imyeyu.server.modules.bill.service.BillService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 收支账单服务
*
* @author 夜雨
* @since 2022-04-01 16:25
*/
@Service
@RequiredArgsConstructor
public class BillServiceImplement extends AbstractEntityService<Bill, Long> implements BillService {
private final BillMapper mapper;
@Override
protected BaseMapper<Bill, Long> mapper() {
return mapper;
}
}

View File

@ -0,0 +1,93 @@
package com.imyeyu.server.modules.blog.controller;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.blog.service.ArticleService;
import com.imyeyu.server.modules.blog.vo.article.ArticleView;
import com.imyeyu.server.modules.blog.vo.article.KeywordPage;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 文章接口
*
* @author 夜雨
* @since 2021-02-17 17:47
*/
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/article")
public class ArticleController {
private final ArticleService service;
/**
* 查看
*
* @param id 文章 ID
* @return 文章
*/
@AOPLog
@RequestRateLimit
@RequestMapping("/{id}")
public ArticleView view(@Min(1) @NotNull @PathVariable Long id) {
return service.view(id);
}
/**
* 喜欢文章
*
* @param id 文章 ID
* @return 最新喜欢数量
*/
@AOPLog
@RequestRateLimit
@RequestMapping("/like/{id}")
public int like(@Min(1) @NotNull @PathVariable Long id) {
return service.like(id);
}
/**
* 主列表
*
* @param page 页面参数
* @return 文章列表
*/
@RequestRateLimit
@RequestMapping("/list")
public PageResult<Article> list(@Valid @RequestBody Page page) {
return service.page(page);
}
/**
* 根据关键字获取列表
*
* @param page 关键字页面参数
* @return 文章列表
*/
@RequestRateLimit
@RequestMapping("/list/search")
public PageResult<Article> listByKeyword(@Valid @RequestBody KeywordPage page) {
return service.pageByKeyword(page);
}
/** @return 每周访问排位 */
@RequestMapping("/list/ranking")
public List<ArticleRanking> ranking() {
return service.listRanking();
}
}

View File

@ -0,0 +1,32 @@
package com.imyeyu.server.modules.blog.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.server.modules.blog.entity.Friend;
import com.imyeyu.server.modules.blog.service.FriendService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 主控
*
* @author 夜雨
* @since 2023-02-04 10:28
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/")
public class BlogController {
private final FriendService friendService;
/** @return 所有友链列表 */
@GetMapping("/friend")
public List<Friend> friend() {
return friendService.listAll();
}
}

View File

@ -0,0 +1,95 @@
package com.imyeyu.server.modules.blog.entity;
import com.google.gson.JsonElement;
import com.imyeyu.server.modules.common.bean.CommentSupport;
import com.imyeyu.spring.entity.Destroyable;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 文章
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Article extends Entity implements CommentSupport, Destroyable {
/**
* 文章渲染类型,对应前端模板
*
* @author 夜雨
* @since 2021-07-04 09:23
*/
public enum Type {
/** 关于 */
ABOUT,
/** 公版 */
PUBLIC,
/** 音乐 */
MUSIC,
/** 软件 */
SOFTWARE
}
/** 标题 */
protected String title;
/** 类型 */
protected Type type;
/** 分类 ID */
protected long classId;
/** 摘要 */
protected String digest;
/** 数据 */
protected String data;
/** 扩展数据 */
protected JsonElement extendData;
/** 阅读数量 */
protected int reads;
/** 喜欢数量 */
protected int likes;
/** 是否显示评论 */
protected boolean showComment;
/** true 为可评论 */
protected boolean canComment;
/** true 为可排位 */
protected boolean canRanking;
/** @return true 为可评论 */
@Override
public boolean canComment() {
return canComment;
}
/** @return true 为不可评论 */
@Override
public boolean canNotComment() {
return !canComment;
}
/** @return true 为可排位 */
public boolean canRanking() {
return canRanking;
}
/** @return true 为不可排位 */
public boolean canNotRanking() {
return !canRanking;
}
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.modules.blog.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 访问排行(每周)
* 只记录访问次数、标题和最近访问,具体文章由 Redis key 记录
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ArticleRanking extends Entity {
private String title;
private Article.Type type;
private int count = 1;
private Long recentAt; // 最近访问
public ArticleRanking(Long id, String title, Article.Type type) {
setId(id);
this.title = title;
this.type = type;
}
/** 访问计数 + 1 */
public void read() {
count++;
}
}

View File

@ -0,0 +1,36 @@
package com.imyeyu.server.modules.blog.entity;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 评论回复提醒队列,和 CommentReplyRecord 不一样,本队列在推送消息后就删除,而后者会持久保存
*
* <p>基本逻辑:
* <pre>
* 触发:用户回复一条评论
* 条件:被回复者是注册用户 && 不是回复自己 && 邮箱已验证 && 接收回复提醒邮件
* 事件:添加本对象到队列列表,等待邮件推送服务调度,邮件推送服务
* </pre>
* 会针对用户收集本队列消息组合成邮件再一并推送
*
* @author 夜雨
* @since 2021-08-25 00:00
*/
@Data
@NoArgsConstructor
public class CommentRemindQueue {
@Id
@AutoUUID
private String UUID;
private Long userId;
private Long replyId;
private CommentReplyView reply;
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.server.modules.blog.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.imyeyu.spring.entity.Entity;
/**
* 夜雨 创建于 2021-07-15 15:59
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Friend extends Entity {
private String icon;
private String name;
private String link;
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.server.modules.blog.mapper;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 文章
*
* @author 夜雨
* @since 2021-02-23 21:34
*/
public interface ArticleMapper extends BaseMapper<Article, Long> {
long countByKeyword(String keyword);
List<Article> selectByKeyword(String keyword, Long offset, int limit);
@Select("UPDATE `article` SET `likes` = `likes` + 1 WHERE `id` = #{articleId}")
void like(Long articleId);
@Select("UPDATE `article` SET `reads` = `reads` + 1 WHERE `id` = #{articleId}")
void read(Long articleId);
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.server.modules.blog.mapper;
import com.imyeyu.server.modules.blog.entity.Friend;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 友链
*
* @author 夜雨
* @since 2021-07-15 16:11
*/
public interface FriendMapper extends BaseMapper<Friend, Long> {
@Select("SELECT * FROM friend WHERE 1 = 1" + NOT_DELETE)
List<Friend> listAll();
}

View File

@ -0,0 +1,48 @@
package com.imyeyu.server.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.blog.vo.article.ArticleView;
import com.imyeyu.server.modules.blog.vo.article.KeywordPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.PageableService;
import java.util.List;
/**
* 文章服务
*
* @author 夜雨
* @since 2021-02-23 21:33
*/
public interface ArticleService extends GettableService<Article, Long>, PageableService<Article> {
/**
* 获取文章,此方法触发阅读计数,包括触发每周热门排行统计,同一 IP 3 小时内访问多次的文章只计一次
*
* @param id 文章 ID
* @throws TimiException 服务异常
*/
ArticleView view(long id);
PageResult<Article> pageByKeyword(KeywordPage page);
/**
* 获取每周阅读排行
*
* @return 热门文章列表
* @throws TimiException 服务异常
*/
List<ArticleRanking> listRanking();
/**
* 喜欢文章
*
* @param articleId 文章 ID
* @return 最新喜欢数量
* @throws TimiException 服务异常
*/
int like(Long articleId);
}

View File

@ -0,0 +1,41 @@
package com.imyeyu.server.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.spring.service.CreatableService;
import java.util.List;
/**
* 评论回复队列服务
*
* @author 夜雨
* @since 2021-08-25 00:11
*/
public interface CommentRemindQueueService extends CreatableService<CommentRemindQueue> {
/**
* 根据用户 ID 获取
*
* @param userId 用户 ID
* @return 回复提醒列表
* @throws TimiException 服务异常
*/
List<CommentRemindQueue> listByUserId(Long userId);
/**
* 根据用户 ID 移出队列
*
* @param uid 用户 ID
* @throws TimiException 服务异常
*/
void destroyByUserId(Long uid);
/**
* 根据回复 ID 移出队列
*
* @param rid 回复 ID
* @throws TimiException 服务异常
*/
void destroyByReplyId(Long rid);
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.blog.service;
import com.imyeyu.server.modules.blog.entity.Friend;
import java.util.List;
/**
* 友链服务
*
* @author 夜雨
* @since 2021-07-15 16:04
*/
public interface FriendService {
List<Friend> listAll();
}

View File

@ -0,0 +1,113 @@
package com.imyeyu.server.modules.blog.service.implement;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.config.dbsource.TimiServerDBConfig;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.blog.entity.ArticleRanking;
import com.imyeyu.server.modules.blog.mapper.ArticleMapper;
import com.imyeyu.server.modules.blog.service.ArticleService;
import com.imyeyu.server.modules.blog.vo.article.ArticleView;
import com.imyeyu.server.modules.blog.vo.article.KeywordPage;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.entity.Comment;
import com.imyeyu.server.modules.common.entity.Tag;
import com.imyeyu.server.modules.common.mapper.CommentMapper;
import com.imyeyu.server.modules.common.service.AttachmentService;
import com.imyeyu.server.modules.common.service.TagService;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Comparator;
import java.util.List;
/**
* 文章服务实现
*
* @author 夜雨
* @since 2021-02-17 17:48
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleServiceImplement extends AbstractEntityService<Article, Long> implements ArticleService {
private final TagService tagService;
private final AttachmentService attachmentService;
private final ArticleMapper mapper;
private final CommentMapper commentMapper;
private final Redis<String, Long> redisArticleRead;
private final Redis<Long, ArticleRanking> redisArticleRanking;
@Override
protected BaseMapper<Article, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public ArticleView view(long id) {
String ip = TimiSpring.getRequestIP();
Article article = get(id);
TimiException.required(article, "article.not_found");
// 计数
if (!redisArticleRead.contains(ip, article.getId())) {
// 3 小时内访问记录
redisArticleRead.add(ip, article.getId());
redisArticleRead.setExpire(ip, Time.H * 3);
mapper.read(article.getId());
// 每周访问计数
if (article.canRanking()) {
ArticleRanking ranking = redisArticleRanking.get(article.getId());
if (ranking == null) {
ranking = new ArticleRanking(article.getId(), article.getTitle(), article.getType());
ranking.setRecentAt(Time.now());
redisArticleRanking.set(article.getId(), ranking, Time.D * 7);
} else {
ranking.read();
ranking.setRecentAt(Time.now());
redisArticleRanking.setAndKeepTTL(article.getId(), ranking);
}
}
}
ArticleView view = new ArticleView();
BeanUtils.copyProperties(article, view);
view.setComments(commentMapper.countAll(Comment.BizType.ARTICLE, article.getId()));
view.setTagList(tagService.listByBizID(Tag.BizType.ARTICLE, String.valueOf(article.getId())));
view.setAttachmentList(attachmentService.listByBizId(Attachment.BizType.ARTICLE, article.getId()));
return view;
}
@Override
public PageResult<Article> pageByKeyword(KeywordPage page) {
PageResult<Article> result = new PageResult<>();
result.setList(mapper.selectByKeyword(page.getKeyword(), page.getOffset(), page.getLimit()));
result.setTotal(mapper.countByKeyword(page.getKeyword()));
return result;
}
@Override
public List<ArticleRanking> listRanking() {
List<ArticleRanking> list = redisArticleRanking.values();
list.sort(Comparator.comparing(ArticleRanking::getCount).reversed());
return list.subList(0, Math.min(10, list.size()));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public int like(Long articleId) {
mapper.like(articleId);
return get(articleId).getLikes();
}
}

View File

@ -0,0 +1,48 @@
package com.imyeyu.server.modules.blog.service.implement;
import com.imyeyu.server.config.dbsource.TimiServerDBConfig;
import com.imyeyu.server.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.server.modules.blog.service.CommentRemindQueueService;
import com.imyeyu.server.modules.common.mapper.CommentRemindQueueMapper;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
/**
* 评论回复队列服务实现
*
* @author 夜雨
* @since 2021-08-25 00:11
*/
@Service
@RequiredArgsConstructor
public class CommentRemindQueueServiceImplement extends AbstractEntityService<CommentRemindQueue, String> implements CommentRemindQueueService {
private final CommentRemindQueueMapper mapper;
@Override
protected BaseMapper<CommentRemindQueue, String> mapper() {
return mapper;
}
@Override
public List<CommentRemindQueue> listByUserId(Long userId) {
return mapper.listByUserId(userId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroyByUserId(Long userId) {
mapper.destroyByUserId(userId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroyByReplyId(Long replyId) {
mapper.destroyByReplyId(replyId);
}
}

View File

@ -0,0 +1,27 @@
package com.imyeyu.server.modules.blog.service.implement;
import lombok.RequiredArgsConstructor;
import com.imyeyu.server.modules.blog.entity.Friend;
import com.imyeyu.server.modules.blog.mapper.FriendMapper;
import com.imyeyu.server.modules.blog.service.FriendService;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 友链服务实现
*
* @author 夜雨
* @since 2021-07-15 16:05
*/
@Service
@RequiredArgsConstructor
public class FriendServiceImplement implements FriendService {
private final FriendMapper mapper;
@Override
public List<Friend> listAll() {
return mapper.listAll();
}
}

View File

@ -0,0 +1,148 @@
package com.imyeyu.server.modules.blog.util;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.config.RedisConfig;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.User;
import com.imyeyu.server.modules.common.service.SettingService;
import com.imyeyu.server.modules.common.service.UserService;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.spring.util.RedisSerializers;
import com.imyeyu.utils.Time;
import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.Cookie;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* Redis 令牌缓存
*
* <p>一级缓存 Session二级缓存 Redis有效期为 {@link SettingKey#TTL_USER_TOKEN} 天,每次触发
* 二级缓存获取时会刷新这个时间,即指定天数内不再访问则视为登出
*
* @author 夜雨
* @since 2023-07-17 16:58
*/
@Component
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class UserToken {
private final RedisConfig redisConfig;
private final UserService userService;
private final SettingService settingService;
private Redis<String, Long> redis;
@PostConstruct
private void postConstruct() {
redis = redisConfig.getRedis(redisConfig.getDatabase().getUserToken(), RedisSerializers.STRING, RedisSerializers.LONG);
}
public Long set(String token, Long userId) {
long ttl = Time.D * settingService.getAsInt(SettingKey.TTL_USER_TOKEN);
// 会话
TimiSpring.setSessionAttr(token, userId);
// 跨站 Cookie
Cookie cookie = Objects.requireNonNullElse(TimiSpring.getCookie("Token"), new Cookie("Token", token));
cookie.setDomain(settingService.getAsString(SettingKey.DOMAIN_ROOT));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge((int) (ttl / 1000));
TimiSpring.addCookie(cookie);
// Redis
redis.set(token, userId, ttl);
return Time.now() + ttl;
}
public Long getExpireAt(String token) {
return Time.now() + redis.getExpire(token);
}
/**
* 获取令牌是否有效
*
* @param token 令牌
* @return true 为有效
*/
public boolean isValid(String token) {
return getUserId(token) != null;
}
/**
* 获取令牌是否无效({@link #isValid(String)} 取反)
*
* @param token 令牌
* @return true 为无效
*/
public boolean isInvalid(String token) {
return !isValid(token);
}
/**
* 获取用户 ID
* <p>一级缓存 Session二级缓存 Redis每次触发二级缓存获取时会刷新这个时间
* <p>Session 存键为 token值为 UserIdRedis 也相同
*
* @param token 令牌
* @return 用户 ID
* @throws TimiException 无效 token
*/
public @Nullable Long getRequiredUserId(String token) throws TimiException {
return TimiException.required(getUserId(token), "invalid token");
}
/**
* 获取用户 ID
* <p>一级缓存 Session二级缓存 Redis每次触发二级缓存获取时会刷新这个时间
* <p>Session 存键为 token值为 UserIdRedis 也相同
*
* @param token 令牌
* @return 用户 IDtoken 无效时为 null
*/
public @Nullable Long getUserId(String token) {
if (TimiJava.isEmpty(token)) {
return null;
}
Long userId;
// Session
if (TimiSpring.getSessionAttr(token) instanceof Long sessionUserId) {
userId = sessionUserId;
} else {
// Redis
userId = redis.get(token);
}
if (TimiJava.isEmpty(userId)) {
return null;
}
// 刷新
set(token, userId);
return userId;
}
public @NotNull User getUser(String token) {
return userService.get(getRequiredUserId(token));
}
public void clear(String token) {
// 会话
TimiSpring.removeSessionAttr(token);
// 清除跨站 Cookie
Cookie cookie = new Cookie("Token", "DIED");
cookie.setDomain(settingService.getAsString(SettingKey.DOMAIN_ROOT));
cookie.setPath("/");
cookie.setSecure(true);
cookie.setMaxAge(0);
TimiSpring.addCookie(cookie);
// Redis
redis.destroy(token);
}
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.server.modules.blog.vo.article;
import com.imyeyu.server.modules.blog.entity.Article;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.entity.Tag;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-07 17:19
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ArticleView extends Article {
private long comments;
private List<Tag> tagList;
private List<Attachment> attachmentList;
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.blog.vo.article;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-07-14 17:52
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class ClassPage extends Page {
private long classId;
}

View File

@ -0,0 +1,18 @@
package com.imyeyu.server.modules.blog.vo.article;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-07-14 17:53
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class KeywordPage extends Page {
@NotBlank(message = "article.keyword.empty")
private String keyword;
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.blog.vo.article;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
* @since 2023-07-14 17:53
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class LabelPage extends Page {
private long labelId;
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.common.bean;
/**
* 支持评论的实体
*
* @author 夜雨
* @since 2023-10-10 11:39
*/
public interface CommentSupport {
/** @return true 为可评论 */
boolean canComment();
/** @return true 为不可评论 */
boolean canNotComment();
}

View File

@ -0,0 +1,30 @@
package com.imyeyu.server.modules.common.bean;
import lombok.Getter;
import lombok.Setter;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
/**
* 邮件服务异常
*
* @author 夜雨
* @since 2021-10-03 11:14
*/
public class EmailException extends TimiException {
/** 邮箱 */
@Setter
@Getter
private String email;
public EmailException(TimiCode code, String email) {
super(code);
this.email = email;
}
public EmailException(TimiCode code, String msg, String email) {
super(code, msg);
this.email = email;
}
}

View File

@ -0,0 +1,18 @@
package com.imyeyu.server.modules.common.bean;
/**
*
* @author 夜雨
* @since 2021-09-20 11:49
*/
public enum ImageType {
/** 双线性 */
AUTO,
/** 模糊 */
SMOOTH,
/** 像素 */
PIXELATED
}

View File

@ -0,0 +1,156 @@
package com.imyeyu.server.modules.common.bean;
/**
* 系统设置
*
* @author 夜雨
* @since 2021-07-20 21:46
*/
public enum SettingKey {
// ---------- 通用 ----------
RUN_ENV,
PUBLIC_RESOURCES,
/** 启用注册 */
ENABLE_REGISTER,
/** 启用登录 */
ENABLE_LOGIN,
/** 启用评论 */
ENABLE_COMMENT,
/** 启用测试 */
ENABLE_DEBUG,
/** 启用账号数据更新User 和 UserProfile */
ENABLE_USER_UPDATE,
/** 启用灰色滤镜 */
ENABLE_GRAY_FILTER,
// ---------- ICP 备案号 ----------
ICP_IMYEYU_COM,
// ---------- 域名 ----------
DOMAIN_ROOT,
DOMAIN_API,
DOMAIN_GIT,
DOMAIN_BLOG,
DOMAIN_SPACE,
DOMAIN_RESOURCE,
DOMAIN_DOWNLOAD,
DOMAIN_FOREVER_MC,
// ---------- ForeverMC ----------
/** 启用登录服务 */
FMC_PLAYER_LOGIN_ENABLE,
/** 最多绑定玩家数量 */
FMC_MAX_BIND,
/** 闪烁标语 */
FMC_SPLASHES,
/** 启动器背景 */
FMC_BG,
FMC_BGM,
FMC_BG_SWIPER,
/** JRE 列表 */
FMC_JRE,
/** 辅助登录模组 */
FMC_LOGIN_FABRIC,
/** 启用图片地图上传 */
FMC_ENABLE_IMAGE_MAP_UPLOAD,
/** 玩家登录令牌有效期(天) */
FMC_PLAYER_LOGIN_TOKEN_TTL,
/** 服务器与数据中心的通信令牌 */
FMC_SERVER_TOKEN,
// ---------- 生存时间 ----------
TTL_USER_TOKEN,
TTL_SETTING,
TTL_MULTILINGUAL,
// ---------- 多语言翻译 ----------
MULTILINGUAL_TRANSLATE_API,
MULTILINGUAL_TRANSLATE_APP_ID,
MULTILINGUAL_TRANSLATE_KEY,
// ---------- 账单 ----------
BILL_API_TOKEN,
// ---------- Git ----------
GIT_API,
// ---------- 远程音乐 ----------
MUSIC_MAX_FRAME_LENGTH,
MUSIC_PLAYER_PORT,
MUSIC_PLAYER_IP,
MUSIC_CONTROLLER_PORT,
MUSIC_CONTROLLER_IP,
MUSIC_CONTROLLER_URI,
// ---------- 系统 ----------
SYSTEM_FILE_BASE,
SYSTEM_FILE_TYPE,
SYSTEM_FILE_SYNC,
/** 文件过滤(通过密钥类型) */
SYSTEM_FILE_FILTER,
SYSTEM_STATUS_RATE,
SYSTEM_STATUS_LIMIT,
SYSTEM_STATUS_NETWORK_MAC,
SYSTEM_TERMINAL_TTL,
SYSTEM_TERMINAL_FILTERS,
/** 一般密钥 */
SYSTEM_API_KEY,
/** 超级密钥 */
SYSTEM_API_SUPER_KEY,
SYSTEM_REBOOT_COMMAND,
}

View File

@ -0,0 +1,85 @@
package com.imyeyu.server.modules.common.controller;
import com.imyeyu.server.annotation.CaptchaValid;
import com.imyeyu.server.annotation.EnableSetting;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.Comment;
import com.imyeyu.server.modules.common.entity.CommentReply;
import com.imyeyu.server.modules.common.service.CommentReplyService;
import com.imyeyu.server.modules.common.service.CommentService;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.server.modules.common.vo.comment.CommentView;
import com.imyeyu.server.modules.git.vo.issue.CommentPage;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 评论操作接口
* <p>*评论回复只依赖评论而不含业务关联,评论有关联业务,所以此接口是通用接口
*
* @author 夜雨
* @since 2021-02-23 21:36
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/comment")
public class CommentController {
private final CommentService service;
private final CommentReplyService replyService;
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@RequestRateLimit
@PostMapping("/create")
public void create(@Valid @RequestBody CaptchaData<Comment> captchaData) {
service.create(captchaData.getData());
}
@RequestRateLimit
@PostMapping("/list")
public PageResult<CommentView> list(@Valid @RequestBody CommentPage commentPage) {
return service.pageByBizId(commentPage);
}
/**
* 创建评论回复
*
* @param request 回复数据
*/
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT_REPLY)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@RequestRateLimit
@PostMapping("/reply/create")
public void createReply(@Valid @RequestBody CaptchaData<CommentReply> request) {
replyService.create(request.getData());
}
/**
* 获取回复列表
*
* @param page 页面参数
* @return 回复列表
*/
@RequestRateLimit
@RequestMapping("/reply/list")
public PageResult<CommentReplyView> pageCommentReplies(@Valid @RequestBody CommentReplyPage page) {
// 通用接口,只允许查询评论的回复
page.setBizType(CommentReplyPage.BizType.COMMENT);
return replyService.pageByBizType(page);
}
}

View File

@ -0,0 +1,375 @@
package com.imyeyu.server.modules.common.controller;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.network.Network;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.modules.common.bean.ImageType;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.entity.Setting;
import com.imyeyu.server.modules.common.entity.Task;
import com.imyeyu.server.modules.common.entity.Template;
import com.imyeyu.server.modules.common.entity.Version;
import com.imyeyu.server.modules.common.service.AttachmentService;
import com.imyeyu.server.modules.common.service.FeedbackService;
import com.imyeyu.server.modules.common.service.SettingService;
import com.imyeyu.server.modules.common.service.TaskService;
import com.imyeyu.server.modules.common.service.TemplateService;
import com.imyeyu.server.modules.common.service.VersionService;
import com.imyeyu.server.modules.common.vo.FeedbackRequest;
import com.imyeyu.server.modules.common.vo.attachment.AttachmentView;
import com.imyeyu.server.modules.system.util.ResourceHandler;
import com.imyeyu.server.util.CaptchaManager;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.CaptchaData;
import com.mongodb.client.gridfs.GridFSBucket;
import com.mongodb.client.gridfs.GridFSDownloadStream;
import com.mongodb.client.gridfs.model.GridFSFile;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.yaml.snakeyaml.Yaml;
import javax.imageio.ImageIO;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 系统接口
*
* @author 夜雨
* @since 2021-02-23 21:38
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/")
public class CommonController {
private final TaskService taskService;
private final VersionService versionService;
private final SettingService settingService;
private final FeedbackService feedbackService;
private final TemplateService templateService;
private final AttachmentService attachmentService;
private final Gson gson;
private final Yaml yaml;
private final GridFSBucket gridFSBucket;
private final CaptchaManager captchaManager;
private final ResourceHandler resourceHandler;
@AOPLog
@RequestMapping("")
public String root() {
return "IT WORKING! " + TimiSpring.getRequestIP();
}
/**
* 获取验证码
*
* @param width 宽度
* @param height 高度
* @param from 来源
* @param response 返回对象
*/
@IgnoreGlobalReturn
@GetMapping("/captcha")
public void captcha(int width, int height, CaptchaFrom from, HttpServletResponse response) {
// 返回图像流
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache"); // 禁止缓存
response.setDateHeader("Expires", -1);
response.setContentType("image/jpg");
try {
// 宽度
if (width < 64) {
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream());
return;
}
// 高度
if (height < 19) {
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream());
return;
}
// 来自
if (TimiJava.isEmpty(from)) {
ImageIO.write(captchaManager.error(TimiCode.ARG_MISS), "jpg", response.getOutputStream());
return;
}
// 输出图像流
ImageIO.write(captchaManager.generate(from, width, height), "jpg", response.getOutputStream());
} catch (Exception e) {
log.error("CommonController.getCaptcha", e);
try {
ImageIO.write(captchaManager.error(TimiCode.ERROR), "jpg", response.getOutputStream());
} catch (IOException subE) {
log.error("write error image fail", subE);
}
}
}
/**
* 获取软件最新版本状态
*
* @param name 软件名称
* @return 最新版本状态
* @deprecated 兼容旧程序
*/
@AOPLog
@GetMapping("/versions/{name}")
@Deprecated
public Version versions(@Valid @NotBlank @PathVariable("name") String name) {
return versionService.getByName(name);
}
/**
* 获取软件最新版本状态
*
* @param name 软件名称
* @return 最新版本状态
*/
@AOPLog
@GetMapping("/version/{name}")
public Version version(@Valid @NotBlank @PathVariable("name") String name) {
return versionService.getByName(name);
}
@AOPLog
@RequestRateLimit
@PostMapping("/feedback")
public void createFeedback(@Valid @NotNull @RequestBody CaptchaData<FeedbackRequest> request) {
captchaManager.test(request.getCaptcha(), request.getFrom());
feedbackService.create(request.getData());
}
/** @return 公开任务信息 */
@AOPLog
@RequestRateLimit
@GetMapping("/tasklist")
public List<Task> getTasks() {
return taskService.listAll4Public();
}
@RequestRateLimit
@GetMapping("/template")
public String viewTemplate(@RequestParam Template.BizType bizType, @RequestParam String bizCode) {
return templateService.get(bizType, bizCode).getData();
}
@RequestRateLimit
@GetMapping("/setting/{key}")
public String settingByKey(@PathVariable("key") String key, @RequestParam(value = "as", required = false) Setting.Type asType) {
Setting setting = settingService.getByKey(SettingKey.valueOf(key.toUpperCase()));
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
String result = setting.getValue();
if (asType == null) {
return result;
}
switch (asType) {
case JSON -> {
if (setting.getType() == Setting.Type.YAML) {
Map<String, Object> obj = yaml.load(setting.getValue());
result = gson.toJson(obj);
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = gson.fromJson(setting.getValue(), new TypeToken<Map<String, Object>>() {}.getType());
result = yaml.dump(obj);
}
}
}
return result;
}
@RequestRateLimit
@PostMapping("/setting/map")
public Map<SettingKey, String> mapSettingByKeys(@RequestBody Map<SettingKey, Map<String, Object>> settingMap) {
List<Setting> result = settingService.listByKeys(new ArrayList<>(settingMap.keySet()));
for (int i = 0; i < result.size(); i++) {
Setting setting = result.get(i);
if (setting.isPrivate()) {
throw new TimiException(TimiCode.PERMISSION_ERROR);
}
Map<String, Object> args = settingMap.get(setting.getKey());
if (args == null) {
continue;
}
if (args.containsKey("as")) {
switch (Ref.toType(Setting.Type.class, args.get("as").toString())) {
case JSON -> {
if (setting.getType() == Setting.Type.YAML) {
Map<String, Object> obj = new Yaml().load(setting.getValue());
setting.setValue(gson.toJson(obj));
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = gson.fromJson(setting.getValue(), new TypeToken<Map<String, Object>>() {}.getType());
setting.setValue(new Yaml().dump(obj));
}
}
}
}
}
return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue));
}
@RequestRateLimit
@GetMapping("/setting/flushCache")
public void settingFlushCache() {
settingService.flushCache();
}
@AOPLog
@RequestRateLimit
@GetMapping("/attachment/{mongoId}")
public AttachmentView getAttachment(@PathVariable String mongoId) {
return attachmentService.viewByMongoId(mongoId);
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping("/attachment/read/{mongoId}")
public void readAttachment(
@PathVariable String mongoId,
@RequestParam(name = "size", required = false) Integer size,
@RequestParam(name = "type", required = false) String type,
HttpServletRequest req,
HttpServletResponse resp
) {
try {
GridFSFile file = attachmentService.readByMongoId(mongoId);
if (file == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
return;
}
GridFSDownloadStream mimeReadStream = gridFSBucket.openDownloadStream(file.getObjectId());
String mimeType = new Tika().detect(mimeReadStream);
if (TimiJava.isNotEmpty(mimeType)) {
resp.setContentType(mimeType);
}
if (size != null) {
String fileType = switch (mimeType) {
case "image/png" -> "png";
case "image/jpeg" -> "jpg";
default -> throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO not support Re render mineType");
};
switch (mimeType) {
case "image/png", "image/jpeg" -> {
// 图片缩放
GridFSDownloadStream stream = gridFSBucket.openDownloadStream(file.getObjectId());
byte[] bytes = IO.toBytes(stream);
BufferedImage imgSrc = ImageIO.read(new ByteArrayInputStream(bytes));
double scale;
if (imgSrc.getHeight() < imgSrc.getWidth()) {
// 横向
scale = 1D * size / imgSrc.getWidth();
} else {
scale = 1D * size / imgSrc.getHeight();
}
int width = (int) (imgSrc.getWidth() * scale);
int height = (int) (imgSrc.getHeight() * scale);
BufferedImage imgResult = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = imgResult.createGraphics();
if (ImageType.PIXELATED == Ref.toType(ImageType.class, type)) {
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
} else {
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
g.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
g.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
}
g.drawImage(imgSrc, 0, 0, width, height, null);
ImageIO.write(imgResult, fileType, resp.getOutputStream());
}
default -> throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO not support Re render mineType");
}
} else {
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
GridFsResource gridFsResource = new GridFsResource(file, downloadStream);
req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.MONGO);
req.setAttribute(ResourceHandler.ATTR_VALUE, gridFsResource);
resourceHandler.handleRequest(req, resp);
}
} catch (Exception e) {
log.error("read attachment error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping("/attachment/download/{mongoId}")
public void downloadAttachment(@PathVariable String mongoId, HttpServletResponse resp) {
try {
Attachment attachment = attachmentService.getByMongoId(mongoId);
GridFSFile file = attachmentService.readByMongoId(mongoId);
if (file == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
return;
}
{
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
String mimeType = new Tika().detect(downloadStream);
if (TimiJava.isNotEmpty(mimeType)) {
resp.setContentType(mimeType);
}
}
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(file.getFilename()));
resp.setHeader("Content-Range", String.valueOf(attachment.getSize()));
resp.setHeader("Accept-Ranges", "bytes");
resp.setContentLengthLong(attachment.getSize());
IO.toOutputStream(downloadStream, resp.getOutputStream());
resp.flushBuffer();
} catch (Exception e) {
log.error("read attachment error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
resp.setCharacterEncoding(StandardCharsets.UTF_8.toString());
}
}
}

View File

@ -0,0 +1,97 @@
package com.imyeyu.server.modules.common.controller;
import com.imyeyu.server.modules.common.entity.Icon;
import com.imyeyu.server.modules.common.service.IconService;
import com.imyeyu.server.modules.common.vo.icon.AllResponse;
import com.imyeyu.server.modules.common.vo.icon.NamePage;
import com.imyeyu.server.modules.common.vo.icon.UnicodePage;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* timi-icon 前端接口
*
* @author 夜雨
* @since 2022-09-14 23:59
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/icon")
public class IconController {
private final IconService service;
/**
* 获取图标列表
*
* @param page 查询列表参数
* @return 图标列表
*/
@RequestRateLimit
@PostMapping("/list")
public PageResult<Icon> list(@RequestBody Page page) {
return service.page(page);
}
/**
* 获取所有图标,为了减小传输数据,此接口只返回 name 名称、Unicode 代码和 SVG 路径
*
* @param latest 请求缓存的最新数据时间
* @return 所有图标,如果请求缓存的最新时间等于数据库的最新数据时间,不返回任何数据
*/
@RequestRateLimit
@GetMapping("/list/all")
public AllResponse listAll(@Valid @RequestParam Long latest) {
AllResponse resp = service.listAll(latest);
List<Icon> icons = resp.getIcons();
for (int i = 0; i < icons.size(); i++) {
Icon icon = icons.get(i);
icon.setId(null);
icon.setCreatedAt(null);
icon.setUpdatedAt(null);
}
return resp;
}
/**
* 根据名称获取查询列表参数列表
*
* @param page 查询列表参数
* @return 图标列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list/name")
public PageResult<Icon> listByName(@Valid @RequestBody NamePage page) {
return service.pageByName(page);
}
/**
* 根据 Unicode 获取图标列表
*
* @param page 查询列表参数
* @return 图标列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list/unicode")
public PageResult<Icon> listByUnicode(@Valid @RequestBody UnicodePage page) {
return service.pageByUnicode(page);
}
}

View File

@ -0,0 +1,315 @@
package com.imyeyu.server.modules.common.controller;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.annotation.CaptchaValid;
import com.imyeyu.server.annotation.EnableSetting;
import com.imyeyu.server.bean.CaptchaFrom;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.CommentReply;
import com.imyeyu.server.modules.common.entity.UserConfig;
import com.imyeyu.server.modules.common.entity.UserPrivacy;
import com.imyeyu.server.modules.common.service.CommentReplyService;
import com.imyeyu.server.modules.common.service.CommentService;
import com.imyeyu.server.modules.common.service.UserConfigService;
import com.imyeyu.server.modules.common.service.UserPrivacyService;
import com.imyeyu.server.modules.common.service.UserProfileService;
import com.imyeyu.server.modules.common.service.UserService;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.server.modules.common.vo.comment.CommentView;
import com.imyeyu.server.modules.common.vo.comment.UserCommentPage;
import com.imyeyu.server.modules.common.vo.user.EmailVerifyCallbackRequest;
import com.imyeyu.server.modules.common.vo.user.LoginRequest;
import com.imyeyu.server.modules.common.vo.user.LoginResponse;
import com.imyeyu.server.modules.common.vo.user.RegisterRequest;
import com.imyeyu.server.modules.common.vo.user.UpdatePasswordByKeyRequest;
import com.imyeyu.server.modules.common.vo.user.UpdatePasswordRequest;
import com.imyeyu.server.modules.common.vo.user.UserRequest;
import com.imyeyu.server.modules.common.vo.user.UserView;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.utils.Time;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 用户接口
*
* @author 夜雨
* @since 2021-02-23 21:38
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/user")
public class UserController implements TimiJava {
private final UserService service;
private final CommentService commentService;
private final UserConfigService configService;
private final UserProfileService profileService;
private final UserPrivacyService privacyService;
private final CommentReplyService commentReplyService;
/**
* 注册。执行成功会自动登录
*
* @param request 注册请求
* @return 登录数据
*/
@AOPLog
@CaptchaValid(CaptchaFrom.REGISTER)
@EnableSetting(value = SettingKey.ENABLE_REGISTER, message = "user.register.off_service")
@RequestRateLimit(value = 1, lifeCycle = 60)
@PostMapping("/register")
public LoginResponse register(@Valid @RequestBody CaptchaData<RegisterRequest> request) {
return service.register(request.getData());
}
/**
* 登录
*
* @param request 登录请求
* @return 登录数据
*/
@AOPLog
@CaptchaValid(CaptchaFrom.LOGIN)
@EnableSetting(value = SettingKey.ENABLE_LOGIN, message = "user.login.off_service")
@RequestRateLimit
@PostMapping("/login")
public LoginResponse login(@Valid @RequestBody CaptchaData<LoginRequest> request) {
return service.login(request.getData());
}
/**
* 根据令牌登录,请求头携带 Token 参数,通常用于延续登录令牌
*
* @return 登录数据
*/
@AOPLog
@EnableSetting(value = SettingKey.ENABLE_LOGIN, message = "user.login.off_service")
@RequestRateLimit
@PostMapping("/login/token")
public LoginResponse login4Token() {
return service.login4Token();
}
/** 登出 */
@AOPLog
@RequestRateLimit
@PostMapping("/logout")
public void logout() {
service.logout();
}
/** 发送邮箱验证邮件 */
@AOPLog
@RequiredToken
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/email/verify")
public void sendEmailVerify() {
service.sendEmailVerify();
}
/**
* 邮箱验证邮件回调,验证请求的密钥来源于 {@link #sendEmailVerify()} 接口发送的邮件
*
* @param request 邮箱验证请求
*/
@AOPLog
@RequiredToken
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/email/verify/callback")
public void emailVerifyCallback(@Valid @RequestBody EmailVerifyCallbackRequest request) {
service.emailVerifyCallback(request.getKey());
}
/**
* 修改密码,需要已登录状态,使用旧密码修改
*
* @param request 修改密码请求
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/password/update")
public void updatePassword(@Valid @RequestBody UpdatePasswordRequest request) {
service.updatePassword(request.getOldValue(), request.getNewValue());
}
/**
* 发送用于重置密码的忘记密码邮件,入参数据可能是 UID、邮箱或用户名该数据目标用户的邮箱需要通过验证
*
* @param request 忘记密码邮件请求
*/
@AOPLog
@CaptchaValid(CaptchaFrom.RESET_PASSWORD)
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/password/forget")
public void sendPasswordForgetVerify(@Valid @RequestBody CaptchaData<String> request) {
service.sendPasswordForgetVerify(request.getData());
}
/**
* 修改密码,不需要登录状态,入参数据的密钥来源于 {@link #sendPasswordForgetVerify(CaptchaData)} 接口发送的邮件
*
* @param request 重置密码请求
*/
@AOPLog
@RequestRateLimit(value = 1, lifeCycle = 50)
@PostMapping("/password/reset")
public void resetPasswordByKey(@Valid @RequestBody UpdatePasswordByKeyRequest request) {
service.resetPasswordByKey(request.getKey(), request.getNewPassword());
}
/**
* 注销账号,此操作将会标记此用户的所有数据为删除状态
*
* @param password 密码
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/cancel")
public void cancel(@RequestSingleParam String password) {
service.cancel(password);
}
/**
* 获取用户资料
*
* @param userId 目标用户 ID
* @return 用户资料
*/
@AOPLog
@RequestRateLimit
@PostMapping("/view/{userId}")
public UserView view(@Min(1) @NotNull @PathVariable Long userId) throws Exception {
return service.view(userId).doFilter();
}
/**
* 更新用户数据
*
* @param data 用户数据(包括账号数据)
*/
@AOPLog
@RequiredToken
@EnableSetting(value = SettingKey.ENABLE_USER_UPDATE, message = "user.data.off_service")
@RequestRateLimit
@PostMapping("/profile/update")
public void updateProfile(@Valid UserRequest data) {
profileService.update(data);
}
/**
* 获取用户隐私控制
*
* @return 用户资料
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/privacy")
public UserPrivacy privacy() {
return privacyService.get(service.getLoginUser().getId());
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/privacy/update")
public void updatePrivacy(@Valid @RequestBody UserPrivacy privacy) {
privacyService.update(privacy);
}
/**
* 获取用户设置
*
* @return 用户设置
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/config")
public UserConfig config() {
return configService.get(service.getLoginUser().getId());
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/config/update")
public void updateConfig(@Valid @RequestBody UserConfig config) {
configService.update(config);
}
/**
* 获取用户评论
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/list")
public PageResult<CommentView> listComment(@Valid @RequestBody UserCommentPage page) {
page.setUserId(service.getLoginUser().getId());
return commentService.pageByUserId(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/delete")
public void deleteComment(@RequestSingleParam Long commentId) {
commentService.get(commentId);
commentService.delete(commentId);
}
/**
* 获取用户被回复的评论
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/list")
public PageResult<CommentReplyView> listCommentReply(@Valid @RequestBody CommentReplyPage page) {
page.setBizId(service.getLoginUser().getId());
return commentReplyService.pageByBizType(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/delete")
public void deleteCommentReply(@RequestSingleParam Long replyId) {
CommentReply reply = commentReplyService.get(replyId);
TimiException.requiredTrue(reply.getSenderId().equals(service.getLoginUser().getId()), "user.comment.reply.delete.not_owner");
commentReplyService.delete(replyId);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/ignore")
public void ignoreCommentReply(@RequestSingleParam Long replyId) {
CommentReply reply = commentReplyService.get(replyId);
TimiException.requiredTrue(reply.getReceiverId().equals(service.getLoginUser().getId()), "user.comment.reply.ignore.not_owner");
reply.setIgnoredAt(Time.now());
commentReplyService.update(reply);
}
}

View File

@ -0,0 +1,74 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.server.bean.MultilingualHandler;
import com.imyeyu.spring.entity.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
/**
* @author 夜雨
* @since 2023-08-15 10:17
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Attachment extends Entity implements MultilingualHandler {
/**
* 附件类型
*
* @author 夜雨
* @since 2023-08-21 16:32
*/
@Getter
@AllArgsConstructor
public enum BizType {
/** 用户 */
USER,
/** 文章 */
ARTICLE,
/** Git */
GIT,
/** 歌词 */
LYRIC,
/** ForeverMC */
FMC,
/** 镜像 */
MIRROR,
/** 系统 */
SYSTEM
}
private BizType bizType;
private Long bizId;
private String attachType;
private String mongoId;
@MultilingualField
private String title;
private String name;
private Long size;
public void setAttachTypeValue(Enum<?> attachType) {
this.attachType = attachType.toString();
}
public <T extends Enum<T>> T getAttachTypeValue(Class<T> attachTypeClass) {
return Ref.toType(attachTypeClass, attachType);
}
}

View File

@ -0,0 +1,68 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.spring.service.GettableService;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import com.imyeyu.server.modules.blog.service.implement.ArticleServiceImplement;
import com.imyeyu.server.modules.common.bean.CommentSupport;
import com.imyeyu.server.modules.git.service.implement.IssueServiceImplement;
import com.imyeyu.server.modules.git.service.implement.MergeServiceImplement;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.spring.service.BaseService;
/**
* 评论
*
* @author 夜雨
* @since 2021-02-25 14:46
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Comment extends Entity {
/**
* 关联业务类型
* <p>
* TODO 添加模块名称以便区别邮件通知推送来源,使用多语言键
*
* @author 夜雨
* @since 2023-08-06 23:42
*/
@Getter
@AllArgsConstructor
public enum BizType {
ARTICLE(ArticleServiceImplement.class),
GIT_ISSUE(IssueServiceImplement.class),
GIT_MERGE(MergeServiceImplement.class);
final Class<? extends GettableService<? extends CommentSupport, Long>> serviceClass;
}
/** 关联业务类型 */
private BizType bizType;
/** 关联业务 ID */
private Long bizId;
/** 发送用户 ID登录用户评论有值游客无 */
private Long userId;
/** 发送用户昵称,游客评论有值,登录用户无 */
private String nick;
/** 评论数据 */
@NotBlank
private String content;
/** 发送用户 IP */
private String ip;
}

View File

@ -0,0 +1,45 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import com.imyeyu.spring.entity.Entity;
/**
* 评论回复
*
* @author 夜雨
* @since 2021-03-01 17:11
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CommentReply extends Entity {
/** 所属评论 ID */
private Long commentId;
/** 被回复的回复,回复主评论时为 NULL */
private Long replyId;
/** 发送用户 ID登录用户回复有值游客无 */
private Long senderId;
/** 回复用户 ID系统用户回复有值游客无 */
private Long receiverId;
/** 发送用户昵称,游客回复有值,登录用户无 */
private String senderNick;
/** 回复用户昵称,游客回复有值,系统用户无 */
private String receiverNick;
/** 回复数据 */
private String content;
/** 发送用户 IP */
private String ip;
/** 被回复用户忽略该回复的时间 */
private Long ignoredAt;
}

View File

@ -0,0 +1,43 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import lombok.Data;
/**
* 邮件队列
*
* @author 夜雨
* @since 2021-08-24 14:59
*/
@Data
public class EmailQueue {
/**
* 业务类型
*
* @author 夜雨
* @since 2021-08-24 15:54
*/
public enum BizType {
/** 回复提醒 */
REPLY_REMINAD,
/** 邮箱验证 */
EMAIL_VERIFY,
/** 重置密码 */
RESET_PASSWORD
}
@Id
@AutoUUID
private String UUID;
private BizType bizType;
private Long bizId;
private Long sendAt;
}

View File

@ -0,0 +1,23 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* @author 夜雨
* @since 2021-08-24 18:00
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class EmailQueueLog extends Entity {
private String UUID;
private EmailQueue.BizType bizType;
private Long bizId;
private String sendTo;
private Long sendAt;
private Boolean isSent;
private String exceptionMsg;
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 反馈
*
* @author 夜雨
* @since 2021-11-16 22:22
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Feedback extends Entity {
private String from;
private String email;
private String data;
private String ip;
}

View File

@ -0,0 +1,25 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 字体图标
*
* @author 夜雨
* @since 2022-09-09 10:54
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Icon extends Entity {
/** 名称 */
private String name;
/** Unicode */
private String unicode;
/** SVG 路径 */
private String svg;
}

View File

@ -0,0 +1,67 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.server.TimiServerAPI;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.lang.reflect.Field;
/**
* @author 夜雨
* @since 2023-10-24 16:41
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Multilingual extends Entity {
protected String key;
protected String zhCN;
protected String zhTW;
protected String enUS;
protected String ruRU;
protected String koKR;
protected String jaJP;
protected String deDE;
/** @return 根据用户环境获取语言值 */
public String getValue() {
try {
Field field = Ref.getField(getClass(), TimiServerAPI.getUserLanguage().toString().replace("_", ""));
if (field == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language");
}
return Ref.getFieldValue(this, field, String.class);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 获取指定语言值
*
* @param language 指定语言
* @return 值
*/
public String getValue(com.imyeyu.java.bean.Language language) {
try {
Field field = Ref.getField(getClass(), language.toString().replace("_", ""));
if (field == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language");
}
return Ref.getFieldValue(this, field, String.class);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -0,0 +1,51 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Updatable;
import lombok.Data;
/**
* 系统配置
*
* @author 夜雨
* @since 2021-07-20 21:46
*/
@Data
public class Setting implements Creatable, Updatable {
/**
*
*
* @author 夜雨
* @since 2025-01-10 17:08
*/
public enum Type {
INTEGER,
STRING,
JSON,
YAML,
}
@Id
private SettingKey key;
private String value;
private Type type;
private boolean isPrivate;
private Long createdAt;
private Long updatedAt;
public boolean isPublic() {
return !isPrivate;
}
}

View File

@ -0,0 +1,39 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.server.bean.MultilingualHandler;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author 夜雨
* @since 2024-08-28 14:26
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Tag extends Entity implements MultilingualHandler {
/**
*
*
* @author 夜雨
* @since 2024-08-28 14:26
*/
public enum BizType {
ARTICLE,
MUSIC,
SERVER_FILE,
WALLPAPER
}
protected BizType bizType;
protected String bizID;
@MultilingualHandler.MultilingualField
protected String value;
}

View File

@ -0,0 +1,40 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import com.imyeyu.spring.entity.Entity;
import java.util.List;
/**
* 开发任务
*
* @author 夜雨
* @since 2022-02-26 11:12
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Task extends Entity {
/**
* 任务状态
*
* @author 夜雨
* @since 2022-02-26 11:53
*/
@Getter
public enum Status {
UPDATE, WITH, WAIT, KEEP, DIE;
final int sort = ordinal();
}
private String name;
private Status status;
private String digest;
// 关联数据
private List<TaskDetail> details;
}

View File

@ -0,0 +1,48 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import com.imyeyu.spring.entity.Entity;
/**
* 开发任务详细信息
*
* @author 夜雨
* @since 2022-02-27 17:58
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class TaskDetail extends Entity {
/**
* 类型
*
* @author 夜雨
* @since 2022-02-27 18:03
*/
@Getter
public enum Type {
BUG, FEATURE
}
/**
* 状态
*
* @author 夜雨
* @since 2022-02-27 18:06
*/
@Getter
public enum Status {
UPDATE, WAIT, FINISH, CLOSE
}
private Long taskId;
private Type type;
private Status status;
private String digest;
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* @author 夜雨
* @since 2023-09-21 00:53
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Template extends Entity {
/**
*
*
* @author 夜雨
* @since 2023-09-22 16:38
*/
public enum BizType {
GIT,
FOREVER_MC
}
private BizType bizType;
private String bizCode;
private String data;
}

View File

@ -0,0 +1,68 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.utils.Time;
/**
* 用户
*
* @author 夜雨
* @since 2021-03-01 17:11
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class User extends Entity {
/**
*
*
* @author 夜雨
* @since 2024-02-21 14:48
*/
public enum AttachType {
AVATAR,
WRAPPER,
LICENSE,
DEFAULT_AVATAR,
DEFAULT_WRAPPER
}
/** 用户名 */
protected String name;
/** 密码 */
protected String password;
/** 邮箱 */
protected String email;
/** 邮箱验证时间 */
protected Long emailVerifyAt;
/** 解除禁言时间 */
protected Long unmuteAt;
/** 解除封禁时间 */
protected Long unbanAt;
/** @return true 为禁言中 */
public boolean isMuting() {
return unmuteAt != null && Time.now() < unmuteAt;
}
/** @return true 为封禁中 */
public boolean isBanning() {
return unbanAt != null && Time.now() < unbanAt;
}
public boolean emailVerified() {
return emailVerifyAt != null;
}
}

View File

@ -0,0 +1,33 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Updatable;
import jakarta.validation.constraints.Min;
import lombok.Data;
/**
* 用户设置
*
* @author 夜雨
* @since 2021-08-12 15:06
*/
@Data
public class UserConfig implements Updatable {
@Min(1)
@Id
private Long userId;
private Boolean emailReplyRemind;
private Long updatedAt;
public UserConfig(Long uid) {
this.userId = uid;
emailReplyRemind = true;
}
public Boolean isEmailReplyRemind() {
return emailReplyRemind;
}
}

View File

@ -0,0 +1,51 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Updatable;
import jakarta.validation.constraints.Min;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.lang.reflect.Field;
import java.util.List;
/**
* 用户隐私控制
*
* @author 夜雨
* @since 2021-07-27 16:51
*/
@Data
@NoArgsConstructor
public class UserPrivacy implements Updatable {
@Min(1)
@Id
private Long userId;
private boolean email;
private boolean sex;
private boolean birthdate;
private boolean qq;
private boolean lastLoginAt;
private boolean createdAt;
private Long updatedAt;
public UserPrivacy(Long uid) {
this.userId = uid;
}
/** @return 过滤字段列表 */
public List<String> listFilterFields() {
return Ref.listFields(getClass()).stream().filter(f -> {
try {
f.setAccessible(true);
return boolean.class.isAssignableFrom(f.getType()) && !(boolean) f.get(this);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}).map(Field::getName).toList();
}
}

View File

@ -0,0 +1,66 @@
package com.imyeyu.server.modules.common.entity;
import com.imyeyu.server.modules.common.bean.ImageType;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Updatable;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 用户数据
*
* @author 夜雨
* @since 2021-05-29 15:58
*/
@Data
@NoArgsConstructor
public class UserProfile implements Updatable {
/** 用户 ID */
@Min(1)
@Id
protected Long userId;
/** 封面类型 */
protected ImageType wrapperType;
/** 头像类型 */
protected ImageType avatarType;
/** 经验值 */
protected Integer exp;
/** 性别 */
@Max(1)
@Min(0)
protected Byte sex;
/** 出生日期 */
@Min(0)
protected Long birthdate;
/** QQ */
@Pattern(regexp = "[1-9]\\d{4,14}")
protected String qq;
/** 说明 */
@Size(max = 240)
protected String description;
/** 最近登录 IP */
protected String lastLoginIP;
/** 最近登录时间 */
protected Long lastLoginAt;
/** 修改时间 */
protected Long updatedAt;
public UserProfile(Long userId) {
this.userId = userId;
}
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.server.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 版本管理
*
* @author 夜雨
* @since 2021-06-10 16:01
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Version extends Entity {
private String name;
private String version;
private String content;
private String url;
}

View File

@ -0,0 +1,31 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 夜雨
* @since 2023-08-15 10:22
*/
public interface AttachmentMapper extends BaseMapper<Attachment, Long> {
/** 有效条件,非删除和销毁 */
String VALID = NOT_DELETE + " AND destroy_at IS NULL";
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} " + VALID + LIMIT_1)
Attachment selectByBizId(Attachment.BizType bizType, long bizId);
@Select("SELECT * FROM attachment WHERE mongo_id = #{mongoId} " + VALID + LIMIT_1)
Attachment selectByMongoId(String mongoId);
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND attach_type = #{attachType} " + VALID + LIMIT_1)
Attachment selectByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType);
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND " + VALID + PAGE)
List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, long offset, int limit);
List<Attachment> listByAttachType(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
}

View File

@ -0,0 +1,40 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Comment;
import com.imyeyu.server.modules.common.vo.comment.CommentView;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.LinkedHashMap;
import java.util.List;
/**
* 评论
*
* @author 夜雨
* @since 2021-2-23 21:33
*/
public interface CommentMapper extends BaseMapper<Comment, Long> {
@Select("SELECT * FROM comment WHERE id = #{id}" + NOT_DELETE)
Comment select(Long id);
@Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE id = #{id}")
@Override
void delete(Long id);
@Select("SELECT COUNT(1) FROM comment WHERE biz_type = #{bizType} AND biz_id = #{bizId}" + NOT_DELETE)
long count(Comment.BizType bizType, Long bizId);
long countAll(Comment.BizType bizType, Long bizId);
List<CommentView> list(Comment.BizType bizType, Long bizId, Long offset, int limit, LinkedHashMap<String, OrderType> orderMap);
long countByUserId(Long userId);
List<CommentView> listByUserId(Long userId, Long offset, int limit, LinkedHashMap<String, OrderType> orderMap);
@Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE user_id = #{userId} ")
void deleteByUserId(Long userId);
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 评论回复提醒队列
*
* @author 夜雨
* @since 2021-08-25 00:15
*/
public interface CommentRemindQueueMapper extends BaseMapper<CommentRemindQueue, String> {
@Select("SELECT * FROM comment_remind_queue WHERE user_id = #{userId}")
List<CommentRemindQueue> listByUserId(Long userId);
@Delete("DELETE FROM comment_remind_queue WHERE user_id = #{userId}")
void destroyByUserId(Long userId);
@Delete("DELETE FROM comment_remind_queue WHERE reply_id = #{replyId}")
void destroyByReplyId(Long replyId);
}

View File

@ -0,0 +1,35 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.CommentReply;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* 评论回复
*
* @author 夜雨
* @since 2021-08-24 10:36
*/
public interface CommentReplyMapper extends BaseMapper<CommentReply, Long> {
@Select("SELECT * FROM comment_reply WHERE sender_id = #{senderId}" + NOT_DELETE)
List<CommentReply> listAllBySenderId(Long senderId);
@Select("SELECT COUNT(1) FROM comment_reply WHERE ${bizType.column} = #{bizId}" + NOT_DELETE)
long countByBizType(CommentReplyPage.BizType bizType, Long bizId);
@Select("SELECT * FROM comment_reply WHERE ${bizType.column} = #{bizId} AND ignored_at IS NULL" + NOT_DELETE + PAGE)
List<CommentReplyView> listByBizType(CommentReplyPage.BizType bizType, Long bizId, Long offset, int limit);
@Update("UPDATE comment_reply SET deleted_at = " + UNIX_TIME + " WHERE sender_id = #{userId} OR receiver_id = #{userId}")
void deleteByUserId(Long userId);
@Update("UPDATE comment_reply SET deleted_at = " + UNIX_TIME + " WHERE comment_id = #{commentId}")
void deleteByCommentId(Long commentId);
}

View File

@ -0,0 +1,11 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.EmailQueueLog;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* @author 夜雨
* @since 2023-08-10 10:38
*/
public interface EmailQueueLogMapper extends BaseMapper<EmailQueueLog, Long> {
}

View File

@ -0,0 +1,22 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.EmailQueue;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 邮件推送队列
*
* @author 夜雨
* @since 2021-08-24 16:22
*/
public interface EmailQueueMapper extends BaseMapper<EmailQueue, String> {
@Select("SELECT * FROM email_queue WHERE biz_type = #{bizType} AND biz_id = #{bizId}")
EmailQueue query(EmailQueue.BizType bizType, Long bizId);
@Select("SELECT * FROM email_queue")
List<EmailQueue> listAll();
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Feedback;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 反馈
*
* @author 夜雨
* @since 2021-11-16 22:28
*/
public interface FeedbackMapper extends BaseMapper<Feedback, Long> {
}

View File

@ -0,0 +1,43 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Icon;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 图标
*
* @author 夜雨
* @since 2022-09-15 00:02
*/
public interface IconMapper extends BaseMapper<Icon, Long> {
@Select("SELECT COUNT(1) FROM icon" + NOT_DELETE)
@Override
long count();
@Select("SELECT * FROM icon LIMIT #{offset}, #{limit}" + NOT_DELETE)
@Override
List<Icon> list(long offset, int limit);
@Select("SELECT * FROM icon WHERE 1 = 1" + NOT_DELETE)
List<Icon> listAll();
@Select("SELECT COUNT(1) FROM icon WHERE name LIKE CONCAT('%', #{name}, '%')" + NOT_DELETE)
long countByName(String name);
@Select("SELECT * FROM icon WHERE name LIKE CONCAT('%', #{name}, '%')" + PAGE)
List<Icon> listByName(String name, long offset, int limit);
long countByLabel(String lang, String label);
List<Icon> listByLabel(String lang, String label, long offset, int limit);
@Select("SELECT COUNT(1) FROM icon WHERE unicode = #{unicode}" + NOT_DELETE)
long countByUnicode(String unicode);
@Select("SELECT * FROM icon WHERE unicode = #{unicode}" + PAGE)
List<Icon> listByUnicode(String unicode, long offset, int limit);
}

View File

@ -0,0 +1,32 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Multilingual;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* @author 夜雨
* @since 2023-10-25 10:47
*/
public interface MultilingualMapper extends BaseMapper<Multilingual, Long> {
// 以下临时
@Select("SELECT * FROM multilingual WHERE key LIKE CONCAT('%', #{key}, '%')" + NOT_DELETE)
List<Multilingual> selectByKeyLike(String key);
List<Multilingual> selectByKeyList(List<String> keys);
// 以上临时
@Select("SELECT * FROM multilingual WHERE zh_cn = #{zhCN}" + NOT_DELETE + LIMIT_1)
Multilingual selectByZhCN(String zhCN);
@Select("SELECT * FROM multilingual WHERE en_US IS NULL OR en_US = ''" + NOT_DELETE)
List<Multilingual> selectByNotTranslate();
@Select("SELECT * FROM multilingual WHERE `key` = #{key}" + NOT_DELETE + LIMIT_1)
Multilingual selectByKey(String key);
}

View File

@ -0,0 +1,23 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.Setting;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 系统配置
*
* @author 夜雨
* @since 2021-07-20 22:26
*/
public interface SettingMapper extends BaseMapper<Setting, String> {
@Select("SELECT * FROM `setting` WHERE `key` = #{key}")
Setting selectByKey(SettingKey key);
@Select("SELECT * FROM `setting`")
List<Setting> listAll();
}

View File

@ -0,0 +1,11 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Tag;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* @author 夜雨
* @since 2025-05-30 22:48
*/
public interface TagMapper extends BaseMapper<Tag, Long> {
}

View File

@ -0,0 +1,16 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Task;
import java.util.List;
/**
* 任务服务
*
* @author 夜雨
* @since 2022-04-03 15:37
*/
public interface TaskMapper {
List<Task> listAll4Public();
}

View File

@ -0,0 +1,15 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Template;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* @author 夜雨
* @since 2023-09-22 16:41
*/
public interface TemplateMapper extends BaseMapper<Template, String> {
@Select("SELECT * FROM template WHERE biz_type = #{bizType} AND biz_code = #{bizCode}" + NOT_DELETE)
Template query(Template.BizType bizType, String bizCode);
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.UserConfig;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户设置
*
* @author 夜雨
* @since 2021-08-12 16:36
*/
public interface UserConfigMapper extends BaseMapper<UserConfig, Long> {
}

View File

@ -0,0 +1,20 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.User;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* 用户
*
* @author 夜雨
* @since 2021-02-23 21:33
*/
public interface UserMapper extends BaseMapper<User, Long> {
@Select("SELECT * FROM user WHERE BINARY name = #{name}" + NOT_DELETE + LIMIT_1)
User selectByName(String name);
@Select("SELECT * FROM user WHERE BINARY email = #{email} AND email_verify_at IS NOT NULL" + NOT_DELETE + LIMIT_1)
User selectByEmail(String email);
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.UserPrivacy;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户隐私控制
*
* @author 夜雨
* @since 2021-07-27 17:18
*/
public interface UserPrivacyMapper extends BaseMapper<UserPrivacy, Long> {
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.UserProfile;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户数据
*
* @author 夜雨
* @since 2021-07-27 17:04
*/
public interface UserProfileMapper extends BaseMapper<UserProfile, Long> {
}

View File

@ -0,0 +1,17 @@
package com.imyeyu.server.modules.common.mapper;
import com.imyeyu.server.modules.common.entity.Version;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* 版本管理
*
* @author 夜雨
* @since 2021-06-10 16:08
*/
public interface VersionMapper extends BaseMapper<Version, String> {
@Select("SELECT * FROM version WHERE name = #{name}" + NOT_DELETE)
Version queryByName(String name);
}

View File

@ -0,0 +1,56 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.common.entity.Attachment;
import com.imyeyu.server.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.server.modules.common.vo.attachment.AttachmentView;
import com.imyeyu.spring.service.DeletableService;
import com.imyeyu.spring.service.DestroyableService;
import com.imyeyu.spring.service.GettableService;
import com.mongodb.client.gridfs.model.GridFSFile;
import java.io.InputStream;
import java.util.List;
/**
* 附件服务
*
* <p>删除和销毁都为数据库软删除,但删除不删除 MongoDB 文件,而销毁则删除 MongoDB 文件
*
* @author 夜雨
* @since 2023-08-15 10:21
*/
public interface AttachmentService extends GettableService<Attachment, Long>, DeletableService<Long>, DestroyableService<Long> {
/**
*
*
* @param request
*/
void create(AttachmentRequest request);
Attachment getByBizId(Attachment.BizType bizType, long bizId);
Attachment getByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType);
Attachment getByMongoId(String mongoId);
AttachmentView viewByMongoId(String mongoId);
GridFSFile readByMongoId(String mongoId);
InputStream getInputStreamByMongoId(String mongoId);
byte[] readAllByMongoId(String mongoId);
/**
* 根据业务获取所有附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypes
* @return 所有附件
* @throws TimiException 服务异常
*/
List<Attachment> listByBizId(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
}

View File

@ -0,0 +1,21 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.server.modules.common.entity.CommentReply;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyPage;
import com.imyeyu.server.modules.common.vo.comment.CommentReplyView;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.DeletableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.UpdatableService;
/**
* 评论回复服务
*
* @author 夜雨
* @since 2021-08-24 10:33
*/
public interface CommentReplyService extends CreatableService<CommentReply>, GettableService<CommentReply, Long>, UpdatableService<CommentReply>, DeletableService<Long> {
PageResult<CommentReplyView> pageByBizType(CommentReplyPage page);
}

View File

@ -0,0 +1,31 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.common.entity.Comment;
import com.imyeyu.server.modules.common.vo.comment.CommentView;
import com.imyeyu.server.modules.common.vo.comment.UserCommentPage;
import com.imyeyu.server.modules.git.vo.issue.CommentPage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.DeletableService;
import com.imyeyu.spring.service.GettableService;
/**
* 评论服务
*
* @author 夜雨
* @since 2021-02-23 21:32
*/
public interface CommentService extends CreatableService<Comment>, GettableService<Comment, Long>, DeletableService<Long> {
PageResult<CommentView> pageByBizId(CommentPage page);
/**
* 获取用户评论页面
*
* @param userCommentPage 页面参数
* @return 页面列表
* @throws TimiException 服务异常
*/
PageResult<CommentView> pageByUserId(UserCommentPage userCommentPage);
}

View File

@ -0,0 +1,52 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.common.entity.EmailQueue;
import com.imyeyu.server.modules.common.entity.EmailQueueLog;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.DestroyableService;
import java.util.List;
/**
* 邮件推送队列服务
*
* @author 夜雨
* @since 2021-08-24 16:20
*/
public interface EmailQueueService extends CreatableService<EmailQueue>, DestroyableService<String> {
/**
* 根据推送类型和数据 ID 获取推送对象
*
* @param type 推送类型
* @param dataId 数据 ID
* @return 推送对象
* @throws TimiException 服务异常
*/
EmailQueue get(EmailQueue.BizType type, Long dataId);
/**
* 移出队列
*
* @param UUID UUID
* @throws TimiException 服务异常
*/
void destroy(String UUID);
/**
* 添加推送日志
*
* @param log 推送日志
* @throws TimiException 服务异常
*/
void addLog(EmailQueueLog log);
/**
* 遍历推送队列
*
* @return 推送队列
* @throws TimiException 服务异常
*/
List<EmailQueue> listAll();
}

View File

@ -0,0 +1,14 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.server.modules.common.vo.FeedbackRequest;
/**
* 反馈服务
*
* @author 夜雨
* @since 2021-11-16 22:27
*/
public interface FeedbackService {
void create(FeedbackRequest request);
}

View File

@ -0,0 +1,23 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.server.modules.common.entity.Icon;
import com.imyeyu.server.modules.common.vo.icon.AllResponse;
import com.imyeyu.server.modules.common.vo.icon.NamePage;
import com.imyeyu.server.modules.common.vo.icon.UnicodePage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.PageableService;
/**
* 图标服务
*
* @author 夜雨
* @since 2022-09-09 16:47
*/
public interface IconService extends PageableService<Icon> {
AllResponse listAll(Long latest);
PageResult<Icon> pageByName(NamePage page);
PageResult<Icon> pageByUnicode(UnicodePage page);
}

View File

@ -0,0 +1,24 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.java.bean.Language;
import com.imyeyu.server.modules.common.entity.Multilingual;
import com.imyeyu.spring.service.UpdatableService;
import java.util.List;
/**
* @author 夜雨
* @since 2023-10-25 10:47
*/
public interface MultilingualService extends UpdatableService<Multilingual> {
Long create(String key, String zhCN);
Long createIfNotExist(String key, String zhCN);
String get(Language language, Long id);
String getByKey(Language language, String key);
List<Multilingual> listByNotTranslate();
}

View File

@ -0,0 +1,78 @@
package com.imyeyu.server.modules.common.service;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.reflect.TypeToken;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.common.bean.SettingKey;
import com.imyeyu.server.modules.common.entity.Setting;
import com.imyeyu.spring.service.UpdatableService;
import java.util.Arrays;
import java.util.List;
/**
* 系统配置服务
*
* @author 夜雨
* @since 2021-07-20 22:06
*/
public interface SettingService extends UpdatableService<Setting> {
default List<Setting> listByKeys(SettingKey... keys) {
return listByKeys(Arrays.asList(keys));
}
List<Setting> listByKeys(List<SettingKey> keyList);
Setting getByKey(SettingKey key);
/**
* 获取指定类型配置值字符串
*
* @param key 键
* @return 配置值
*/
String getAsString(SettingKey key);
int getAsInt(SettingKey key);
long getAsLong(SettingKey key);
double getAsDouble(SettingKey key);
/**
* 获取为布尔值
*
* @param key 键
* @return 配置值
* @throws TimiException 服务异常
*/
boolean is(SettingKey key);
/**
* 获取为布尔值,并取反
*
* @param key 键
* @return 配置值
* @throws TimiException 服务异常
*/
boolean not(SettingKey key);
JsonElement getAsJsonElement(SettingKey key);
JsonObject getAsJsonObject(SettingKey key);
JsonArray getAsJsonArray(SettingKey key);
<T> T fromJson(SettingKey key, Class<T> clazz);
<T> T fromJson(SettingKey key, TypeToken<T> typeToken);
<T> T fromYaml(SettingKey key, Class<T> clazz);
List<Setting> listAll();
void flushCache();
}

View File

@ -0,0 +1,17 @@
package com.imyeyu.server.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.server.modules.common.entity.Tag;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.DeletableService;
import java.util.List;
/**
* @author 夜雨
* @since 2025-05-30 22:46
*/
public interface TagService extends CreatableService<Tag>, DeletableService<Long> {
List<Tag> listByBizID(Tag.BizType bizType, String bizID) throws TimiException;
}

Some files were not shown because too many files have changed in this diff Show More