refactor all

This commit is contained in:
Timi
2026-07-31 01:03:53 +08:00
parent 4e7f340cc6
commit e45809a0db
512 changed files with 14620 additions and 13519 deletions
@@ -1,93 +0,0 @@
package com.imyeyu.api.modules.blog.controller;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.blog.service.ArticleService;
import com.imyeyu.api.modules.blog.vo.article.ArticleView;
import com.imyeyu.api.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();
}
}
@@ -1,32 +0,0 @@
package com.imyeyu.api.modules.blog.controller;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.api.modules.blog.entity.Friend;
import com.imyeyu.api.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();
}
}
@@ -1,92 +0,0 @@
package com.imyeyu.api.modules.blog.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.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 {
/** 公版 */
PUBLIC,
/** 音乐 */
MUSIC,
/** 软件 */
SOFTWARE
}
/** 标题 */
protected String title;
/** 类型 */
protected Type type;
/** 摘要 */
protected String digest;
/** 数据 */
protected String data;
/** 扩展数据 */
protected JsonNode extendData;
/** 阅读数量 */
protected int reads;
/** 喜欢数量 */
protected int likes;
/** true 为显示评论 */
protected boolean showComment;
/** true 为可评论 */
protected boolean canComment;
/** true 为可排位 */
protected boolean canRanking;
/** true 为可通过列表查询 */
protected boolean canList;
/** @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;
}
}
@@ -1,35 +0,0 @@
package com.imyeyu.api.modules.blog.entity;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 访问排行(每周)
* 只记录访问次数、标题和最近访问,具体文章由 Redis key 记录
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@NoArgsConstructor
@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++;
}
}
@@ -1,36 +0,0 @@
package com.imyeyu.api.modules.blog.entity;
import com.imyeyu.api.modules.common.entity.CommentReply;
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 CommentReply reply;
}
@@ -1,21 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,33 +0,0 @@
package com.imyeyu.api.modules.blog.mapper;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.spring.bean.Page;
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> {
@Override
long countByPage(Page page);
@Override
List<Article> selectByPage(Page page);
long countByKeyword(String keyword);
List<Article> selectByKeyword(String keyword, Long offset, long 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);
}
@@ -1,20 +0,0 @@
package com.imyeyu.api.modules.blog.mapper;
import com.imyeyu.api.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();
}
@@ -1,48 +0,0 @@
package com.imyeyu.api.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.blog.vo.article.ArticleView;
import com.imyeyu.api.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);
}
@@ -1,41 +0,0 @@
package com.imyeyu.api.modules.blog.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.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);
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.modules.blog.service;
import com.imyeyu.api.modules.blog.entity.Friend;
import java.util.List;
/**
* 友链服务
*
* @author 夜雨
* @since 2021-07-15 16:04
*/
public interface FriendService {
List<Friend> listAll();
}
@@ -1,122 +0,0 @@
package com.imyeyu.api.modules.blog.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.blog.entity.ArticleRanking;
import com.imyeyu.api.modules.blog.mapper.ArticleMapper;
import com.imyeyu.api.modules.blog.service.ArticleService;
import com.imyeyu.api.modules.blog.vo.article.ArticleView;
import com.imyeyu.api.modules.blog.vo.article.KeywordPage;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.mapper.CommentMapper;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
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;
}
@Override
public PageResult<Article> page(Page page) {
PageResult<Article> result = new PageResult<>();
result.setList(mapper.selectByPage(page));
result.setTotal(mapper.countByPage(page));
return result;
}
@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();
}
}
@@ -1,52 +0,0 @@
package com.imyeyu.api.modules.blog.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.api.modules.blog.service.CommentRemindQueueService;
import com.imyeyu.api.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) {
CommentRemindQueue example = new CommentRemindQueue();
example.setUserId(userId);
mapper.deleteAllByExample(example);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroyByReplyId(Long replyId) {
CommentRemindQueue example = new CommentRemindQueue();
example.setReplyId(replyId);
mapper.destroyAllByExample(example);
}
}
@@ -1,27 +0,0 @@
package com.imyeyu.api.modules.blog.service.implement;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.modules.blog.entity.Friend;
import com.imyeyu.api.modules.blog.mapper.FriendMapper;
import com.imyeyu.api.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();
}
}
@@ -1,147 +0,0 @@
package com.imyeyu.api.modules.blog.util;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.config.RedisConfig;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.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);
}
}
@@ -1,24 +0,0 @@
package com.imyeyu.api.modules.blog.vo.article;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.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;
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,18 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.modules.common.bean;
/**
* 支持评论的实体
*
* @author 夜雨
* @since 2023-10-10 11:39
*/
public interface CommentSupport {
/** @return true 为可评论 */
boolean canComment();
/** @return true 为不可评论 */
boolean canNotComment();
}
@@ -1,191 +0,0 @@
package com.imyeyu.api.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,
TEMP_FILE_PATH,
// ---------- 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,
GIT_ABOUT_ARTICLE,
GIT_REPO_PATH,
// ---------- 远程音乐 ----------
MUSIC_MAX_FRAME_LENGTH,
MUSIC_PLAYER_PORT,
MUSIC_PLAYER_IP,
MUSIC_CONTROLLER_PORT,
MUSIC_CONTROLLER_IP,
MUSIC_CONTROLLER_URI,
// ---------- 日记 ----------
JOURNAL_KEY,
JOURNAL_APP_ID,
JOURNAL_APP_SECRET,
JOURNAL_MEMO,
JOURNAL_OPEN_ID_WHITE_LIST,
// ---------- 临时文件 ----------
/** 临时文件最小缓存时间 */
TEMP_FILE_TTL_MIN,
/** 临时文件最长缓存时间 */
TEMP_FILE_TTL_MAX,
/** 临时文件默认缓存时间 */
TEMP_FILE_TTL_DEFAULT,
/** 已过期的临时文件保留时间 */
TEMP_FILE_RESIDUE_TIME,
/** 每个 IP 限制有效临时文件容量 */
TEMP_FILE_LIMIT,
// ---------- 系统 ----------
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,
}
@@ -0,0 +1,50 @@
package com.imyeyu.api.modules.common.bean;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 设置模块
*
* @author 夜雨
* @since 2026-04-24 00:59
*/
@Getter
@AllArgsConstructor
public enum SettingModule {
COMMON(Setting.Module.Common.class),
DOMAIN(Setting.Module.Domain.class),
FOREVER_MC(Setting.Module.ForeverMC.class),
MULTILINGUAL(Setting.Module.Multilingual.class),
GITEA(Setting.Module.Gitea.class),
MUSIC(Setting.Module.Music.class),
JOURNAL(Setting.Module.Journal.class),
TEMP_FILE(Setting.Module.TempFile.class),
SYSTEM(Setting.Module.System.class),
SYSTEM_STATUS(Setting.Module.SystemStatus.class);
/** 模块枚举类 */
final Class<? extends Enum<?>> clazz;
public static <T extends Enum<?>> SettingModule fromClass(Class<T> clazz) {
for (SettingModule value : values()) {
if (value.clazz.equals(clazz)) {
return value;
}
}
throw new TimiException(TimiCode.ERROR, "not found System Module: %s".formatted(clazz.getName()));
}
}
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.common.bean.attachment;
import lombok.Data;
/**
* 附件访问密钥 VO
*
* @author 夜雨
* @since 2026-01-13 10:05
*/
@Data
public class AccessKey {
/** 访问密钥 */
private String value;
/** 附件 ID */
private String attachmentId;
/** true 为一次性访问 */
private boolean isDisposable;
/** 过期时间戳 */
private Long expiredAt;
/** 创建时间戳 */
private Long createdAt;
}
@@ -1,4 +1,4 @@
package com.imyeyu.api.modules.common.bean;
package com.imyeyu.api.modules.common.bean.attachment;
/**
* @author 夜雨
@@ -1,4 +1,4 @@
package com.imyeyu.api.modules.common.bean;
package com.imyeyu.api.modules.common.bean.attachment;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -33,10 +33,16 @@ public class Metadata {
@EqualsAndHashCode(callSuper = true)
public static class ThumbImage extends Image {
private long sourceId;
private String sourceId;
private String sourceMongoId;
private String sourceMimeType;
/** 请求宽度(缓存键之一,null 表示未指定) */
private Integer requestWidth;
/** 请求高度(缓存键之一,null 表示未指定) */
private Integer requestHeight;
}
}
@@ -0,0 +1,18 @@
package com.imyeyu.api.modules.common.bean.attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import java.util.List;
/**
*
*
* @author 夜雨
* @since 2026-05-13 17:27
*/
public interface SettingSupport {
String getId();
List<Setting> getSettingList();
}
@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.common.bean.attachment;
import lombok.Data;
/**
* 临时文件元数据
*
* @author 夜雨
* @since 2025-09-27 01:47
*/
@Data
public class TempFileMetaData {
/** 附件 ID */
private String id;
/** 文件名 */
private String name;
/** 原始文件名 */
private String originalName;
/** 储存 ID */
private String mongoId;
}
@@ -0,0 +1,281 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.TempFileService;
import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
import com.imyeyu.java.TimiJava;
import com.imyeyu.network.Network;
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.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.utils.Time;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
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.springframework.web.multipart.MultipartFile;
import java.io.UnsupportedEncodingException;
import java.util.List;
///
///
/// @author 夜雨
/// @since 2026-07-27 19:26
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/attach")
public class AttachmentController {
private final TempFileService tempFileService;
private final AttachmentService service;
/**
* 查询附件详情
*
* @param id 附件 ID
* @return 附件详情
*/
@RequestRateLimit
@PostMapping("/detail")
public Attachment detail(@RequestParam String id) {
return service.get(id);
}
/**
* 创建附件
*
* @param attachment 附件
* @return 创建结果
*/
@AOPLog
@RequestRateLimit
@PostMapping(value = "/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Attachment create(Attachment attachment) {
service.create(attachment);
return attachment;
}
/**
* 更新附件
*
* @param attachment 附件
*/
@AOPLog
@RequestRateLimit
@PostMapping(value = "/update", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void update(Attachment attachment) {
service.update(attachment);
}
/**
* 删除附件
*
* @param id 附件 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
/**
* 按业务查询单个附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @return 附件详情
*/
@RequestRateLimit
@GetMapping("/detail/byBiz")
public Attachment detailByBiz(@RequestParam Attachment.BizType bizType, @RequestParam String bizId) {
return service.getByBizId(bizType, bizId);
}
/**
* 按业务和附件类型查询单个附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachType 附件类型
* @return 附件详情
*/
@RequestRateLimit
@GetMapping("/detail/byAttachType")
public Attachment detailByAttachType(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam String attachType
) {
return service.getByAttachType(bizType, bizId, attachType);
}
/**
* 按业务查询附件列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypeList 附件类型列表
* @return 附件列表
*/
@RequestRateLimit
@GetMapping("/list/biz")
public List<Attachment> listByBiz(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam(required = false) List<String> attachTypeList
) {
List<String> types = TimiJava.defaultIfNull(attachTypeList, List.of());
return service.listByBizId(bizType, bizId, types.toArray(String[]::new));
}
/**
* 按业务统计附件数量
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypeList 附件类型列表
* @return 附件数量
*/
@RequestRateLimit
@GetMapping("/count/biz")
public long countByBiz(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam(required = false) List<String> attachTypeList
) {
List<String> types = TimiJava.defaultIfNull(attachTypeList, List.of());
return service.countByBizId(bizType, bizId, types.toArray(String[]::new));
}
/**
* 按业务分页查询附件
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param attachTypeList 附件类型列表
* @param page 分页参数
* @return 分页结果
*/
@RequestRateLimit
@PostMapping("/page/biz")
public PageResult<Attachment> pageByBiz(
@RequestParam Attachment.BizType bizType,
@RequestParam String bizId,
@RequestParam(required = false) List<String> attachTypeList,
@RequestBody Page<Attachment> page
) {
return service.pageByBizId(bizType, bizId, attachTypeList, page);
}
/**
* 按业务差分更新附件
*
* @param req 更新请求
*/
@AOPLog
@RequestRateLimit
@PostMapping("/update/biz")
public void updateByBiz(@RequestBody BizUpdateReq req) {
service.updateByBiz(req);
}
/**
* 按业务删除全部附件
*
* @param req 删除请求
*/
@AOPLog
@RequestRateLimit
@PostMapping("/delete/biz")
public void deleteByBiz(@RequestBody BizDeleteReq req) {
service.deleteByBiz(req);
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping({"/read/{id}", "/download/{id}"})
public void readById(@PathVariable String id, @RequestParam(required = false) Integer thumbWidth, @RequestParam(required = false) Integer thumbHeight) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
Attachment attach = service.get(id);
if (attach == null) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
responseAttachment(attach, thumbWidth, thumbHeight);
}
@AOPLog
@PostMapping("/temp/upload")
public List<TempFileResp> upload(@RequestParam("file") List<MultipartFile> files, @RequestParam(value = "ttl", required = false) String ttl) {
return tempFileService.create(files, Time.D);
}
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping({"/temp/read", "/temp/download"})
public void read(@RequestParam String id) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
Attachment attach = service.get(id);
if (TimiSpring.getRequest().getRequestURI().endsWith("/download")) {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
}
response(attach);
}
private void responseAttachment(Attachment attach, Integer thumbWidth, Integer thumbHeight) throws UnsupportedEncodingException {
HttpServletResponse resp = TimiSpring.getResponse();
if (attach != null && TimiSpring.getRequest().getRequestURI().endsWith("/download") && attach.getName() != null) {
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
}
responseThumb(attach, thumbWidth, thumbHeight);
}
protected void responseThumb(Attachment attachment, Integer thumbWidth, Integer thumbHeight) {
HttpServletResponse resp = TimiSpring.getResponse();
if (TimiJava.isEmpty(attachment)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (thumbWidth == null && thumbHeight == null) {
// 原图
response(attachment);
return;
}
// 缩略图
Attachment thumb = service.fetchThumb(attachment, thumbWidth, thumbHeight);
response(TimiJava.defaultIfNull(thumb, attachment));
}
protected void response(Attachment attachment) {
HttpServletResponse resp = TimiSpring.getResponse();
try {
if (TimiJava.isEmpty(attachment)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
resp.setContentType(attachment.getMimeType());
attachment.doResponse();
} catch (Exception e) {
log.error("response attachment error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
@@ -1,87 +1,264 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.service.CommentReplyService;
import com.imyeyu.api.modules.user.entity.UserBehaviorRecord;
import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.api.modules.common.vo.comment.CommentCreateReq;
import com.imyeyu.api.modules.common.vo.comment.CommentLayerQueryReq;
import com.imyeyu.api.modules.common.vo.comment.CommentLayerResp;
import com.imyeyu.api.modules.common.vo.comment.CommentNodeResp;
import com.imyeyu.api.modules.common.vo.comment.CommentReplyReq;
import com.imyeyu.api.modules.common.vo.comment.CommentThreadResp;
import com.imyeyu.api.modules.common.vo.comment.FlatCommentListResp;
import com.imyeyu.api.modules.common.vo.comment.NestedCommentListResp;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.util.ResponseView;
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;
/**
* 评论操作接口
* <p>*评论回复只依赖评论而不含业务关联,评论有关联业务,所以此接口是通用接口
*
* @author 夜雨
* @since 2021-02-23 21:36
*/
@Slf4j
@Validated
@RestController
@RequiredArgsConstructor
@RequestMapping("/comment")
public class CommentController {
private final CommentService service;
private final CommentReplyService replyService;
/**
* 创建主评论
*
* @param req 创建请求
* @return 评论树详情
*/
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@PostMapping("/create")
public void create(@Valid @RequestBody CaptchaData<Comment> captchaData) {
service.create(captchaData.getData());
}
@RequestRateLimit
@PostMapping("/list")
public PageResult<Comment> list(@Valid @RequestBody Page<Comment> page) {
return service.page(page);
public Comment create(@RequestBody @Valid CommentCreateReq req) {
Comment entity = new Comment();
entity.setBizType(req.getBizType());
entity.setBizId(req.getBizId());
entity.setUserId(req.getUserId());
entity.setUserNick(req.getUserNick());
entity.setContent(req.getContent());
entity.setIp(TimiSpring.getRequestIP());
service.create(entity);
return service.detailTree(entity.getId(), req.getUserId());
}
/**
* 创建评论回复
* 回复评论
*
* @param request 回复数据
* @param req 回复请求
* @return 评论树详情
*/
@AOPLog
@CaptchaValid(CaptchaFrom.COMMENT_REPLY)
@EnableSetting(value = SettingKey.ENABLE_COMMENT, message = "comment.off_service")
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@PostMapping("/reply/create")
public void createReply(@Valid @RequestBody CaptchaData<CommentReply> request) {
replyService.create(request.getData());
@PostMapping("/reply")
public Comment reply(@RequestBody @Valid CommentReplyReq req) {
Comment entity = new Comment();
entity.setParentId(req.getParentId());
entity.setUserId(req.getUserId());
entity.setUserNick(req.getUserNick());
entity.setContent(req.getContent());
entity.setIp(TimiSpring.getRequestIP());
service.create(entity);
return service.detailTree(entity.getId(), req.getUserId());
}
/**
* 获取回复列表
* 删除当前用户自己的评论
*
* @param page 页面参数
* @return 回复列表
* @param id 评论 ID
* @param userId 用户 ID
*/
@AOPLog
@RequestRateLimit
@RequestMapping("/reply/list")
public PageResult<CommentReply> pageReplies(@Valid @RequestBody Page<CommentReply> page) {
// 通用接口,只允许查询评论的回复
CommentReply example = new CommentReply();
if (page.getEqualsExample() != null) {
example.setCommentId(page.getEqualsExample().getCommentId());
}
page.setEqualsExample(example);
page.setLikesExample(null);
return replyService.page(page);
@PostMapping("/delete")
public void delete(@RequestParam String id, @RequestParam String userId) {
service.deleteOwned(id, userId);
}
/**
* 点赞评论
*
* @param commentId 评论 ID
* @param userId 用户 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/like")
public void like(@RequestParam String commentId, @RequestParam String userId) {
service.like(commentId, userId);
}
/**
* 不喜欢评论
*
* @param commentId 评论 ID
* @param userId 用户 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/unlike")
public void unlike(@RequestParam String commentId, @RequestParam String userId) {
service.unlike(commentId, userId);
}
/**
* 查询用户点赞评论记录
*
* @param page 分页参数
* @param userId 用户 ID
* @return 点赞评论记录分页
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@PostMapping("/like/list")
public PageResult<UserBehaviorRecord> listLikedRecord(@RequestBody Page<UserBehaviorRecord> page, @RequestParam String userId) {
return service.listLikedRecord(page, userId);
}
/**
* 查询扁平主评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 扁平主评论列表
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/flat/list")
public FlatCommentListResp listFlatRoot(
@RequestParam String bizType,
@RequestParam String bizId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.listFlatRoot(bizType, bizId, offset, currentUserId);
}
/**
* 查询扁平评论线程
*
* @param commentId 评论 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 评论线程
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/flat/thread")
public CommentThreadResp getFlatThread(
@RequestParam String commentId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.getUserThread(commentId, offset, currentUserId);
}
/**
* 查询层级主评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 层级主评论列表
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/nested/list")
public NestedCommentListResp listNestedRoot(
@RequestParam String bizType,
@RequestParam String bizId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.listNestedRoot(bizType, bizId, offset, currentUserId);
}
/**
* 查询评论节点
*
* @param commentId 评论 ID
* @param currentUserId 当前用户 ID,可为 null
* @return 评论节点
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/nested/detail")
public CommentNodeResp getNestedDetail(@RequestParam String commentId, @RequestParam(required = false) String currentUserId) {
return service.getUserNode(commentId, currentUserId);
}
/**
* 查询直属子评论分页
*
* @param commentId 评论 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID,可为 null
* @return 子评论分页
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/nested/children")
public CommentLayerResp getNestedChildren(
@RequestParam String commentId,
@RequestParam(defaultValue = "0") int offset,
@RequestParam(required = false) String currentUserId
) {
return service.getUserChildren(commentId, offset, currentUserId);
}
/**
* 查询单层评论列表
*
* @param req 查询请求
* @param currentUserId 当前用户 ID,可为 null
* @return 评论层
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@PostMapping("/layer/list")
public CommentLayerResp listLayer(@RequestBody CommentLayerQueryReq req, @RequestParam(required = false) String currentUserId) {
return service.listSdkLayer(req, currentUserId);
}
/**
* 查询评论树详情
*
* @param id 评论 ID
* @param currentUserId 当前用户 ID,可为 null
* @return 评论树详情
*/
@JsonView(ResponseView.Public.class)
@RequestRateLimit
@GetMapping("/detail/tree")
public Comment detailTree(@RequestParam String id, @RequestParam(required = false) String currentUserId) {
return service.detailTree(id, currentUserId);
}
}
@@ -1,78 +1,23 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.ImageType;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.entity.Task;
import com.imyeyu.api.modules.common.entity.Template;
import com.imyeyu.api.modules.common.entity.Version;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.ClipboardService;
import com.imyeyu.api.modules.common.service.FeedbackService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.TaskService;
import com.imyeyu.api.modules.common.service.TempFileService;
import com.imyeyu.api.modules.common.service.TemplateService;
import com.imyeyu.api.modules.common.service.VersionService;
import com.imyeyu.api.modules.common.vo.ClipboardRequest;
import com.imyeyu.api.modules.common.vo.FeedbackRequest;
import com.imyeyu.api.modules.common.vo.TempFileResponse;
import com.imyeyu.api.modules.system.util.ResourceHandler;
import com.imyeyu.api.util.CaptchaManager;
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.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.imyeyu.spring.bean.RequestRange;
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 com.imyeyu.utils.Decoder;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Cleanup;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika;
import org.springframework.data.mongodb.gridfs.GridFsResource;
import org.springframework.http.MediaType;
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.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
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;
/**
* 系统接口
@@ -87,20 +32,7 @@ import java.util.stream.Collectors;
@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 TempFileService tempFileService;
private final AttachmentService attachmentService;
private final ClipboardService clipboardService;
private final ObjectMapper jackson;
private final Yaml yaml;
private final GridFSBucket gridFSBucket;
private final CaptchaManager captchaManager;
private final ResourceHandler resourceHandler;
@AOPLog
@RequestMapping("")
@@ -108,399 +40,56 @@ public class CommonController {
return "IT WORKING! " + TimiSpring.getRequestIP();
}
/**
* 获取共享剪切板内容
*
* @param id 会话 ID
* @return 剪切板内容
*/
@AOPLog
@RequestRateLimit
@GetMapping("/clipboard/{id}")
public String clipboardGet(@Valid @NotBlank @PathVariable("id") String id) {
return clipboardService.getContent(id);
}
/**
* 设置共享剪切板内容
*
* @param id 会话 ID
* @param request 请求内容
*/
@AOPLog
@RequestRateLimit
@PostMapping("/clipboard/{id}")
public void clipboardSet(@Valid @NotBlank @PathVariable("id") String id, @Valid @RequestBody ClipboardRequest request) {
clipboardService.setContent(id, request.getContent());
}
/**
* 订阅共享剪切板实时更新
*
* @param id 会话 ID
* @return SSE 发射器
*/
@AOPLog
@IgnoreGlobalReturn
@GetMapping(value = "/clipboard/stream/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter clipboardStream(@Valid @NotBlank @PathVariable("id") String id) {
return clipboardService.subscribe(id);
}
/**
* 获取验证码
*
* @param width 宽度
* @param height 高度
* @param from 来源
* @param response 返回对象
* @param width 宽度
* @param height 高度
* @return Base64 编码的图形验证码和唯一识别 ID
*/
@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");
public CaptchaManager.Result captcha(Integer width, Integer height) {
TimiException.requiredTrue(width != null && 64 < width, "not found width or less than 64");
TimiException.requiredTrue(height != null && 19 < height, "not found height or less than 19");
return captchaManager.generate(width, height);
}
/**
* 获取验证码(返回图形数据流,唯一识别 ID 在返回头 X-Captcha-ID
*
* @param width 宽度
* @param height 高度
*/
@GetMapping("/captcha/img")
public void captchaImg(Integer width, Integer height, HttpServletResponse resp) {
CaptchaManager.Result result = captchaManager.generate(width, height);
resp.setHeader("Pragma", "no-cache");
resp.setHeader("Cache-Control", "no-cache");
resp.setHeader("X-Captcha-ID", result.id());
resp.setHeader("Access-Control-Expose-Headers", "X-Captcha-ID");
resp.setDateHeader("Expires", -1);
resp.setContentType("image/png");
try {
// 宽度
ServletOutputStream os = resp.getOutputStream();
if (width < 64) {
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "jpg", response.getOutputStream());
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "png", os);
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());
ImageIO.write(captchaManager.error(TimiCode.ARG_BAD), "png", os);
return;
}
// 输出图像流
ImageIO.write(captchaManager.generate(from, width, height), "jpg", response.getOutputStream());
} catch (Exception e) {
log.error("CommonController.getCaptcha", e);
IO.toOutputStream(os, Decoder.base64(result.data().split(",")[1]));
} catch (IOException e) {
log.error("captcha img error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
try {
ImageIO.write(captchaManager.error(TimiCode.ERROR), "jpg", response.getOutputStream());
ImageIO.write(captchaManager.error(TimiCode.ERROR), "png", resp.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.getCaptchaId());
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) throws JsonProcessingException {
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 = jackson.writeValueAsString(obj);
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = jackson.readValue(setting.getValue(), new TypeReference<>() {});
result = yaml.dump(obj);
}
}
}
return result;
}
@RequestRateLimit
@PostMapping("/setting/map")
public Map<SettingKey, String> mapSettingByKeys(@RequestBody Map<SettingKey, Map<String, Object>> settingMap) throws JsonProcessingException {
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(jackson.writeValueAsString(obj));
}
}
case YAML -> {
if (setting.getType() == Setting.Type.JSON) {
Map<String, Object> obj = jackson.readValue(setting.getValue(), new TypeReference<>() {});
setting.setValue(new Yaml().dump(obj));
}
}
}
}
}
return result.stream().collect(Collectors.toMap(Setting::getKey, Setting::getValue));
}
@AOPLog
@RequestRateLimit
@GetMapping("/attachment/{mongoId}")
public Attachment getAttachment(@PathVariable String mongoId) {
return attachmentService.getByMongoId(mongoId);
}
@AOPLog
@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());
}
}
/**
* 上传临时文件
*
* @param files 文件列表
* @param ttl 缓存时长(毫秒),最长 3 天(259200000ms
* @return 临时文件响应列表
*/
@AOPLog
@PostMapping("/temp/file/upload")
public List<TempFileResponse> tempFileUpload(@RequestParam("file") List<MultipartFile> files, @RequestParam(value = "ttl", required = false) Long ttl) {
return tempFileService.store(files, ttl);
}
/**
* 读取临时文件
*
* @param mongoId mongoId
* @param req 请求
* @param resp 返回
*/
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping("/temp/file/read/{mongoId}")
public void tempFileRead(@PathVariable String mongoId, HttpServletRequest req, HttpServletResponse resp) {
try {
Attachment attach = attachmentService.getByMongoId(mongoId);
if (TimiJava.isEmpty(attach)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
GridFSFile file = attachmentService.readByMongoId(attach.getMongoId());
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);
resp.setContentType(attach.getMimeType());
resourceHandler.handleRequest(req, resp);
} catch (Exception e) {
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 下载临时文件
*
* @param mongoId
* @param resp
*/
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@RequestMapping("/temp/file/download/{mongoId}")
public void tempFileDownload(@PathVariable String mongoId, HttpServletResponse resp) {
try {
Attachment attach = attachmentService.getByMongoId(mongoId);
if (TimiJava.isEmpty(attach)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
resp.setContentType(attach.getMimeType());
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
resp.setHeader("Accept-Ranges", "bytes");
GridFSFile file = attachmentService.readByMongoId(mongoId);
@Cleanup
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
RequestRange range = TimiSpring.getRequestRange(attach.getSize());
if (range == null) {
// 完整文件
resp.setContentLengthLong(attach.getSize());
resp.setStatus(HttpServletResponse.SC_OK);
IO.toOutputStream(downloadStream, resp.getOutputStream());
} else {
// 分片文件
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
resp.setHeader("Content-Range", "bytes %s-%s/%s".formatted(range.getStart(), range.getEnd(), attach.getSize()));
resp.setContentLengthLong(range.getLength());
IO.toOutputStream(downloadStream, resp.getOutputStream(), range.getStart(), range.getEnd());
resp.flushBuffer();
}
} catch (Exception e) {
log.error("download error", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
@@ -1,26 +1,19 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.common.entity.Icon;
import com.imyeyu.api.modules.common.service.IconService;
import com.imyeyu.api.modules.common.vo.icon.AllResponse;
import com.imyeyu.api.modules.common.vo.icon.NamePage;
import com.imyeyu.api.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 com.imyeyu.network.Network;
import com.imyeyu.spring.annotation.IgnoreGlobalReturn;
import jakarta.servlet.http.HttpServletResponse;
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.io.IOException;
import java.util.List;
import java.util.Map;
/**
* timi-icon 前端接口
@@ -38,60 +31,38 @@ public class IconController {
private final IconService service;
/**
* 获取图标列表
* 查询全部图标名称
*
* @param page 查询列表参数
* @return 图标列表
* @return 图标名称列表
*/
@RequestRateLimit
@PostMapping("/list")
public PageResult<Icon> list(@RequestBody Page page) {
return service.page(page);
@GetMapping("/name/list")
public List<String> listName() {
return service.listName();
}
@GetMapping("/svg/map")
public Map<String, String> mapSvg() {
return service.mapSvg();
}
@GetMapping("/unicode/map")
public Map<String, String> mapUnicode() {
return service.mapUnicode();
}
/**
* 获取所有图标,为了减小传输数据,此接口只返回 name 名称、Unicode 代码和 SVG 路径
* 导出 TTF 字体文件
*
* @param latest 请求缓存的最新数据时间
* @return 所有图标,如果请求缓存的最新时间等于数据库的最新数据时间,不返回任何数据
* @param resp HTTP 响应
* @throws IOException 写出字体文件失败
*/
@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);
@IgnoreGlobalReturn
@GetMapping(value = "/export/ttf", produces = "font/ttf")
public void exportTtf(HttpServletResponse resp) throws IOException {
byte[] body = service.exportTtf();
resp.setContentType("font/ttf");
resp.setContentLength(body.length);
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader("timi-icon.ttf"));
resp.getOutputStream().write(body);
}
}
@@ -0,0 +1,35 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequiredToken;
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;
/**
*
*
* @author 夜雨
* @since 2026-06-13 09:31
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/log")
public class LoggerController {
private final LoggerService service;
@AOPLog
@RequiredToken
@PostMapping("/create")
public void create(@Valid @RequestBody Logger logger) {
service.create(logger);
}
}
@@ -0,0 +1,94 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.annotation.RequireCoreRole;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.bean.CoreRoleCode;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.util.ResponseView;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 多语言管理接口
*
* @author Codex
* @since 2026-07-27
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/multilingual")
public class MultilingualController {
private final MultilingualService service;
/**
* 查询多语言详情
*
* @param id 多语言 ID
* @return 多语言详情
*/
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_READ)
@PostMapping("/detail")
public Multilingual detail(@RequestParam String id) {
return service.get(id);
}
/**
* 创建多语言
*
* @param multilingual 多语言
* @return 创建结果
*/
@AOPLog
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_CREATE)
@PostMapping("/create")
public Multilingual create(@RequestBody Multilingual multilingual) {
service.create(multilingual);
return multilingual;
}
/**
* 更新多语言
*
* @param multilingual 多语言
*/
@AOPLog
@RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_UPDATE)
@PostMapping("/update")
public void update(@RequestBody Multilingual multilingual) {
service.update(multilingual);
}
/**
* 删除多语言
*
* @param id 多语言 ID
*/
@AOPLog
@RequestRateLimit
@RequireCoreRole(CoreRoleCode.ADMIN)
@RequireCorePermission(CorePermissionCode.MULTILINGUAL_DELETE)
@PostMapping("/delete")
public void delete(@RequestParam String id) {
service.delete(id);
}
}
@@ -0,0 +1,100 @@
package com.imyeyu.api.modules.common.controller;
import com.fasterxml.jackson.annotation.JsonView;
import com.imyeyu.api.annotation.RequireCorePermission;
import com.imyeyu.api.bean.CorePermissionCode;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.SettingService;
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 com.imyeyu.spring.util.ResponseView;
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;
/**
*
*
* @author 夜雨
* @since 2026-04-23 16:31
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/system/setting")
public class SettingController {
private final SettingService service;
/**
* 查询配置列表
*
* @param page 分页查询
* @return 配置列表
*/
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_READ)
@PostMapping("/list")
public PageResult<Setting> list(@RequestBody Page<Setting> page) {
return service.page(page);
}
/**
* 查询配置详情
*
* @param id 配置 ID
* @return 配置详情
*/
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@PostMapping("/detail")
public Setting detail(@RequestBody String id) {
return service.get(id);
}
/**
* 创建配置
*
* @param setting 配置
* @return 配置
*/
@AOPLog
@JsonView(ResponseView.Admin.class)
@RequestRateLimit
@PostMapping("/create")
public Setting create(@RequestBody Setting setting) {
service.create(setting);
return setting;
}
/**
* 更新配置
*
* @param setting 配置
*/
@AOPLog
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody Setting setting) {
service.update(setting);
}
/**
* 删除配置
*
* @param id 配置 ID
*/
@AOPLog
@RequestRateLimit
@RequireCorePermission(CorePermissionCode.SETTING_DELETE)
@PostMapping("/delete")
public void delete(@RequestBody String id) {
service.delete(id);
}
}
@@ -1,316 +0,0 @@
package com.imyeyu.api.modules.common.controller;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.annotation.EnableSetting;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.api.modules.common.entity.UserPrivacy;
import com.imyeyu.api.modules.common.service.CommentReplyService;
import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.api.modules.common.service.UserConfigService;
import com.imyeyu.api.modules.common.service.UserPrivacyService;
import com.imyeyu.api.modules.common.service.UserProfileService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.common.vo.user.EmailVerifyCallbackRequest;
import com.imyeyu.api.modules.common.vo.user.LoginRequest;
import com.imyeyu.api.modules.common.vo.user.LoginResponse;
import com.imyeyu.api.modules.common.vo.user.RegisterRequest;
import com.imyeyu.api.modules.common.vo.user.UpdatePasswordByKeyRequest;
import com.imyeyu.api.modules.common.vo.user.UpdatePasswordRequest;
import com.imyeyu.api.modules.common.vo.user.UserRequest;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page;
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(@RequestBody 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<Comment> listComment(@Valid @RequestBody Page<Comment> page) {
Comment example = new Comment();
example.setUserId(service.getLoginUser().getId());
page.setEqualsExample(example);
return commentService.page(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/delete")
public void deleteComment(@RequestBody Long commentId) {
commentService.get(commentId);
commentService.delete(commentId);
}
/**
* 获取用户被回复的评论
*/
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/list")
public PageResult<CommentReply> listCommentReply(@Valid @RequestBody Page<CommentReply> page) {
CommentReply example = new CommentReply();
example.setReceiverId(service.getLoginUser().getId());
page.setEqualsExample(example);
return commentReplyService.page(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/comment/reply/delete")
public void deleteCommentReply(@RequestBody 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(@RequestBody 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);
}
}
@@ -0,0 +1,82 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.bean.attachment.SettingSupport;
import com.imyeyu.spring.annotation.table.PageIgnore;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 文章
*
* @author 夜雨
* @since 2021-03-01 17:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Article extends UUIDEntity implements SettingSupport {
/**
* 类型
*
* @author 夜雨
* @since 2026-06-05 15:55
*/
public enum BizType {
/** 博客 */
BLOG
}
/**
* 正文类型
*
* @author 夜雨
* @since 2026-06-05 15:56
*/
public enum ContentType {
/** 纯文本 */
TEXT,
/** Markdown */
MARKDOWN,
/** TipTap 富文本 */
TIP_TAP
}
/** 业务类型 */
protected BizType bizType;
/** 业务 ID */
protected String bizId;
/** 标题 */
protected String title;
/** 摘要 */
protected String digest;
/** 正文类型 */
protected ContentType contentType;
/** 内容 */
@PageIgnore
protected String content;
/** 阅读数量 */
protected Integer reads;
/** 喜欢数量 */
protected Integer likes;
@Transient
protected List<Setting> settingList;
@Transient
protected List<Comment> commentList;
}
@@ -2,16 +2,17 @@ package com.imyeyu.api.modules.common.entity;
import com.fasterxml.jackson.databind.JsonNode;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.bean.MultilingualHandler;
import com.imyeyu.api.modules.common.bean.attachment.AccessKey;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity;
import lombok.AllArgsConstructor;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
/**
@@ -20,61 +21,33 @@ import java.io.InputStream;
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Attachment extends Entity implements MultilingualHandler {
public class Attachment extends UUIDEntity {
/**
* 附件类型
*
* @author 夜雨
* @since 2023-08-21 16:32
*/
@Getter
@AllArgsConstructor
///
///
/// @author 夜雨
/// @since 2026-07-17 20:52
public enum BizType {
/** 用户 */
USER,
/** 文章 */
ARTICLE,
GAO_CUSTOMER,
/** Git */
GIT,
GAO_REGISTRATION_EVENT_RECORD,
/** 歌词 */
LYRIC,
TEMP_FILE,
/** ForeverMC */
FMC,
/** 镜像 */
MIRROR,
/** 日记 */
JOURNAL,
/** 日记出行 */
JOURNAL_TRAVEL,
/** 日记瞬间 */
JOURNAL_MOMENT,
/** 系统 */
SYSTEM,
/** 临时文件 */
TEMP_FILE
MIRROR
}
protected BizType bizType;
protected Long bizId;
protected String bizId;
protected String attachType;
protected String mongoId;
@MultilingualField
protected String title;
protected String name;
@@ -93,9 +66,22 @@ public class Attachment extends Entity implements MultilingualHandler {
protected Long destroyAt;
/** 数据流 */
@Transient
protected InputStream inputStream;
/** 保存自临时文件 ID */
@Transient
protected String tempFileId;
/** 访问密钥 */
@Transient
protected AccessKey accessKey;
/** 请求文件 */
@Transient
protected MultipartFile file;
public InputStream openInputStream() {
AttachmentService service = TimiServerAPI.applicationContext.getBean(AttachmentService.class);
return service.getInputStreamByMongoId(mongoId);
@@ -108,4 +94,9 @@ public class Attachment extends Entity implements MultilingualHandler {
public <T extends Enum<T>> T getAttachTypeValue(Class<T> attachTypeClass) {
return Ref.toType(attachTypeClass, attachType);
}
/** 执行接口流式返回 */
public void doResponse() throws IOException {
TimiSpring.responseRangeStream(openInputStream(), size);
}
}
@@ -1,20 +1,11 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.blog.entity.Article;
import com.imyeyu.api.modules.blog.service.implement.ArticleServiceImplement;
import com.imyeyu.api.modules.common.bean.CommentSupport;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.api.modules.git.bean.gitea.Repository;
import com.imyeyu.api.modules.git.service.implement.IssueServiceImplement;
import com.imyeyu.api.modules.git.service.implement.MergeServiceImplement;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.entity.UUIDEntity;
import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
@@ -23,71 +14,94 @@ import java.util.List;
* 评论
*
* @author 夜雨
* @since 2021-02-25 14:46
* @since 2026-05-27 17:20
*/
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Comment extends Entity {
public class Comment extends UUIDEntity {
/**
* 关联业务类型
* <p>
* TODO 添加模块名称以便区别邮件通知推送来源,使用多语言键
* 评论状态
*
* @author 夜雨
* @since 2023-08-06 23:42
* @since 2026-05-27 17:20
*/
@Getter
@AllArgsConstructor
public enum BizType {
public enum Status {
ARTICLE(ArticleServiceImplement.class),
/** 正常 */
NORMAL,
GIT_ISSUE(IssueServiceImplement.class),
GIT_MERGE(MergeServiceImplement.class);
final Class<? extends GettableService<? extends CommentSupport, Long>> serviceClass;
/** 隐藏 */
HIDDEN
}
/** 关联业务类型 */
private BizType bizType;
/** 业务类型 */
protected String bizType;
/** 关联业务 ID */
private Long bizId;
/** 业务 ID */
protected String bizId;
/** 发送用户 ID,登录用户评论有值,游客无 */
private Long userId;
/** 父评论 ID,主评论为 null */
protected String parentId;
/** 发送用户昵称,游客评论有值,登录用户无 */
private String nick;
/** 根评论 ID,主评论为自身 ID */
protected String rootId;
/** 评论数据 */
/** 评论用户 ID */
protected String userId;
/** 评论用户昵称,游客评论时使用 */
protected String userNick;
/** 被回复用户 ID */
protected String replyUserId;
/** 被回复用户昵称,被回复人为游客时使用 */
protected String replyUserNick;
/** 评论层级,1 为主评论 */
protected Integer level;
/** 评论内容 */
@NotBlank
private String content;
protected String content;
/** 发送用户 IP */
private String ip;
/** IP 地址 */
protected String ip;
/** 回复数 */
/** 回复数 */
protected Integer replyCount;
/** 点赞数 */
protected Integer likeCount;
/** 评论状态 */
protected Status status;
/** true 为置顶 */
protected Boolean isTop;
/** 最后回复时间 */
protected Long lastReplyAt;
/** 评论用户信息 */
@Transient
private long repliesLength;
protected User user;
/** 发送用户 */
/** 被回复用户信息 */
@Transient
private UserView user;
protected User replyUser;
/** 关联文章 */
/** 子评论列表 */
@Transient
private Article article;
protected List<Comment> replyList;
/** 关联仓库 */
/** true 为当前用户已点赞 */
@Transient
private Repository repository;
protected Boolean liked;
/** 回复列表 */
/** true 为当前用户已不喜欢 */
@Transient
private List<CommentReply> replies;
protected Boolean unliked;
}
@@ -1,59 +0,0 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 评论回复
*
* @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;
/** 所属评论 */
@Transient
private Comment comment;
/** 发送用户 */
@Transient
private UserView sender;
/** 回复用户 */
@Transient
private UserView receiver;
}
@@ -0,0 +1,47 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 数据字典
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DataDict extends UUIDEntity {
/** 业务类型 */
private String bizType;
/** 代码 */
private String code;
/** 父级代码 */
private String parentCode;
/** 字典名称语言键 */
private String nameLangId;
/** 字典名称(读取) */
@Transient
protected String name;
/** 搜索名称 */
@Transient
protected String searchName;
/** 字典中文名称(写入) */
@Transient
protected String zhCN;
/** 选项列表 */
@Transient
protected List<DataDictOption> dataDictOptionList;
}
@@ -0,0 +1,50 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 数据字典选项
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DataDictOption extends UUIDEntity {
/** 所属字典 ID */
private String dictId;
/** 值代码 */
private String valueCode;
/** 父级值代码 */
private String valueParentCode;
/** 值语言键 */
private String valueLangId;
/** 值(读取) */
@Transient
protected String value;
/** 中文值(写入) */
@Transient
protected String zhCN;
@Transient
protected String searchValue;
/** 所属字典 */
@Transient
protected DataDict dataDict;
/** 附加参数设置 */
@Transient
protected List<Setting> settingList;
}
@@ -0,0 +1,34 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.DeleteColumn;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.IDEntity;
import lombok.Data;
/**
* 邮件过滤
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
public class EmailFilter implements IDEntity<String>, Creatable, Deletable {
/** 主键 */
@Id
@AutoUUID
private String id;
/** 邮箱 */
private String email;
/** 创建时间 */
private Long createdAt;
/** 删除时间 */
@DeleteColumn
private Long deletedAt;
}
@@ -1,43 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,23 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,21 +0,0 @@
package com.imyeyu.api.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;
}
@@ -2,7 +2,7 @@ package com.imyeyu.api.modules.common.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.spring.entity.UUIDEntity;
/**
* 字体图标
@@ -12,7 +12,7 @@ import com.imyeyu.spring.entity.Entity;
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Icon extends Entity {
public class Icon extends UUIDEntity {
/** 名称 */
private String name;
@@ -1,28 +1,43 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.java.bean.Language;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.IDEntity;
import com.imyeyu.spring.entity.Updatable;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
/**
* @author 夜雨
* @since 2023-10-24 16:41
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Multilingual extends Language implements Serializable, IDEntity<Long>, Creatable, Updatable, Deletable {
public class Multilingual extends UUIDEntity {
protected Long id;
protected String zhCN;
protected Long createdAt;
protected String zhTW;
protected Long updatedAt;
protected String enUS;
protected Long deletedAt;
protected String ruRU;
protected String jaJP;
protected String deDE;
protected String koKR;
protected Long lastAccessAt;
public String getValue(Language.Enum language) {
return switch (language) {
case zh_CN -> zhCN;
case zh_TW -> zhTW;
case en_US -> enUS;
case ru_RU -> ruRU;
case ja_JP -> jaJP;
case de_DE -> deDE;
case ko_KR -> koKR;
};
}
}
@@ -0,0 +1,44 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.AutoUUID;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
/**
* 通知
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Notify extends UUIDEntity {
/** 主键 */
@Id
@AutoUUID
private String id;
/** 业务类型 */
private String bizType;
/** 提醒业务类型 */
private String bizRemindedType;
/** 提醒业务 ID */
private String bizRemindedId;
/** 参数 */
private String args;
/** 创建者 */
private String createdBy;
@Transient
protected List<NotifyDetail> detailList;
}
@@ -0,0 +1,97 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 通知详情
*
* @author 夜雨
* @since 2026-05-13 14:40
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class NotifyDetail extends UUIDEntity {
/**
*
*
* @author 夜雨
* @since 2025-10-28 15:53
*/
@AllArgsConstructor
public enum MsgType {
SMS,
MAIL,
HTTP,
WECHAT;
}
/**
*
*
* @author 夜雨
* @since 2025-10-28 15:55
*/
public enum Status {
WAITING,
REMINDED,
TIMEOUT,
CANCEL,
FAIL
}
/** 所属通知 ID */
private String notifyId;
/** 消息类型 */
private MsgType msgType;
/** 模板 ID */
private String templateId;
/** 标题 */
private String subject;
/** 内容 */
private String data;
/** 参数 */
private String args;
/** 发送来源 */
private String sendFrom;
/** 发送去向 */
private String sendTo;
/** 发送时间 */
private Long sendAt;
/** 重试计数 */
private Integer retry;
/** 超时时间 */
private Long timeoutAt;
/** 结果描述 */
private String resultDesc;
/** 状态 */
private Status status;
@Transient
protected Notify notify;
}
@@ -1,10 +1,20 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.spring.annotation.table.Id;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Updatable;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.bean.SettingModule;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import com.imyeyu.spring.util.ResponseView;
import com.imyeyu.utils.Time;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
/**
* 系统配置
@@ -13,7 +23,9 @@ import lombok.Data;
* @since 2021-07-20 21:46
*/
@Data
public class Setting implements Creatable, Updatable {
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class Setting extends UUIDEntity {
/**
*
@@ -21,31 +33,486 @@ public class Setting implements Creatable, Updatable {
* @author 夜雨
* @since 2025-01-10 17:08
*/
public enum Type {
INTEGER,
public enum ValueType {
/** 字符串 */
STRING,
JSON,
/** 单选 */
SELECT_RADIO,
YAML,
/** 多选 */
SELECT_CHECKBOX,
/** 整数 */
INTEGER,
/** 十六进制颜色 */
COLOR,
/** 浮点数 */
FLOAT,
/** 日期 */
DATE,
/** 日期时间 */
DATETIME,
/** 时间 */
TIME,
/** 时长 */
DURATION,
/** 布尔值 */
BOOLEAN,
/** JSON 列表 */
JSON_LIST,
/** JSON 对象 */
JSON_OBJECT
}
@Id
private SettingKey key;
@JsonView(ResponseView.Public.class)
private String bizType;
@JsonView(ResponseView.Public.class)
private String bizId;
@JsonView(ResponseView.Public.class)
private String code;
@JsonView(ResponseView.Public.class)
private String description;
@JsonView(ResponseView.Public.class)
private String value;
private Type type;
@JsonView(ResponseView.Public.class)
private ValueType valueType;
private boolean isPrivate;
@JsonView(ResponseView.Public.class)
private JsonNode valueArgs;
private Long createdAt;
@JsonView(ResponseView.Admin.class)
private Boolean isPrivate;
private Long updatedAt;
@Transient
private Enum<?> keyRaw;
public boolean isPublic() {
return !isPrivate;
public Setting(Enum<?> key) {
setKeyRaw(key);
}
public Setting(Enum<?> key, Object value, ValueType valueType) {
setKeyRaw(key);
this.value = value.toString();
this.valueType = valueType;
}
/**
* 以字符串形式返回配置值
*
* @return 配置字符串值
*/
public String asString() {
return value;
}
public Long asTime() {
return Time.parseToMS(asString());
}
/**
* 以整数形式返回配置值
*
* @return 配置整数值
* @throws NumberFormatException 配置值不是合法整数
*/
public Integer asInteger() {
return Integer.parseInt(value);
}
/**
* 以长整数形式返回配置值
*
* @return 配置长整数值
* @throws NumberFormatException 配置值不是合法长整数
*/
public Long asLong() {
return Long.parseLong(value);
}
/**
* 以双精度浮点数形式返回配置值
*
* @return 配置双精度浮点数值
* @throws NumberFormatException 配置值不是合法双精度浮点数
*/
public Double asDouble() {
return Double.parseDouble(value);
}
/**
* 以布尔值形式返回配置值
*
* @return 配置布尔值
*/
public Boolean asBoolean() {
return Boolean.parseBoolean(value);
}
public boolean isTrue() {
return asBoolean();
}
public boolean isFalse() {
return !isTrue();
}
/**
* 以 JSON 节点形式返回配置值
*
* @return 配置 JSON 节点
* @throws IllegalArgumentException 配置值不是合法 JSON
*/
public JsonNode asJsonNode() {
return asJsonNode(JsonNode.class);
}
/**
* 以 JSON 数组形式返回配置值
*
* @return 配置 JSON 数组
* @throws IllegalArgumentException 配置值不是合法 JSON 数组
*/
public ArrayNode asArrayNode() {
return asJsonNode(ArrayNode.class);
}
/**
* 以 JSON 对象形式返回配置值
*
* @return 配置 JSON 对象
* @throws IllegalArgumentException 配置值不是合法 JSON 对象
*/
public ObjectNode asObjectNode() {
return asJsonNode(ObjectNode.class);
}
/**
* 解析配置 JSON 值
*
* @param type 目标类型
* @param <T> 目标类型
* @return 解析后的 JSON 节点
* @throws IllegalArgumentException 配置值不是合法 JSON
*/
public <T extends JsonNode> T asJsonNode(Class<T> type) {
if (value == null) {
return null;
}
try {
ObjectMapper mapper = TimiServerAPI.applicationContext.getBean(ObjectMapper.class);
return mapper.readValue(value, type);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException("setting value is not valid json: %s".formatted(value), e);
}
}
public void setKeyRaw(Enum<?> key) {
this.keyRaw = key;
this.bizType = SettingModule.fromClass(key.getClass()).toString();
this.code = key.toString();
}
public boolean is(Enum<?> key) {
return SettingModule.fromClass(key.getClass()).toString().equals(bizType) && key.toString().equals(code);
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public static final class Module {
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:39
*/
public enum Common {
/** 微信收款码 */
WECHAT_RECEIVE_QR_CODE
}
/**
* 域名
*
* @author 夜雨
* @since 2026-04-26 17:18
*/
public enum Domain {
ROOT,
API,
GIT,
BLOG,
SPACE,
FOREVER_MC
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:14
*/
public enum Blog {
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:03
*/
public enum User {
/** 启用登录 */
ENABLE_LOGIN,
/** 启用注册 */
ENABLE_REGISTER,
/** 允许数据更新 */
ENABLE_PROFILE_UPDATE,
/** 令牌有效期 */
TOKEN_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:21
*/
public enum Comment {
/** 启用服务 */
ENABLE
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:03
*/
public enum ICP {
IMYEYU_COM
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 15:01
*/
public enum ForeverMC {
PLAYER_BIND_MAX,
LOGIN_TOKEN_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:57
*/
public enum Multilingual {
/** 翻译 API */
TRANSLATE_API,
/** 翻译 AppId */
TRANSLATE_APP_ID,
/** 翻译密钥 */
TRANSLATE_KEY,
/** 翻译计划任务表达式 */
TRANSLATE_CORN,
/** 缓存时长 */
CACHE_TTL
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:55
*/
public enum Gitea {
/** 接口地址 */
API,
/** 关于页面所属文章 ID */
ABOUT_ARTICLE_ID,
/** 仓库文件地址 */
REPOSITORY_PATH
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:52
*/
public enum Music {
/** 启用服务 */
ENABLE,
/** 通信消息最大帧长度 */
MAX_FRAME_LENGTH,
/** 受控端 IP */
PLAYER_IP,
/** 受控端端口 */
PLAYER_PORT,
/** 控制端 IP */
CONTROLLER_IP,
/** 控制端端口 */
CONTROLLER_PORT,
/** 控制端入口地址 */
CONTROLLER_URI,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:49
*/
public enum Journal {
/** 启用服务 */
ENABLE,
/** 审核中 */
REVIEWING,
/** 访问密钥 */
KEY,
/** 微信小程序 AppId */
APP_ID,
/** 微信小程序 AppSecret */
APP_SECRET,
/** 备忘录内容 */
MEMO,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:47
*/
public enum TempFile {
/** 启用服务 */
ENABLE,
/** 最小缓存时长 */
TTL_MIN,
/** 默认缓存时长 */
TTL_DEFAULT,
/** 最大缓存时长 */
TTL_MAX,
/** 已过期文件残留时长 */
RESIDUE_TTL,
/** 每个 IP 限制有效临时文件容量 */
LIMIT_SIZE_OF_IP
}
/**
*
*
* @author 夜雨
* @since 2026-04-27 14:35
*/
public enum FileSync {
ENABLE,
CRON,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public enum System {
/** 文件系统基准路径 */
FILE_BASE_PATH,
/** 设置缓存时长 */
SETTING_TTL,
/** 哀悼滤镜 */
ENABLE_MOURN_FILTER,
}
/**
*
*
* @author 夜雨
* @since 2026-04-24 14:23
*/
public enum SystemStatus {
/** 状态采集数据量 */
LIMIT,
/** 状态采集网卡 */
NETWORK_MAC,
/** 状态采集频率 */
RATE,
}
}
}
@@ -1,7 +1,7 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.bean.MultilingualHandler;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@@ -11,29 +11,13 @@ import lombok.EqualsAndHashCode;
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Tag extends Entity implements MultilingualHandler {
public class Tag extends UUIDEntity {
/**
*
*
* @author 夜雨
* @since 2024-08-28 14:26
*/
public enum BizType {
protected String langKey;
ARTICLE,
MUSIC,
SERVER_FILE,
WALLPAPER
}
protected BizType bizType;
protected String bizID;
@MultilingualHandler.MultilingualField
@Transient
protected String value;
@Transient
protected String zhCN;
}
@@ -0,0 +1,25 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.spring.entity.UUIDEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 标签应用
*
* @author 夜雨
* @since 2025-12-02 17:01
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TagApply extends UUIDEntity {
/** 业务类型 */
protected String bizType;
/** 业务 ID */
protected String bizId;
/** 标签 ID */
protected String tagId;
}
@@ -1,40 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,48 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,33 +0,0 @@
package com.imyeyu.api.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;
}
@@ -1,68 +0,0 @@
package com.imyeyu.api.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;
}
}
@@ -1,33 +0,0 @@
package com.imyeyu.api.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;
}
}
@@ -1,51 +0,0 @@
package com.imyeyu.api.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();
}
}
@@ -1,66 +0,0 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.api.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;
}
}
@@ -1,21 +0,0 @@
package com.imyeyu.api.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;
}
@@ -0,0 +1,20 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* 文章
*
* @author 夜雨
* @since 2021-02-23 21:34
*/
public interface ArticleMapper extends BaseMapper<Article, String> {
@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);
}
@@ -12,25 +12,46 @@ import java.util.List;
* @author 夜雨
* @since 2023-08-15 10:22
*/
public interface AttachmentMapper extends BaseMapper<Attachment, Long>, RawMapper<Attachment, Long> {
public interface AttachmentMapper extends BaseMapper<Attachment, String>, RawMapper<Attachment, String> {
/** 有效条件,非删除和销毁 */
String VALID = NOT_DELETE + " AND (`destroy_at` IS NULL OR " + UNIX_TIME + " < `destroy_at`)";
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} " + VALID + LIMIT_1)
Attachment selectByBizId(Attachment.BizType bizType, long bizId);
Attachment selectByBizId(Attachment.BizType bizType, String bizId);
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} " + VALID)
List<Attachment> selectAllByBizId(Attachment.BizType bizType, String 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);
Attachment selectByAttachType(Attachment.BizType bizType, String bizId, String attachType);
List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, Page<Attachment> page);
List<Attachment> selectByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes, Page<Attachment> page);
long countByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes);
long countByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes);
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, List<String> md5s);
/**
* 按源附件 ID 与请求尺寸查询已缓存缩略图
*
* @param bizType 源附件业务类型
* @param sourceId 源附件 ID
* @param requestWidth 请求宽度,{@code null} 表示未指定
* @param requestHeight 请求高度,{@code null} 表示未指定
* @return 缩略图附件,不存在返回 {@code null}
*/
Attachment selectThumb(Attachment.BizType bizType, String sourceId, Integer requestWidth, Integer requestHeight);
/**
* 查询源附件的所有未销毁缩略图(含已软删除)
*
* @param sourceId 源附件 ID
* @return 缩略图列表
*/
@Select("SELECT * FROM attachment WHERE biz_id = #{sourceId} AND attach_type = 'THUMB' AND is_destroyed = FALSE")
List<Attachment> selectThumbsBySourceId(String sourceId);
@Select("SELECT * FROM attachment WHERE `is_destroyed` = FALSE AND `destroy_at` < " + UNIX_TIME)
List<Attachment> selectNeedDestroy();
@@ -2,18 +2,154 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* 评论
* 评论 Mapper
*
* @author 夜雨
* @since 2021-2-23 21:33
* @since 2026-05-27 17:20
*/
public interface CommentMapper extends BaseMapper<Comment, Long> {
public interface CommentMapper extends BaseMapper<Comment, String> {
long countAll(Comment.BizType bizType, Long bizId);
/**
* 根据 ID 列表查询评论
*
* @param idList 评论 ID 列表
* @return 评论列表
*/
List<Comment> listByIdList(List<String> idList);
@Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE user_id = #{userId} ")
void deleteByUserId(Long userId);
/**
* 查询业务下的主评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param offset 起始偏移量
* @param limit 查询条数
* @return 主评论列表
*/
List<Comment> selectRootList(String bizType, String bizId, int offset, int limit);
/**
* 统计业务下的主评论数量
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @return 主评论数量
*/
long countRoot(String bizType, String bizId);
/**
* 查询多个根评论下的回复预览列表
*
* @param rootIdList 根评论 ID 列表
* @param limit 每个根评论最多返回条数
* @return 回复列表
*/
List<Comment> selectReplyPreviewByRootIds(List<String> rootIdList, int limit);
/**
* 查询父评论下的回复列表
*
* @param parentId 父评论 ID
* @param offset 起始偏移量
* @param limit 查询条数
* @return 回复列表
*/
List<Comment> selectReplyListByParentId(String parentId, int offset, int limit);
/**
* 查询根评论下的回复列表
*
* @param rootId 根评论 ID
* @param offset 起始偏移量
* @param limit 查询条数
* @return 回复列表
*/
List<Comment> selectReplyListByRootId(String rootId, int offset, int limit);
/**
* 统计父评论下的回复数量
*
* @param parentId 父评论 ID
* @return 回复数量
*/
long countReplyByParentId(String parentId);
/**
* 统计根评论下的回复数量
*
* @param rootId 根评论 ID
* @return 回复数量
*/
long countReplyByRootId(String rootId);
/**
* 查询多个根评论下的全部回复列表
*
* @param rootIdList 根评论 ID 列表
* @return 回复列表
*/
List<Comment> selectAllReplyListByRootIds(List<String> rootIdList);
/**
* 查询根评论下的全部回复列表
*
* @param rootId 根评论 ID
* @return 回复列表
*/
List<Comment> selectAllReplyListByRootId(String rootId);
/**
* 查询单层评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param parentId 父评论 ID
* @param offset 起始偏移量
* @param limit 查询条数
* @return 评论列表
*/
List<Comment> selectLayerList(String bizType, String bizId, String parentId, int offset, int limit);
/**
* 统计单层评论数量
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param parentId 父评论 ID
* @return 评论数量
*/
long countLayer(String bizType, String bizId, String parentId);
/**
* 查询根评论最新回复时间
*
* @param rootId 根评论 ID
* @return 最新回复时间
*/
@Select("SELECT created_at FROM comment WHERE root_id = #{rootId} AND parent_id IS NOT NULL" + NOT_DELETE + "ORDER BY created_at DESC LIMIT 1")
Long selectLatestReplyAtByRootId(String rootId);
/**
* 调整回复数
*
* @param id 评论 ID
* @param delta 变更值
*/
@Update("UPDATE comment SET reply_count = GREATEST(reply_count + #{delta}, 0), updated_at = " + UNIX_TIME + " WHERE id = #{id}" + NOT_DELETE)
void increaseReplyCount(String id, int delta);
/**
* 调整点赞数
*
* @param id 评论 ID
* @param delta 变更值
*/
@Update("UPDATE comment SET like_count = GREATEST(like_count + #{delta}, 0), updated_at = " + UNIX_TIME + " WHERE id = #{id}" + NOT_DELETE)
void increaseLikeCount(String id, int delta);
}
@@ -1,19 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.spring.mapper.BaseMapper;
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);
}
@@ -1,14 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 评论回复
*
* @author 夜雨
* @since 2021-08-24 10:36
*/
public interface CommentReplyMapper extends BaseMapper<CommentReply, Long> {
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.DataDict;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 数据字典 Mapper
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface DataDictMapper extends BaseMapper<DataDict, String> {
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.DataDictOption;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 数据字典选项 Mapper
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface DataDictOptionMapper extends BaseMapper<DataDictOption, String> {
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.EmailFilter;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 邮件过滤 Mapper
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface EmailFilterMapper extends BaseMapper<EmailFilter, String> {
}
@@ -1,11 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.EmailQueueLog;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* @author 夜雨
* @since 2023-08-10 10:38
*/
public interface EmailQueueLogMapper extends BaseMapper<EmailQueueLog, Long> {
}
@@ -1,22 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.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();
}
@@ -1,13 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Feedback;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 反馈
*
* @author 夜雨
* @since 2021-11-16 22:28
*/
public interface FeedbackMapper extends BaseMapper<Feedback, Long> {
}
@@ -2,9 +2,6 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Icon;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 图标
@@ -12,24 +9,5 @@ import java.util.List;
* @author 夜雨
* @since 2022-09-15 00:02
*/
public interface IconMapper extends BaseMapper<Icon, Long> {
@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, long limit);
long countByLabel(String lang, String label);
List<Icon> listByLabel(String lang, String label, long offset, long 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, long limit);
public interface IconMapper extends BaseMapper<Icon, String> {
}
@@ -2,7 +2,6 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@@ -10,23 +9,7 @@ import java.util.List;
* @author 夜雨
* @since 2023-10-25 10:47
*/
public interface MultilingualMapper extends BaseMapper<Multilingual, Long> {
public interface MultilingualMapper extends BaseMapper<Multilingual, String> {
// 以下临时
@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);
List<Multilingual> selectByIdList(List<String> idList);
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 通知详情 Mapper
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface NotifyDetailMapper extends BaseMapper<NotifyDetail, String> {
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 通知 Mapper
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface NotifyMapper extends BaseMapper<Notify, String> {
}
@@ -1,11 +1,7 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 系统配置
@@ -14,10 +10,4 @@ import java.util.List;
* @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();
}
@@ -7,5 +7,5 @@ import com.imyeyu.spring.mapper.BaseMapper;
* @author 夜雨
* @since 2025-05-30 22:48
*/
public interface TagMapper extends BaseMapper<Tag, Long> {
public interface TagMapper extends BaseMapper<Tag, String> {
}
@@ -1,16 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Task;
import java.util.List;
/**
* 任务服务
*
* @author 夜雨
* @since 2022-04-03 15:37
*/
public interface TaskMapper {
List<Task> listAll4Public();
}
@@ -1,15 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.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);
}
@@ -1,13 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户设置
*
* @author 夜雨
* @since 2021-08-12 16:36
*/
public interface UserConfigMapper extends BaseMapper<UserConfig, Long> {
}
@@ -1,20 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.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);
}
@@ -1,13 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.UserPrivacy;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户隐私控制
*
* @author 夜雨
* @since 2021-07-27 17:18
*/
public interface UserPrivacyMapper extends BaseMapper<UserPrivacy, Long> {
}
@@ -1,13 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.UserProfile;
import com.imyeyu.spring.mapper.BaseMapper;
/**
* 用户数据
*
* @author 夜雨
* @since 2021-07-27 17:04
*/
public interface UserProfileMapper extends BaseMapper<UserProfile, Long> {
}
@@ -1,17 +0,0 @@
package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.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);
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.spring.service.BaseService;
/**
* 文章服务
*
* @author 夜雨
* @since 2021-02-23 21:33
*/
public interface ArticleService extends BaseService<Article, String> {
}
@@ -1,9 +1,11 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
import com.mongodb.client.gridfs.model.GridFSFile;
import java.io.InputStream;
import java.util.List;
@@ -11,28 +13,32 @@ import java.util.List;
/**
* 附件服务
*
* <p>删除和销毁都为数据库软删除,但删除不删除 MongoDB 文件,而销毁则删除 MongoDB 文件
*
* @author 夜雨
* @since 2023-08-15 10:21
*/
public interface AttachmentService extends BaseService<Attachment, Long> {
public interface AttachmentService extends BaseService<Attachment, String> {
Attachment createMedia(Attachment attachment);
/**
* 按需获取或创建缩略图附件。
* <p>当附件为图片或视频时,若指定尺寸的缩略图已存在则直接返回缓存,
* 否则生成并持久化后返回。不支持缩略图的附件类型返回 {@code null}。</p>
*
* @param source 源附件(必须已持久化,mimeType 已填充)
* @param requestWidth 请求宽度,{@code null} 表示未指定
* @param requestHeight 请求高度,{@code null} 表示未指定
* @return 缩略图附件,不支持时返回 {@code null}
*/
Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight);
void deleteMedia(Long thumbId) throws TimiException;
Attachment getByBizId(Attachment.BizType bizType, String bizId);
Attachment getByBizId(Attachment.BizType bizType, long bizId);
Attachment getByAttachType(Attachment.BizType bizType, String bizId, String attachType);
Attachment getByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType);
Attachment getByMongoId(String MongoId);
Attachment getByMongoId(String mongoId);
InputStream getInputStreamByMongoId(String MongoId);
GridFSFile readByMongoId(String mongoId);
InputStream getInputStreamByMongoId(String mongoId);
byte[] readAllByMongoId(String mongoId);
byte[] readAllByMongoId(String MongoId);
/**
* 根据业务获取所有附件
@@ -41,9 +47,8 @@ public interface AttachmentService extends BaseService<Attachment, Long> {
* @param bizId 业务 ID,可为 null
* @param attachTypes 附件类型,可为 null
* @return 所有附件
* @throws TimiException 服务异常
*/
List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, Enum<?> ...attachTypes);
List<Attachment> listByBizId(Attachment.BizType bizType, String bizId, String... attachTypes);
/**
* 根据业务获取所有附件
@@ -52,13 +57,26 @@ public interface AttachmentService extends BaseService<Attachment, Long> {
* @param bizId 业务 ID,可为 null
* @param attachTypes 附件类型,可为 null
* @return 附件数量
* @throws TimiException 服务异常
*/
long countByBizId(Attachment.BizType bizType, Long bizId, Enum<?> ...attachTypes);
long countByBizId(Attachment.BizType bizType, String bizId, String... attachTypes);
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s);
void deleteByBizId(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
PageResult<Attachment> pageByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypeList, Page<Attachment> page);
List<Attachment> listNeedDestroy();
/**
* 根据业务差分保存附件列表。
*
* <ul>
* <li>请求有、数据库无 → 新增({@link Attachment#getTempFileId()} 必填)</li>
* <li>数据库有、请求无 → 删除</li>
* <li>请求有、数据库有、无 tempFileId → 仅更新元数据</li>
* <li>请求有、数据库有、有 tempFileId → 更新文件与元数据</li>
* </ul>
*
* @param req 差分更新请求
*/
void updateByBiz(BizUpdateReq req);
void deleteByBiz(BizDeleteReq req);
}
@@ -1,13 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.spring.service.BaseService;
/**
* 评论回复服务
*
* @author 夜雨
* @since 2021-08-24 10:33
*/
public interface CommentReplyService extends BaseService<CommentReply, Long> {
}
@@ -1,13 +1,124 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.user.entity.UserBehaviorRecord;
import com.imyeyu.api.modules.common.vo.comment.CommentLayerQueryReq;
import com.imyeyu.api.modules.common.vo.comment.CommentLayerResp;
import com.imyeyu.api.modules.common.vo.comment.CommentNodeResp;
import com.imyeyu.api.modules.common.vo.comment.CommentThreadResp;
import com.imyeyu.api.modules.common.vo.comment.FlatCommentListResp;
import com.imyeyu.api.modules.common.vo.comment.NestedCommentListResp;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.BaseService;
/**
* 评论服务
*
* @author 夜雨
* @since 2021-02-23 21:32
* @since 2026-05-27 17:20
*/
public interface CommentService extends BaseService<Comment, Long> {
public interface CommentService extends BaseService<Comment, String> {
/**
* 点赞评论
*
* @param commentId 评论 ID
* @param userId 用户 ID
*/
void like(String commentId, String userId);
/**
* 不喜欢评论
*
* @param commentId 评论 ID
* @param userId 用户 ID
*/
void unlike(String commentId, String userId);
/**
* 查询当前用户点赞评论记录
*
* @param page 分页参数
* @param userId 用户 ID
* @return 点赞评论记录分页
*/
PageResult<UserBehaviorRecord> listLikedRecord(Page<UserBehaviorRecord> page, String userId);
/**
* 查询扁平模式主评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID
* @return 主评论列表
*/
FlatCommentListResp listFlatRoot(String bizType, String bizId, int offset, String currentUserId);
/**
* 查询层级模式主评论列表
*
* @param bizType 业务类型
* @param bizId 业务 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID
* @return 主评论列表
*/
NestedCommentListResp listNestedRoot(String bizType, String bizId, int offset, String currentUserId);
/**
* 查询用户侧评论线程
*
* @param commentId 评论 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID
* @return 评论线程
*/
CommentThreadResp getUserThread(String commentId, int offset, String currentUserId);
/**
* 查询 SDK 评论层
*
* @param req 查询请求
* @param currentUserId 当前用户 ID
* @return 评论层
*/
CommentLayerResp listSdkLayer(CommentLayerQueryReq req, String currentUserId);
/**
* 查询用户侧评论节点
*
* @param commentId 评论 ID
* @param currentUserId 当前用户 ID
* @return 评论节点
*/
CommentNodeResp getUserNode(String commentId, String currentUserId);
/**
* 查询用户侧直属子评论分页
*
* @param commentId 评论 ID
* @param offset 起始偏移量
* @param currentUserId 当前用户 ID
* @return 子评论分页
*/
CommentLayerResp getUserChildren(String commentId, int offset, String currentUserId);
/**
* 查询评论树详情
*
* @param id 评论 ID
* @param currentUserId 当前用户 ID
* @return 评论树详情
*/
Comment detailTree(String id, String currentUserId);
/**
* 删除当前用户自己的评论
*
* @param id 评论 ID
* @param userId 用户 ID
*/
void deleteOwned(String id, String userId);
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.DataDictOption;
import com.imyeyu.spring.service.BaseService;
/**
* 数据字典选项服务
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface DataDictOptionService extends BaseService<DataDictOption, String> {
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.DataDict;
import com.imyeyu.spring.service.BaseService;
/**
* 数据字典服务
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface DataDictService extends BaseService<DataDict, String> {
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.EmailFilter;
import com.imyeyu.spring.service.BaseService;
/**
* 邮件过滤服务
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface EmailFilterService extends BaseService<EmailFilter, String> {
}
@@ -1,52 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.entity.EmailQueue;
import com.imyeyu.api.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();
}
@@ -1,14 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.vo.FeedbackRequest;
/**
* 反馈服务
*
* @author 夜雨
* @since 2021-11-16 22:27
*/
public interface FeedbackService {
void create(FeedbackRequest request);
}
@@ -1,12 +1,11 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Icon;
import com.imyeyu.api.modules.common.vo.icon.AllResponse;
import com.imyeyu.api.modules.common.vo.icon.NamePage;
import com.imyeyu.api.modules.common.vo.icon.UnicodePage;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.PageableService;
import java.util.List;
import java.util.Map;
/**
* 图标服务
*
@@ -15,9 +14,21 @@ import com.imyeyu.spring.service.PageableService;
*/
public interface IconService extends PageableService<Icon> {
AllResponse listAll(Long latest);
/**
* 查询全部图标名称
*
* @return 图标名称列表
*/
List<String> listName();
PageResult<Icon> pageByName(NamePage page);
Map<String, String> mapSvg();
PageResult<Icon> pageByUnicode(UnicodePage page);
Map<String, String> mapUnicode();
/**
* 导出 TTF 字体文件
*
* @return TTF 字体文件字节
*/
byte[] exportTtf();
}
@@ -1,8 +1,7 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.java.bean.Language;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.spring.service.UpdatableService;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
@@ -10,15 +9,16 @@ import java.util.List;
* @author 夜雨
* @since 2023-10-25 10:47
*/
public interface MultilingualService extends UpdatableService<Multilingual> {
public interface MultilingualService extends BaseService<Multilingual, String> {
Long create(String key, String zhCN);
default String create(String zhCN) {
Multilingual multilingual = new Multilingual();
multilingual.setZhCN(zhCN);
create(multilingual);
return multilingual.getId();
}
Long createIfNotExist(String key, String zhCN);
String createIfDifferent(String id, String zhCN);
String get(Language.Enum language, Long id);
String getByKey(Language.Enum language, String key);
List<Multilingual> listByNotTranslate();
List<Multilingual> listByIdList(List<String> idList);
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.spring.service.BaseService;
/**
* 通知详情服务
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface NotifyDetailService extends BaseService<NotifyDetail, String> {
}
@@ -0,0 +1,13 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.spring.service.BaseService;
/**
* 通知服务
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
public interface NotifyService extends BaseService<Notify, String> {
}
@@ -1,80 +1,47 @@
package com.imyeyu.api.modules.common.service;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.bean.attachment.SettingSupport;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.service.UpdatableService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.service.BaseService;
import com.imyeyu.utils.Text;
import java.util.Arrays;
import java.util.List;
/**
* 系统配置服务
* 配置服务
*
* @author 夜雨
* @since 2021-07-20 22:06
*/
public interface SettingService extends UpdatableService<Setting> {
public interface SettingService extends BaseService<Setting, String> {
default List<Setting> listByKeys(SettingKey... keys) {
return listByKeys(Arrays.asList(keys));
Setting getByCode(String bizType, String bizId, String code);
default Setting getSystem(Enum<?> bizType) {
return getByCode(Text.bizClassName(bizType.getClass()), TimiJava.ZERO_UUID, bizType.toString());
}
List<Setting> listByKeys(List<SettingKey> keyList);
List<Setting> listByBizId(String bizType, String bizId);
Setting getByKey(SettingKey key);
default List<Setting> listBySettingSupport(SettingSupport settingSupport) {
String bizType = settingSupport.getClass().getSimpleName().toUpperCase();
return listByBizId(bizType, settingSupport.getId());
}
/**
* 获取指定类型配置值字符串
* 差分更新设置
*
* @param key 键
* @return 配置值
* @param bizType 业务类型
* @param bizId 业务 ID,可为空
* @param list 设置列表
*/
String getAsString(SettingKey key);
void updateByBizId(String bizType, String bizId, List<Setting> list);
int getAsInt(SettingKey key);
default void updateBySettingSupport(SettingSupport settingSupport) {
String bizType = settingSupport.getClass().getSimpleName().toUpperCase();
updateByBizId(bizType, settingSupport.getId(), settingSupport.getSettingList());
}
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);
JsonNode getAsJsonNode(SettingKey key);
ObjectNode getAsJsonObject(SettingKey key);
ArrayNode getAsArrayNode(SettingKey key);
<T> T fromJson(SettingKey key, Class<T> clazz);
<T> T fromJson(SettingKey key, TypeReference<T> typeReference);
<T> T fromYaml(SettingKey key, Class<T> clazz);
List<Setting> listAll();
void clearCache(SettingKey key);
void flushCache();
void deleteByBizId(String bizType, String bizId);
}
@@ -1,17 +1,11 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.DeletableService;
import java.util.List;
import com.imyeyu.spring.service.BaseService;
/**
* @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;
public interface TagService extends BaseService<Tag, String> {
}
@@ -1,23 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.entity.Task;
import java.util.List;
/**
* 任务服务
*
* @author 夜雨
* @since 2022-04-03 15:36
*/
public interface TaskService {
/**
* 查询所有公开任务
*
* @return 公开任务列表
* @throws TimiException 服务异常
*/
List<Task> listAll4Public();
}

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