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
@@ -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();
}
@@ -1,6 +1,7 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.vo.TempFileResponse;
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
import com.imyeyu.api.modules.common.vo.attach.TempFileSaveReq;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
@@ -11,5 +12,14 @@ import java.util.List;
*/
public interface TempFileService {
List<TempFileResponse> store(List<MultipartFile> files, Long ttl);
/**
* 存储临时文件
*
* @param files 文件列表
* @param ttl 存活时间(毫秒)
* @return 临时文件响应列表
*/
List<TempFileResp> create(List<MultipartFile> files, Long ttl);
void save(TempFileSaveReq request);
}
@@ -1,12 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Template;
/**
* @author 夜雨
* @since 2023-09-22 16:38
*/
public interface TemplateService {
Template get(Template.BizType bizType, String bizCode);
}
@@ -1,15 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.UpdatableService;
/**
* 用户设置服务
*
* @author 夜雨
* @since 2021-08-12 16:23
*/
public interface UserConfigService extends GettableService<UserConfig, Long>, CreatableService<UserConfig>, UpdatableService<UserConfig> {
}
@@ -1,15 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.UserPrivacy;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.UpdatableService;
/**
* 用户隐私控制服务
*
* @author 夜雨
* @since 2021-07-27 17:18
*/
public interface UserPrivacyService extends GettableService<UserPrivacy, Long>, CreatableService<UserPrivacy>, UpdatableService<UserPrivacy> {
}
@@ -1,18 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.UserProfile;
import com.imyeyu.api.modules.common.vo.user.UserRequest;
import com.imyeyu.spring.service.CreatableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.UpdatableService;
/**
* 用户数据服务
*
* @author 夜雨
* @since 2021-07-27 17:05
*/
public interface UserProfileService extends GettableService<UserProfile, Long>, CreatableService<UserProfile>, UpdatableService<UserProfile> {
void update(UserRequest request);
}
@@ -1,149 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.entity.User;
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.UserView;
import com.imyeyu.spring.service.GettableService;
/**
* 用户管理服务
* <p>操作任何用户数据前应保证调用 find,以确保用户存在且没有注销(不需要判断,不存在时 find 会抛异常),如果不需要返回数据,可以调用
* 一次 exist(),也不需要判断
* <p>经过令牌验证的操作也不需要检验用户是否存在,不存在的用户无法登录也无法生成正确的令牌
*
* @author 夜雨
* @since 2021-02-23 21:32
*/
public interface UserService extends GettableService<User, Long> {
/**
* 注册用户,传入参 User.password 是明文
*
* @param request 注册数据
* @return 登录返回数据
* @throws TimiException 服务异常
*/
LoginResponse register(RegisterRequest request);
/**
* 执行登录
*
* @param request 登录数据
* @return 登录返回数据
* @throws TimiException 服务异常
*/
LoginResponse login(LoginRequest request);
/**
* 用户视图,未经过权限过滤和隐私过滤数据
*
* @param userId
* @return
* @throws TimiException
*/
UserView view(Long userId);
/**
* 密码验证
*
* @param digestPassword 摘要密码
* @param password 明文密码
* @return true 为无效密码
* @throws TimiException 服务异常
*/
boolean isInvalidPassword(String digestPassword, String password);
/**
* 令牌登录
*
* @return 登录结果
*/
LoginResponse login4Token();
/** 退出登录 */
void logout();
/**
* 查找用户
*
* @param user UID、邮箱和用户名
* @return 用户
* @throws TimiException 服务异常
*/
User get(String user);
boolean isLogged();
User getLoginUser();
/**
* 根据用户名查找
*
* @param name 用户名
* @return 账号数据
* @throws TimiException 服务异常
*/
User getByName(String name);
/**
* 根据邮箱查找
*
* @param email 邮箱
* @return 账号数据
* @throws TimiException 服务异常
*/
User getByEmail(String email);
/**
* 发送邮箱验证邮件
*
* @throws TimiException 服务异常
*/
void sendEmailVerify();
/**
* 邮箱验证回调
*
* @param key 邮件密钥(非登录令牌)
* @throws TimiException 服务异常
*/
void emailVerifyCallback(String key);
/**
* 修改密码
*
* @param oldValue 旧密码
* @param newValue 新密码
* @throws TimiException 服务异常
*/
void updatePassword(String oldValue, String newValue);
/**
* 发送找回密码验证邮件
*
* @param user UID、用户名或邮箱
* @throws TimiException 服务异常
*/
void sendPasswordForgetVerify(String user);
/**
* 重置密码(需要密钥,由找回密码 sendPasswordForgetVerify 接口通过发送邮件分发)
*
* @param key 密钥
* @param password 明文密码
* @throws TimiException 服务异常
*/
void resetPasswordByKey(String key, String password);
/**
* 注销
*
* @param password 密码校验
* @throws TimiException 服务异常
*/
void cancel(String password);
}
@@ -1,14 +0,0 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Version;
/**
* 版本管理服务
*
* @author 夜雨
* @since 2021-06-10 16:06
*/
public interface VersionService {
Version getByName(String name);
}
@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.Article;
import com.imyeyu.api.modules.common.mapper.ArticleMapper;
import com.imyeyu.api.modules.common.service.ArticleService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 文章服务实现
*
* @author 夜雨
* @since 2021-02-17 17:48
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ArticleServiceImplement extends AbstractEntityService<Article, String> implements ArticleService {
private final ArticleMapper mapper;
@Override
protected BaseMapper<Article, String> mapper() {
return mapper;
}
}
@@ -3,19 +3,24 @@ package com.imyeyu.api.modules.common.service.implement;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.bean.Metadata;
import com.imyeyu.api.modules.common.bean.attachment.MediaAttach;
import com.imyeyu.api.modules.common.bean.attachment.Metadata;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.mapper.AttachmentMapper;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.vo.attach.BizDeleteReq;
import com.imyeyu.api.modules.common.vo.attach.BizUpdateReq;
import com.imyeyu.api.util.JavaCV;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArg;
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.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
@@ -38,9 +43,11 @@ import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* @author 夜雨
@@ -49,9 +56,7 @@ import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class AttachmentServiceImplement extends AbstractEntityService<Attachment, Long> implements AttachmentService {
private final SettingService settingService;
public class AttachmentServiceImplement extends AbstractEntityService<Attachment, String> implements AttachmentService {
private final AttachmentMapper mapper;
@@ -60,21 +65,59 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
private final GridFsTemplate gridFsTemplate;
@Override
protected BaseMapper<Attachment, Long> mapper() {
protected BaseMapper<Attachment, String> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
mapper.selectThumbsBySourceId(id).forEach(thumb -> super.delete(thumb.getId()));
super.delete(id);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroy(String id) {
CallbackArg<Attachment> doDestroy = attach -> {
try {
if (attach.getMongoId() != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attach.getMongoId())));
}
if (!attach.isDeleted()) {
attach.setDeletedAt(Time.now());
}
attach.setIsDestroyed(true);
attach.setDestroyAt(Time.now());
mapper.update(attach);
if (Ref.toType(MediaAttach.Type.class, attach.getAttachType()) == MediaAttach.Type.THUMB) {
// 缩略图记录物理删除
mapper.destroy(attach.getId());
}
} catch (Exception e) {
log.error("delete mongo file error, id={}", attach.getId(), e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO delete mongo file error");
}
};
mapper.selectThumbsBySourceId(id).forEach(doDestroy::handler);
doDestroy.handler(mapper.selectRaw(id));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(Attachment attachment) {
TimiException.required(attachment.getBizType(), "not found attachment.bizType");
TimiException.required(attachment.getBizType(), "not found String");
TimiException.required(attachment.getName(), "not found attachment.name");
String mongoId = null;
try {
if (attachment.getFile() != null) {
attachment.setInputStream(attachment.getFile().getInputStream());
}
InputStream is = attachment.getInputStream();
TimiException.required(is, "not found attachment.inputStream");
StringBuilder mongoName = new StringBuilder(attachment.getBizType().toString());
StringBuilder mongoName = new StringBuilder(attachment.getBizType().name());
if (TimiJava.isNotEmpty(attachment.getAttachType())) {
mongoName.append("_").append(attachment.getAttachType().toUpperCase()).append("_");
}
@@ -91,108 +134,154 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
attachment.setMetadata(TimiJava.defaultIfNull(attachment.getMetadata(), jackson.createObjectNode()));
ObjectNode metadata = (ObjectNode) attachment.getMetadata();
metadata.put("width", image.getWidth());
metadata.put("height", image.getHeight());
if (attachment.getMetadata() instanceof ObjectNode obj) {
obj.put("width", image.getWidth());
obj.put("height", image.getHeight());
}
}
mapper.insert(attachment);
} catch (Exception e) {
if (mongoId != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId)));
}
log.error("create attachment error", e);
log.error("create error", e);
throw new TimiException(TimiCode.ARG_BAD).msgKey("TODO read attachment input stream error");
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroy(Long id) {
public void update(Attachment attachment) {
String newMongoId = null;
try {
Attachment attach = mapper.selectRaw(id);
GridFSFile file = readByMongoId(attach.getMongoId());
if (file != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attach.getMongoId())));
if (attachment.getFile() != null) {
attachment.setInputStream(attachment.getFile().getInputStream());
}
if (!attach.isDeleted()) {
attach.setDeletedAt(Time.now());
InputStream stream = attachment.getInputStream();
if (stream == null) {
// 无新文件,仅更新元数据字段
super.update(attachment);
return;
}
attach.setIsDestroyed(true);
attach.setDestroyAt(Time.now());
mapper.update(attach);
} catch (Exception e) {
log.error("delete mongo file error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO delete mongo file error");
}
}
// 有新文件时,先查旧记录获取旧 mongoId 和兜底字段
Attachment old = mapper.selectRaw(attachment.getId());
// 构建存储名,优先取调用方设置的字段,否则沿用旧值
Attachment.BizType bizType = TimiJava.defaultIfNull(attachment.getBizType(), old.getBizType());
String attachType = TimiJava.defaultIfEmpty(attachment.getAttachType(), old.getAttachType());
String name = TimiJava.defaultIfEmpty(attachment.getName(), old.getName());
StringBuilder mongoName = new StringBuilder(bizType.name());
if (TimiJava.isNotEmpty(attachType)) {
mongoName.append("_").append(attachType.toUpperCase()).append("_");
}
mongoName.append(name);
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public Attachment createMedia(Attachment attachment) throws TimiException {
try {
// 储存源文件
attachment.setAttachTypeValue(MediaAttach.Type.SOURCE);
create(attachment);
// 生成缩略图
InputStream mimeStream = attachment.openInputStream();
String mimeType = new Tika().detect(mimeStream);
InputStream sourceStream = attachment.openInputStream();
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
switch (mimeType) {
case "image/png", "image/bmp", "image/jpeg" -> Thumbnails.of(sourceStream).width(256).keepAspectRatio(true).toOutputStream(thumbStream);
case "video/mp4", "video/quicktime" -> {
log.info("capturing thumbnail: {}", attachment.getName());
long start = Time.now();
File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID().toString(), attachment.getName()));
try {
IO.toFile(tempFile, sourceStream);
ByteArrayOutputStream baos = JavaCV.captureThumbnail(IO.getInputStream(tempFile), 2);
Thumbnails.of(IO.toInputStream(baos)).width(256).keepAspectRatio(true).toOutputStream(thumbStream);
log.info("captured thumbnail: {} at {} ms", attachment.getName(), Time.now() - start);
} finally {
IO.destroy(tempFile);
}
// 存储新文件
newMongoId = gridFsTemplate.store(stream, mongoName.toString()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(newMongoId)));
attachment.setMongoId(newMongoId);
attachment.setSize(gridFSFile.getLength());
attachment.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setMimeType(new Tika().detect(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
attachment.setMetadata(TimiJava.defaultIfNull(attachment.getMetadata(), jackson.createObjectNode()));
if (attachment.getMetadata() instanceof ObjectNode obj) {
obj.put("width", image.getWidth());
obj.put("height", image.getHeight());
}
}
Metadata.ThumbImage thumbMetadata = new Metadata.ThumbImage();
thumbMetadata.setSourceId(attachment.getId());
thumbMetadata.setSourceMongoId(attachment.getMongoId());
thumbMetadata.setSourceMimeType(mimeType);
Attachment thumbAttach = new Attachment();
thumbAttach.setName(Network.simpleURIFileName(attachment.getName()) + ".png");
thumbAttach.setBizType(attachment.getBizType());
thumbAttach.setBizId(attachment.getBizId());
thumbAttach.setAttachTypeValue(MediaAttach.Type.THUMB);
thumbAttach.setMetadata(jackson.valueToTree(thumbMetadata));
thumbAttach.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray()));
create(thumbAttach);
return get(thumbAttach.getId());
// 更新数据库后删除旧 GridFS 文件
super.update(attachment);
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(old.getMongoId())));
} catch (Exception e) {
log.error("create media attachment error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO create media attachment error");
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
if (newMongoId != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(newMongoId)));
}
log.error("update error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO update file error");
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteMedia(Long thumbId) throws TimiException {
Attachment attachment = get(thumbId);
delete(attachment.getId());
Metadata.ThumbImage thumbMetadata = jackson.convertValue(attachment.getMetadata(), Metadata.ThumbImage.class);
delete(thumbMetadata.getSourceId());
public Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight) {
TimiException.requiredTrue(requestWidth != null || requestHeight != null, "required width or height");
String mimeType = source.getMimeType();
boolean isImage = mimeType != null && Set.of("image/png", "image/bmp", "image/jpeg").contains(mimeType);
boolean isVideo = mimeType != null && Set.of("video/mp4", "video/quicktime").contains(mimeType);
if (!isImage && !isVideo) {
return null;
}
// 命中缓存直接返回
Attachment cached = mapper.selectThumb(source.getBizType(), source.getId(), requestWidth, requestHeight);
if (cached != null) {
return cached;
}
try {
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
CallbackArg<InputStream> applyThumbSize = inputStream -> {
try {
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
if (requestWidth != null && requestHeight != null) {
builder.size(requestWidth, requestHeight).keepAspectRatio(false).toOutputStream(thumbStream);
} else if (requestWidth != null) {
builder.width(requestWidth).keepAspectRatio(true).toOutputStream(thumbStream);
} else {
builder.height(requestHeight).keepAspectRatio(true).toOutputStream(thumbStream);
}
} catch (IOException e) {
log.error("applyThumbSize error for fetchThumb", e);
throw new RuntimeException(e);
}
};
if (isImage) {
applyThumbSize.handler(source.openInputStream());
} else {
String name = source.getName();
File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID(), name));
try {
IO.toFile(tempFile, source.openInputStream());
ByteArrayOutputStream baos = JavaCV.captureThumbnail(IO.getInputStream(tempFile), 2);
applyThumbSize.handler(IO.toInputStream(baos));
} finally {
IO.destroy(tempFile);
}
}
Metadata.ThumbImage thumbMeta = new Metadata.ThumbImage();
thumbMeta.setSourceId(source.getId());
thumbMeta.setSourceMongoId(source.getMongoId());
thumbMeta.setSourceMimeType(mimeType);
thumbMeta.setRequestWidth(requestWidth);
thumbMeta.setRequestHeight(requestHeight);
Attachment thumb = new Attachment();
thumb.setName(Network.simpleURIFileName(source.getName()) + ".png");
thumb.setBizType(source.getBizType());
thumb.setBizId(source.getId());
thumb.setAttachType(MediaAttach.Type.THUMB.toString());
thumb.setMetadata(jackson.valueToTree(thumbMeta));
thumb.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray()));
// 缩略图 7 天后过期,额外 1 天宽限供后台任务销毁
long expireAt = Time.now() + 7 * Time.D;
thumb.setDeletedAt(expireAt);
thumb.setDestroyAt(expireAt + Time.D);
create(thumb);
return thumb;
} catch (Exception e) {
log.error("create thumbnail error", e);
throw new TimiException(TimiCode.ERROR).msgKey("exception.attachment.create.error");
}
}
@Override
public Attachment getByBizId(Attachment.BizType bizType, long bizId) {
public Attachment getByBizId(Attachment.BizType bizType, String bizId) {
return mapper.selectByBizId(bizType, bizId);
}
@Override
public Attachment getByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType) {
public Attachment getByAttachType(Attachment.BizType bizType, String bizId, String attachType) {
return mapper.selectByAttachType(bizType, bizId, attachType);
}
@@ -201,59 +290,134 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
return mapper.selectByMongoId(mongoId);
}
@Override
public GridFSFile readByMongoId(String mongoId) {
Attachment attach = mapper.selectByMongoId(mongoId);
TimiException.required(attach, "not found attachment: %s".formatted(mongoId));
return gridFsTemplate.findOne(new Query(Criteria.where("_id").is(attach.getMongoId())));
}
@Override
public InputStream getInputStreamByMongoId(String mongoId) {
return gridFSBucket.openDownloadStream(readByMongoId(mongoId).getObjectId());
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
return gridFSBucket.openDownloadStream(gridFSFile.getObjectId());
}
@Override
public byte[] readAllByMongoId(String mongoId) {
try {
return IO.toBytes(getByMongoId(mongoId).openInputStream());
return IO.toBytes(getInputStreamByMongoId(mongoId));
} catch (IOException e) {
throw new TimiException(TimiCode.ERROR, "TODO 读取失败");
throw new TimiException(TimiCode.ERROR, "read error");
}
}
@Override
public List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, Enum<?>... attachTypes) {
return mapper.listByBizId(bizType, bizId, Arrays.asList(attachTypes), null);
public List<Attachment> listByBizId(Attachment.BizType bizType, String bizId, String... attachTypes) {
return mapper.selectByBizId(bizType, bizId, List.of(attachTypes), null);
}
@Override
public long countByBizId(Attachment.BizType bizType, Long bizId, Enum<?>... attachTypes) {
return mapper.countByBizId(bizType, bizId, Arrays.asList(attachTypes));
public long countByBizId(Attachment.BizType bizType, String bizId, String... attachTypes) {
return mapper.countByBizId(bizType, bizId, List.of(attachTypes));
}
@Override
public List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s) throws TimiException {
return mapper.listByMd5s(bizType, bizId, attachTypeList, md5s);
}
@Override
public void deleteByBizId(Attachment.BizType bizType, long bizId, Enum<?>... attachTypes) {
Attachment example = new Attachment();
example.setBizType(bizType);
example.setBizId(bizId);
if (TimiJava.isEmpty(attachTypes)) {
mapper.deleteAllByExample(example);
} else {
for (int i = 0; i < attachTypes.length; i++) {
example.setAttachTypeValue(attachTypes[i]);
mapper.deleteAllByExample(example);
}
}
public PageResult<Attachment> pageByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypeList, Page<Attachment> page) throws TimiException {
PageResult<Attachment> result = new PageResult<>();
result.setList(mapper.selectByBizId(bizType, bizId, attachTypeList, page));
result.setTotal(mapper.countByBizId(bizType, bizId, attachTypeList));
return result;
}
@Override
public List<Attachment> listNeedDestroy() {
return mapper.selectNeedDestroy();
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void updateByBiz(BizUpdateReq req) {
Attachment.BizType bizType = req.getBizType();
String bizId = req.getBizId();
List<Attachment> items = req.getItems();
// 数据库现有附件
List<Attachment> dbList = mapper.selectAllByBizId(bizType, bizId);
Set<String> dbIds = dbList.stream().map(Attachment::getId).collect(Collectors.toSet());
// 请求附件 ID
Set<String> reqIds = items.stream().map(Attachment::getId).filter(Objects::nonNull).collect(Collectors.toSet());
// 数据库有、请求无:删除
for (Attachment attach : dbList) {
if (!reqIds.contains(attach.getId())) {
delete(attach.getId());
}
}
for (Attachment item : items) {
boolean isNew = item.getId() == null || !dbIds.contains(item.getId());
String tempFileId = item.getTempFileId();
if (isNew) {
// 请求有、数据库无:将临时文件记录直接转存为永久附件
if (TimiJava.isEmpty(tempFileId)) {
throw new TimiException(TimiCode.ERROR).msgKey("exception.attachment.file.required");
}
Attachment tempFile = mapper.selectRaw(tempFileId);
tempFile.setBizType(bizType);
tempFile.setBizId(bizId);
tempFile.setAttachType(item.getAttachType());
if (TimiJava.isNotEmpty(item.getName())) {
tempFile.setName(item.getName());
}
if (item.getMetadata() != null) {
tempFile.setMetadata(item.getMetadata());
}
// 清除过期标记,使其成为永久附件
tempFile.setDeletedAt(null);
tempFile.setDestroyAt(null);
mapper.update(tempFile);
} else if (TimiJava.isNotEmpty(tempFileId)) {
// 请求有、数据库有、有 tempFileId → 更新文件与元数据
// 将临时文件的 GridFS 存储转移至当前永久记录,废弃临时记录
Attachment current = mapper.selectRaw(item.getId());
Attachment tempFile = mapper.selectRaw(tempFileId);
// 删除旧 GridFS 文件
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(current.getMongoId())));
// 将存储信息从临时记录迁移到永久记录
current.setMongoId(tempFile.getMongoId());
current.setSize(tempFile.getSize());
current.setMd5(tempFile.getMd5());
current.setMimeType(tempFile.getMimeType());
current.setName(TimiJava.defaultIfEmpty(item.getName(), tempFile.getName()));
if (item.getAttachType() != null) {
current.setAttachType(item.getAttachType());
}
if (item.getMetadata() != null) {
current.setMetadata(item.getMetadata());
}
mapper.update(current);
// 废弃临时文件记录:清空 mongoId 防止后台任务误删已转移的 GridFS 文件
tempFile.setMongoId(null);
tempFile.setIsDestroyed(true);
tempFile.setDestroyAt(Time.now());
mapper.update(tempFile);
} else {
// 请求有、数据库有、无 tempFileId → 仅更新元数据
Attachment current = mapper.selectRaw(item.getId());
if (item.getAttachType() != null) {
current.setAttachType(item.getAttachType());
}
if (TimiJava.isNotEmpty(item.getName())) {
current.setName(item.getName());
}
if (item.getMetadata() != null) {
current.setMetadata(item.getMetadata());
}
mapper.update(current);
}
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteByBiz(BizDeleteReq req) {
List<Attachment> attachmentList = mapper.selectAllByBizId(req.getBizType(), req.getBizId());
for (Attachment attachment : attachmentList) {
delete(attachment.getId());
}
}
}
@@ -30,7 +30,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class ClipboardServiceImplement implements ClipboardService {
/** Redis key 前缀 */
private static final String KEY_PREFIX = "clipboard:";
private static final String KEY_PREFIX = "CLIPBOARD:";
/** 剪切板内容最大长度 */
private static final int MAX_CONTENT_SIZE = 10 * 1024 * 1024;
@@ -1,172 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.TimiServerAPI;
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.bean.CommentSupport;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.entity.EmailQueue;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.api.modules.common.mapper.CommentReplyMapper;
import com.imyeyu.api.modules.common.service.CommentReplyService;
import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.api.modules.common.service.EmailQueueService;
import com.imyeyu.api.modules.common.service.UserConfigService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
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.service.GettableService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.UUID;
/**
* 评论回复服务实现
*
* @author 夜雨
* @since 2021-08-24 10:34
*/
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class CommentReplyServiceImplement extends AbstractEntityService<CommentReply, Long> implements CommentReplyService {
private final UserService userService;
private final CommentService commentService;
private final UserConfigService userConfigService;
private final EmailQueueService emailQueueService;
private final CommentRemindQueueService commentRemindQueueService;
private final CommentReplyMapper mapper;
@Override
protected BaseMapper<CommentReply, Long> mapper() {
return mapper;
}
@Override
public PageResult<CommentReply> page(Page<CommentReply> page) {
PageResult<CommentReply> result = super.page(page);
for (CommentReply reply : result.getList()) {
Comment comment = commentService.get(reply.getCommentId());
if (TimiJava.isNotEmpty(comment.getUserId())) {
comment.setUser(userService.view(comment.getUserId()));
}
reply.setComment(comment);
if (TimiJava.isNotEmpty(reply.getSenderId())) {
reply.setSender(userService.view(reply.getSenderId()));
}
if (TimiJava.isNotEmpty(reply.getReceiverId())) {
reply.setReceiver(userService.view(reply.getReceiverId()));
}
}
return result;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(CommentReply commentReply) {
Comment comment = commentService.get(commentReply.getCommentId());
Class<? extends GettableService<? extends CommentSupport, Long>> serviceClass = comment.getBizType().getServiceClass();
GettableService<? extends CommentSupport, Long> service = TimiServerAPI.applicationContext.getBean(serviceClass);
CommentSupport commentSupport = service.get(comment.getBizId());
if (commentSupport.canNotComment()) {
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("评论已关闭");
}
CommentReply dbReply = new CommentReply();
dbReply.setCommentId(comment.getId());
// 发送者
String token = TimiSpring.getToken();
if (TimiJava.isNotEmpty(token)) {
User senderUser = userService.getLoginUser();
if (senderUser.isBanning() || senderUser.isMuting()) {
throw new TimiException(TimiCode.RESULT_BAN).msgKey("comment.banded");
}
dbReply.setSenderId(senderUser.getId());
dbReply.setSenderNick(null);
} else {
if (TimiJava.isEmpty(commentReply.getSenderNick())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("comment.nick.empty");
}
dbReply.setSenderNick(commentReply.getSenderNick());
}
// 被回复
dbReply.setReplyId(commentReply.getReplyId());
if (commentReply.getReplyId() == null) {
// 回复主评论
dbReply.setReceiverId(comment.getUserId());
if (TimiJava.isEmpty(comment.getUserId())) {
dbReply.setReceiverNick(comment.getNick());
}
} else {
// 回复其他回复
CommentReply targetReply = get(commentReply.getReplyId());
dbReply.setReceiverId(targetReply.getSenderId());
if (TimiJava.isEmpty(targetReply.getSenderId())) {
dbReply.setReceiverNick(targetReply.getSenderNick());
}
}
// 创建回复
dbReply.setContent(commentReply.getContent());
dbReply.setIp(TimiSpring.getRequestIP());
super.create(dbReply);
// 被回复为注册用户时处理通知和邮件
{
if (TimiJava.isEmpty(dbReply.getReceiverId())) {
return;
}
if (TimiJava.isNotEmpty(dbReply.getSenderId()) && dbReply.getSenderId().equals(dbReply.getReceiverId())) {
return;
}
// 被回复账号
User receiverUser = userService.get(dbReply.getReceiverId());
UserConfig userConfig = userConfigService.get(dbReply.getReceiverId());
if (!receiverUser.emailVerified() || !userConfig.isEmailReplyRemind()) {
return;
}
// 添加提醒队列
CommentRemindQueue remindQueue = new CommentRemindQueue();
remindQueue.setUUID(UUID.randomUUID().toString());
remindQueue.setUserId(receiverUser.getId());
remindQueue.setReplyId(dbReply.getId());
commentRemindQueueService.create(remindQueue);
// 邮件队列
EmailQueue emailQueue = emailQueueService.get(EmailQueue.BizType.REPLY_REMINAD, receiverUser.getId());
if (emailQueue == null) {
emailQueue = new EmailQueue();
emailQueue.setBizType(EmailQueue.BizType.REPLY_REMINAD);
emailQueue.setBizId(receiverUser.getId());
long H10 = Time.H * 10;
if (Time.now() < Time.today() + H10) {
emailQueue.setSendAt(Time.today() + H10);
} else {
emailQueue.setSendAt(Time.tomorrow() + H10);
}
emailQueueService.create(emailQueue);
}
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(Long crId) {
super.delete(crId);
commentRemindQueueService.destroyByReplyId(crId);
}
}
@@ -1,135 +1,628 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.blog.entity.CommentRemindQueue;
import com.imyeyu.api.modules.common.bean.CommentSupport;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.user.entity.User;
import com.imyeyu.api.modules.user.entity.UserBehaviorRecord;
import com.imyeyu.api.modules.common.mapper.CommentMapper;
import com.imyeyu.api.modules.common.mapper.CommentRemindQueueMapper;
import com.imyeyu.api.modules.common.mapper.CommentReplyMapper;
import com.imyeyu.api.modules.user.mapper.UserMapper;
import com.imyeyu.api.modules.common.service.CommentService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.user.service.UserBehaviorRecordService;
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.FlatCommentListItemResp;
import com.imyeyu.api.modules.common.vo.comment.FlatCommentListResp;
import com.imyeyu.api.modules.common.vo.comment.NestedCommentListResp;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
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.service.GettableService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 评论操作服务实现
*
* 评论服务实现
*
* @author 夜雨
* @since 2021-02-23 21:41
* @since 2026-05-27 17:20
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class CommentServiceImplement extends AbstractEntityService<Comment, Long> implements CommentService {
@RequiredArgsConstructor
public class CommentServiceImplement extends AbstractEntityService<Comment, String> implements CommentService {
private final UserService userService;
private static final int USER_ROOT_LIMIT = 20;
private static final int USER_REPLY_LIMIT = 6;
private static final int SDK_LAYER_LIMIT = 100;
private static final String BEHAVIOR_BIZ_TYPE = "USER";
private static final String BEHAVIOR_TARGET_TYPE_COMMENT = "COMMENT";
private static final String BEHAVIOR_ACTION_TYPE_LIKE = "LIKE";
private static final String BEHAVIOR_ACTION_TYPE_UNLIKE = "UNLIKE";
private final UserMapper userMapper;
private final UserBehaviorRecordService userBehaviorRecordService;
private final CommentMapper mapper;
private final CommentReplyMapper replyMapper;
private final CommentRemindQueueMapper remindQueueMapper;
@Override
protected BaseMapper<Comment, Long> mapper() {
protected BaseMapper<Comment, String> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public PageResult<Comment> page(Page<Comment> page) {
PageResult<Comment> result = super.page(page);
for (Comment comment : result.getList()) {
public void create(Comment entity) {
TimiException.required(entity, "not found entity");
if (TimiJava.isEmpty(entity.getParentId())) {
createRoot(entity);
return;
}
createReply(entity, entity.getParentId());
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(Comment comment) {
TimiException.required(comment, "not found comment");
TimiException.required(comment.getId(), "not found comment.id");
Comment dbComment = get(comment.getId());
TimiException.required(dbComment, "not found comment");
dbComment.setContent(TimiJava.defaultIfEmpty(comment.getContent(), dbComment.getContent()));
dbComment.setIp(TimiJava.defaultIfEmpty(comment.getIp(), dbComment.getIp()));
dbComment.setStatus(TimiJava.defaultIfNull(comment.getStatus(), dbComment.getStatus()));
dbComment.setIsTop(TimiJava.defaultIfNull(comment.getIsTop(), dbComment.getIsTop()));
super.update(dbComment);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(String id) {
deleteInternal(id, null);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void like(String commentId, String userId) {
Comment comment = get(commentId);
TimiException.required(comment, "not found comment");
TimiException.required(userId, "not found userId");
userBehaviorRecordService.cancel(BEHAVIOR_BIZ_TYPE, userId, BEHAVIOR_TARGET_TYPE_COMMENT, commentId, BEHAVIOR_ACTION_TYPE_UNLIKE);
boolean created = createBehaviorRecord(comment, userId, BEHAVIOR_ACTION_TYPE_LIKE);
if (created) {
mapper.increaseLikeCount(commentId, 1);
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void unlike(String commentId, String userId) {
Comment comment = get(commentId);
TimiException.required(comment, "not found comment");
TimiException.required(userId, "not found userId");
boolean canceled = userBehaviorRecordService.cancel(BEHAVIOR_BIZ_TYPE, userId, BEHAVIOR_TARGET_TYPE_COMMENT, commentId, BEHAVIOR_ACTION_TYPE_LIKE);
if (canceled) {
mapper.increaseLikeCount(commentId, -1);
}
createBehaviorRecord(comment, userId, BEHAVIOR_ACTION_TYPE_UNLIKE);
}
@Override
public PageResult<UserBehaviorRecord> listLikedRecord(Page<UserBehaviorRecord> page, String userId) {
TimiException.required(userId, "not found userId");
if (page == null) {
page = new Page<>();
}
UserBehaviorRecord equalsExample = page.getEqualsExample();
if (equalsExample == null) {
equalsExample = new UserBehaviorRecord();
page.setEqualsExample(equalsExample);
}
equalsExample.setBizType(BEHAVIOR_BIZ_TYPE);
equalsExample.setUserId(userId);
equalsExample.setTargetType(BEHAVIOR_TARGET_TYPE_COMMENT);
equalsExample.setActionType(BEHAVIOR_ACTION_TYPE_LIKE);
PageResult<UserBehaviorRecord> result = userBehaviorRecordService.page(page);
fillBehaviorRecordComment(result.getList());
return result;
}
@Override
public FlatCommentListResp listFlatRoot(String bizType, String bizId, int offset, String currentUserId) {
TimiException.required(bizType, "not found bizType");
TimiException.required(bizId, "not found bizId");
int validOffset = Math.max(offset, 0);
List<Comment> rootList = mapper.selectRootList(bizType, bizId, validOffset, USER_ROOT_LIMIT);
List<FlatCommentListItemResp> itemList = buildFlatRootPreview(rootList, currentUserId);
FlatCommentListResp resp = new FlatCommentListResp();
resp.setTotal(mapper.countRoot(bizType, bizId));
resp.setList(itemList);
return resp;
}
@Override
public NestedCommentListResp listNestedRoot(String bizType, String bizId, int offset, String currentUserId) {
TimiException.required(bizType, "not found bizType");
TimiException.required(bizId, "not found bizId");
int validOffset = Math.max(offset, 0);
List<Comment> rootList = mapper.selectRootList(bizType, bizId, validOffset, USER_ROOT_LIMIT);
List<CommentNodeResp> itemList = buildNestedRootPreview(rootList, currentUserId);
NestedCommentListResp resp = new NestedCommentListResp();
resp.setTotal(mapper.countRoot(bizType, bizId));
resp.setList(itemList);
return resp;
}
@Override
public CommentThreadResp getUserThread(String commentId, int offset, String currentUserId) {
TimiException.required(commentId, "not found commentId");
int validOffset = Math.max(offset, 0);
Comment current = get(commentId);
TimiException.required(current, "not found comment");
String rootId = TimiJava.defaultIfEmpty(current.getRootId(), current.getId());
Comment root = get(rootId);
TimiException.required(root, "not found root comment");
List<Comment> replyList = mapper.selectReplyListByRootId(rootId, validOffset, USER_ROOT_LIMIT);
fillUser(replyList);
fillLiked(replyList, currentUserId);
fillUser(List.of(root));
fillLiked(List.of(root), currentUserId);
CommentThreadResp resp = new CommentThreadResp();
resp.setRoot(root);
resp.setReplyTotal(mapper.countReplyByRootId(rootId));
resp.setReplyList(replyList);
return resp;
}
@Override
public CommentLayerResp listSdkLayer(CommentLayerQueryReq req, String currentUserId) {
TimiException.required(req, "not found req");
TimiException.required(req.getBizType(), "not found req.bizType");
TimiException.required(req.getBizId(), "not found req.bizId");
int validOffset = Math.max(TimiJava.defaultIfNull(req.getOffset(), 0), 0);
int validLimit = TimiJava.defaultIfNull(req.getLimit(), SDK_LAYER_LIMIT);
validLimit = Math.min(Math.max(validLimit, 1), SDK_LAYER_LIMIT);
List<Comment> commentList = mapper.selectLayerList(req.getBizType(), req.getBizId(), req.getParentId(), validOffset, validLimit);
fillUser(commentList);
fillLiked(commentList, currentUserId);
CommentLayerResp resp = new CommentLayerResp();
resp.setTotal(mapper.countLayer(req.getBizType(), req.getBizId(), req.getParentId()));
resp.setList(commentList);
return resp;
}
@Override
public CommentNodeResp getUserNode(String commentId, String currentUserId) {
TimiException.required(commentId, "not found commentId");
Comment comment = get(commentId);
TimiException.required(comment, "not found comment");
fillUser(List.of(comment));
fillLiked(List.of(comment), currentUserId);
List<Comment> replyList = mapper.selectReplyListByParentId(commentId, 0, USER_REPLY_LIMIT);
fillUser(replyList);
fillLiked(replyList, currentUserId);
CommentNodeResp resp = new CommentNodeResp();
resp.setComment(comment);
resp.setReplyTotal(mapper.countReplyByParentId(commentId));
resp.setReplyList(replyList);
return resp;
}
@Override
public CommentLayerResp getUserChildren(String commentId, int offset, String currentUserId) {
TimiException.required(commentId, "not found commentId");
int validOffset = Math.max(offset, 0);
Comment comment = get(commentId);
TimiException.required(comment, "not found comment");
List<Comment> replyList = mapper.selectReplyListByParentId(commentId, validOffset, USER_ROOT_LIMIT);
fillUser(replyList);
fillLiked(replyList, currentUserId);
CommentLayerResp resp = new CommentLayerResp();
resp.setTotal(mapper.countReplyByParentId(commentId));
resp.setList(replyList);
return resp;
}
@Override
public Comment detailTree(String id, String currentUserId) {
Comment current = get(id);
TimiException.required(current, "not found comment");
String rootId = current.getRootId();
if (TimiJava.isEmpty(rootId)) {
rootId = current.getId();
}
Comment root = get(rootId);
TimiException.required(root, "not found root comment");
List<Comment> replyList = mapper.selectAllReplyListByRootId(rootId);
return attachTree(List.of(root), replyList, currentUserId).getFirst();
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteOwned(String id, String userId) {
TimiException.required(userId, "not found userId");
deleteInternal(id, userId);
}
/**
* 创建主评论
*
* @param comment 评论
*/
private void createRoot(Comment comment) {
TimiException.required(comment.getBizType(), "not found comment.bizType");
TimiException.required(comment.getBizId(), "not found comment.bizId");
requireCommentUser(comment);
TimiException.required(comment.getContent(), "not found comment.content");
comment.setParentId(null);
comment.setRootId(null);
comment.setReplyUserId(null);
comment.setReplyUserNick(null);
comment.setLevel(1);
comment.setReplyCount(TimiJava.defaultIfNull(comment.getReplyCount(), 0));
comment.setLikeCount(TimiJava.defaultIfNull(comment.getLikeCount(), 0));
comment.setStatus(TimiJava.defaultIfNull(comment.getStatus(), Comment.Status.NORMAL));
comment.setIsTop(TimiJava.defaultIfNull(comment.getIsTop(), false));
comment.setLastReplyAt(Time.now());
super.create(comment);
comment.setRootId(comment.getId());
super.update(comment);
}
/**
* 创建回复评论
*
* @param comment 评论
* @param parentId 父评论 ID
*/
private void createReply(Comment comment, String parentId) {
requireCommentUser(comment);
TimiException.required(comment.getContent(), "not found comment.content");
Comment parent = get(parentId);
TimiException.required(parent, "not found parent comment");
String rootId = TimiJava.defaultIfEmpty(parent.getRootId(), parent.getId());
comment.setBizType(parent.getBizType());
comment.setBizId(parent.getBizId());
comment.setParentId(parent.getId());
comment.setRootId(rootId);
comment.setReplyUserId(TimiJava.defaultIfEmpty(comment.getReplyUserId(), parent.getUserId()));
comment.setReplyUserNick(TimiJava.defaultIfEmpty(comment.getReplyUserNick(), parent.getUserNick()));
comment.setLevel(TimiJava.defaultIfNull(parent.getLevel(), 1) + 1);
comment.setReplyCount(TimiJava.defaultIfNull(comment.getReplyCount(), 0));
comment.setLikeCount(TimiJava.defaultIfNull(comment.getLikeCount(), 0));
comment.setStatus(TimiJava.defaultIfNull(comment.getStatus(), Comment.Status.NORMAL));
comment.setIsTop(false);
comment.setLastReplyAt(null);
super.create(comment);
mapper.increaseReplyCount(rootId, 1);
if (!rootId.equals(parent.getId())) {
mapper.increaseReplyCount(parent.getId(), 1);
}
Comment root = new Comment();
root.setId(rootId);
root.setLastReplyAt(Time.now());
super.update(root);
}
/**
* 校验评论用户
*
* @param comment 评论
*/
private void requireCommentUser(Comment comment) {
TimiException.requiredTrue(TimiJava.isNotEmpty(comment.getUserId()) || TimiJava.isNotEmpty(comment.getUserNick()), "not found comment.userId or comment.userNick");
}
/**
* 删除评论及其子树
*
* @param id 评论 ID
* @param requiredUserId 要求归属的用户 ID,为 null 时不校验
*/
private void deleteInternal(String id, String requiredUserId) {
Comment current = get(id);
TimiException.required(current, "not found comment");
if (requiredUserId != null) {
TimiException.requiredTrue(requiredUserId.equals(current.getUserId()), "comment not belong to current user");
}
String rootId = TimiJava.defaultIfEmpty(current.getRootId(), current.getId());
List<Comment> replyList = mapper.selectAllReplyListByRootId(rootId);
Map<String, List<Comment>> childMap = groupByParentId(replyList);
List<Comment> deleteList = new ArrayList<>();
collectDeleteComment(current, childMap, deleteList);
for (Comment comment : deleteList) {
super.delete(comment.getId());
}
if (TimiJava.isEmpty(current.getParentId())) {
return;
}
int deleteReplyCount = deleteList.size();
mapper.increaseReplyCount(rootId, -deleteReplyCount);
if (!rootId.equals(current.getParentId())) {
mapper.increaseReplyCount(current.getParentId(), -1);
}
Comment root = new Comment();
root.setId(rootId);
root.setLastReplyAt(mapper.selectLatestReplyAtByRootId(rootId));
super.update(root);
}
/**
* 组装评论树
*
* @param rootList 根评论列表
* @param replyList 回复列表
* @param currentUserId 当前用户 ID
* @return 评论树列表
*/
private List<Comment> attachTree(List<Comment> rootList, List<Comment> replyList, String currentUserId) {
List<Comment> allCommentList = new ArrayList<>(rootList);
allCommentList.addAll(replyList);
fillUser(allCommentList);
Set<String> likedCommentIdSet = getBehaviorCommentIdSet(currentUserId, allCommentList, BEHAVIOR_ACTION_TYPE_LIKE);
Set<String> unlikedCommentIdSet = getBehaviorCommentIdSet(currentUserId, allCommentList, BEHAVIOR_ACTION_TYPE_UNLIKE);
for (Comment root : rootList) {
root.setReplyList(new ArrayList<>());
root.setLiked(likedCommentIdSet.contains(root.getId()));
root.setUnliked(unlikedCommentIdSet.contains(root.getId()));
}
Map<String, Comment> commentMap = new LinkedHashMap<>();
for (Comment root : rootList) {
commentMap.put(root.getId(), root);
}
replyList.sort(Comparator.comparing(Comment::getCreatedAt, Comparator.nullsLast(Long::compareTo)));
for (Comment reply : replyList) {
reply.setReplyList(new ArrayList<>());
reply.setLiked(likedCommentIdSet.contains(reply.getId()));
reply.setUnliked(unlikedCommentIdSet.contains(reply.getId()));
commentMap.put(reply.getId(), reply);
Comment parent = commentMap.get(reply.getParentId());
if (parent != null) {
parent.getReplyList().add(reply);
}
}
return rootList;
}
/**
* 填充点赞状态
*
* @param commentList 评论列表
* @param currentUserId 当前用户 ID
*/
private void fillLiked(List<Comment> commentList, String currentUserId) {
if (TimiJava.isEmpty(commentList)) {
return;
}
Set<String> likedCommentIdSet = getBehaviorCommentIdSet(currentUserId, commentList, BEHAVIOR_ACTION_TYPE_LIKE);
Set<String> unlikedCommentIdSet = getBehaviorCommentIdSet(currentUserId, commentList, BEHAVIOR_ACTION_TYPE_UNLIKE);
for (Comment comment : commentList) {
comment.setLiked(likedCommentIdSet.contains(comment.getId()));
comment.setUnliked(unlikedCommentIdSet.contains(comment.getId()));
comment.setReplyList(new ArrayList<>());
}
}
/**
* 填充评论用户信息
*
* @param commentList 评论列表
*/
private void fillUser(List<Comment> commentList) {
if (TimiJava.isEmpty(commentList)) {
return;
}
Set<String> userIdSet = new LinkedHashSet<>();
for (Comment comment : commentList) {
if (TimiJava.isNotEmpty(comment.getUserId())) {
comment.setUser(userService.view(comment.getUserId()));
userIdSet.add(comment.getUserId());
}
Page<CommentReply> replyPage = new Page<>();
replyPage.setIndex(0);
replyPage.setSize(6);
{
CommentReply exampleReply = new CommentReply();
exampleReply.setCommentId(comment.getId());
replyPage.setEqualsExample(exampleReply);
}
PageResult<CommentReply> replyResult = replyMapper.selectPageResult(replyPage);
comment.setReplies(replyResult.getList());
comment.setRepliesLength(replyResult.getTotal());
for (CommentReply reply : replyResult.getList()) {
if (TimiJava.isNotEmpty(reply.getSenderId())) {
reply.setSender(userService.view(reply.getSenderId()));
}
if (TimiJava.isNotEmpty(reply.getReceiverId())) {
reply.setReceiver(userService.view(reply.getReceiverId()));
}
if (TimiJava.isNotEmpty(comment.getReplyUserId())) {
userIdSet.add(comment.getReplyUserId());
}
}
if (userIdSet.isEmpty()) {
return;
}
Map<String, User> userMap = new LinkedHashMap<>();
for (User user : userMapper.listByIdList(new ArrayList<>(userIdSet))) {
userMap.put(user.getId(), sanitizeUser(user));
}
for (Comment comment : commentList) {
comment.setUser(userMap.get(comment.getUserId()));
comment.setReplyUser(userMap.get(comment.getReplyUserId()));
}
}
/**
* 清理用户敏感字段
*
* @param user 用户
* @return 用户
*/
private User sanitizeUser(User user) {
if (user == null) {
return null;
}
user.setPassword(null);
user.setLastLoginIP(null);
return user;
}
/**
* 查询当前用户行为评论 ID 集合
*
* @param currentUserId 当前用户 ID
* @param commentList 评论列表
* @param actionType 行为类型
* @return 评论 ID 集合
*/
private Set<String> getBehaviorCommentIdSet(String currentUserId, List<Comment> commentList, String actionType) {
if (TimiJava.isEmpty(commentList)) {
return Set.of();
}
return userBehaviorRecordService.getActiveTargetIdSet(BEHAVIOR_BIZ_TYPE, currentUserId, BEHAVIOR_TARGET_TYPE_COMMENT, actionType, commentList.stream().map(Comment::getId).toList());
}
/**
* 创建评论行为记录
*
* @param comment 评论
* @param userId 用户 ID
* @param actionType 行为类型
* @return true 为新增记录
*/
private boolean createBehaviorRecord(Comment comment, String userId, String actionType) {
UserBehaviorRecord record = new UserBehaviorRecord();
record.setBizType(BEHAVIOR_BIZ_TYPE);
record.setUserId(userId);
record.setTargetType(BEHAVIOR_TARGET_TYPE_COMMENT);
record.setTargetId(comment.getId());
record.setActionType(actionType);
return userBehaviorRecordService.record(record);
}
/**
* 填充行为记录评论信息
*
* @param recordList 行为记录列表
*/
private void fillBehaviorRecordComment(List<UserBehaviorRecord> recordList) {
if (TimiJava.isEmpty(recordList)) {
return;
}
List<String> commentIdList = recordList.stream().map(UserBehaviorRecord::getTargetId).filter(TimiJava::isNotEmpty).distinct().toList();
if (commentIdList.isEmpty()) {
return;
}
List<Comment> commentList = mapper.listByIdList(commentIdList);
fillUser(commentList);
Map<String, Comment> commentMap = new LinkedHashMap<>();
for (Comment comment : commentList) {
commentMap.put(comment.getId(), comment);
}
for (UserBehaviorRecord record : recordList) {
record.setComment(commentMap.get(record.getTargetId()));
}
}
/**
* 构建扁平主评论预览
*
* @param rootList 主评论列表
* @param currentUserId 当前用户 ID
* @return 预览列表
*/
private List<FlatCommentListItemResp> buildFlatRootPreview(List<Comment> rootList, String currentUserId) {
if (rootList == null || rootList.isEmpty()) {
return List.of();
}
fillUser(rootList);
fillLiked(rootList, currentUserId);
List<String> rootIdList = rootList.stream().map(Comment::getId).toList();
List<Comment> previewList = mapper.selectReplyPreviewByRootIds(rootIdList, USER_REPLY_LIMIT);
fillUser(previewList);
fillLiked(previewList, currentUserId);
Map<String, List<Comment>> replyMap = new LinkedHashMap<>();
for (Comment reply : previewList) {
replyMap.computeIfAbsent(reply.getRootId(), key -> new ArrayList<>()).add(reply);
}
List<FlatCommentListItemResp> result = new ArrayList<>();
for (Comment root : rootList) {
FlatCommentListItemResp item = new FlatCommentListItemResp();
item.setRoot(root);
item.setReplyPreviewTotal(mapper.countReplyByRootId(root.getId()));
item.setReplyPreviewList(replyMap.getOrDefault(root.getId(), List.of()));
result.add(item);
}
return result;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(Comment comment) {
Class<? extends GettableService<? extends CommentSupport, Long>> serviceClass = comment.getBizType().getServiceClass();
GettableService<? extends CommentSupport, Long> service = TimiServerAPI.applicationContext.getBean(serviceClass);
CommentSupport commentSupport = service.get(comment.getBizId());
TimiException.requiredTrue(commentSupport.canComment(), "comment.not.can.comment");
// 令牌和账号验证
if (TimiJava.isNotEmpty(TimiSpring.getToken())) {
User commentUser = userService.getLoginUser();
TimiException.requiredTrue(!commentUser.isBanning(), "comment.user.banned");
TimiException.requiredTrue(!commentUser.isMuting(), "comment.user.muting");
comment.setUserId(commentUser.getId());
comment.setNick(null);
} else {
TimiException.required(comment.getNick(), "comment.nick.empty");
/**
* 构建层级主评论预览
*
* @param rootList 主评论列表
* @param currentUserId 当前用户 ID
* @return 预览列表
*/
private List<CommentNodeResp> buildNestedRootPreview(List<Comment> rootList, String currentUserId) {
if (rootList == null || rootList.isEmpty()) {
return List.of();
}
// 内容
TimiException.required(comment.getContent(), "comment.data.empty");
comment.setIp(TimiSpring.getRequestIP());
super.create(comment);
fillUser(rootList);
fillLiked(rootList, currentUserId);
List<CommentNodeResp> result = new ArrayList<>();
for (Comment root : rootList) {
List<Comment> replyList = mapper.selectReplyListByParentId(root.getId(), 0, USER_REPLY_LIMIT);
fillUser(replyList);
fillLiked(replyList, currentUserId);
CommentNodeResp item = new CommentNodeResp();
item.setComment(root);
item.setReplyTotal(mapper.countReplyByParentId(root.getId()));
item.setReplyList(replyList);
result.add(item);
}
return result;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(Long commentId) {
User user = userService.getLoginUser();
Comment comment = get(commentId);
if (!comment.getUserId().equals(user.getId())) {
throw new TimiException(TimiCode.PERMISSION_ERROR).msgKey("token.illegal");
}
{
// 移除被回复者的回复提醒队列
CommentReply example = new CommentReply();
example.setSenderId(user.getId());
List<CommentReply> replies = replyMapper.selectAllByExample(example);
for (CommentReply reply : replies) {
CommentRemindQueue destroyRemindExample = new CommentRemindQueue();
destroyRemindExample.setReplyId(reply.getReplyId());
remindQueueMapper.destroyAllByExample(destroyRemindExample);
/**
* 按父评论 ID 分组
*
* @param commentList 评论列表
* @return 分组结果
*/
private Map<String, List<Comment>> groupByParentId(List<Comment> commentList) {
Map<String, List<Comment>> result = new LinkedHashMap<>();
for (Comment comment : commentList) {
if (TimiJava.isEmpty(comment.getParentId())) {
continue;
}
result.computeIfAbsent(comment.getParentId(), key -> new ArrayList<>()).add(comment);
}
{
// 删除回复
CommentReply example = new CommentReply();
example.setCommentId(commentId);
replyMapper.deleteAllByExample(example);
return result;
}
/**
* 收集待删除评论
*
* @param current 当前评论
* @param childMap 子评论分组
* @param result 结果列表
*/
private void collectDeleteComment(Comment current, Map<String, List<Comment>> childMap, List<Comment> result) {
result.add(current);
List<Comment> childList = childMap.get(current.getId());
if (childList == null || childList.isEmpty()) {
return;
}
for (Comment child : childList) {
collectDeleteComment(child, childMap, result);
}
// 删除评论
super.delete(commentId);
}
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.DataDictOption;
import com.imyeyu.api.modules.common.mapper.DataDictOptionMapper;
import com.imyeyu.api.modules.common.service.DataDictOptionService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 数据字典选项服务实现
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Service
@RequiredArgsConstructor
public class DataDictOptionServiceImplement extends AbstractEntityService<DataDictOption, String> implements DataDictOptionService {
private final DataDictOptionMapper mapper;
@Override
protected BaseMapper<DataDictOption, String> mapper() {
return mapper;
}
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.DataDict;
import com.imyeyu.api.modules.common.mapper.DataDictMapper;
import com.imyeyu.api.modules.common.service.DataDictService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 数据字典服务实现
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Service
@RequiredArgsConstructor
public class DataDictServiceImplement extends AbstractEntityService<DataDict, String> implements DataDictService {
private final DataDictMapper mapper;
@Override
protected BaseMapper<DataDict, String> mapper() {
return mapper;
}
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.EmailFilter;
import com.imyeyu.api.modules.common.mapper.EmailFilterMapper;
import com.imyeyu.api.modules.common.service.EmailFilterService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 邮件过滤服务实现
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Service
@RequiredArgsConstructor
public class EmailFilterServiceImplement extends AbstractEntityService<EmailFilter, String> implements EmailFilterService {
private final EmailFilterMapper mapper;
@Override
protected BaseMapper<EmailFilter, String> mapper() {
return mapper;
}
}
@@ -1,50 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.EmailQueue;
import com.imyeyu.api.modules.common.entity.EmailQueueLog;
import com.imyeyu.api.modules.common.mapper.EmailQueueLogMapper;
import com.imyeyu.api.modules.common.mapper.EmailQueueMapper;
import com.imyeyu.api.modules.common.service.EmailQueueService;
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-24 16:21
*/
@Service
@RequiredArgsConstructor
public class EmailQueueServiceImplement extends AbstractEntityService<EmailQueue, String> implements EmailQueueService {
private final EmailQueueMapper mapper;
private final EmailQueueLogMapper logMapper;
@Override
protected BaseMapper<EmailQueue, String> mapper() {
return mapper;
}
@Override
public EmailQueue get(EmailQueue.BizType type, Long dataId) {
return mapper.query(type, dataId);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void addLog(EmailQueueLog log) {
logMapper.insert(log);
}
@Override
public List<EmailQueue> listAll() {
return mapper.listAll();
}
}
@@ -1,40 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Feedback;
import com.imyeyu.api.modules.common.mapper.FeedbackMapper;
import com.imyeyu.api.modules.common.service.FeedbackService;
import com.imyeyu.api.modules.common.vo.FeedbackRequest;
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;
/**
* 反馈服务实现
*
* @author 夜雨
* @since 2021-11-16 22:27
*/
@Service
@RequiredArgsConstructor
public class FeedbackServiceImplement extends AbstractEntityService<Feedback, Long> implements FeedbackService {
private final FeedbackMapper mapper;
@Override
protected BaseMapper<Feedback, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(FeedbackRequest request) {
Feedback feedback = new Feedback();
feedback.setFrom(request.getFrom());
feedback.setEmail(request.getEmail());
feedback.setData(request.getData());
super.create(feedback);
}
}
@@ -3,16 +3,21 @@ package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.Icon;
import com.imyeyu.api.modules.common.mapper.IconMapper;
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.bean.PageResult;
import com.imyeyu.font.icon.IconFontBuilder;
import com.imyeyu.font.icon.model.GlyphAlignment;
import com.imyeyu.font.icon.model.IconGlyph;
import com.imyeyu.font.icon.model.IconFontOptions;
import com.imyeyu.java.TimiJava;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 图标服务实现
@@ -22,53 +27,55 @@ import java.util.List;
*/
@Service
@RequiredArgsConstructor
public class IconServiceImplement extends AbstractEntityService<Icon, Long> implements IconService {
public class IconServiceImplement extends AbstractEntityService<Icon, String> implements IconService {
private static final IconFontOptions TTF_OPTIONS = createTtfOptions();
private final IconMapper mapper;
@Override
protected BaseMapper<Icon, Long> mapper() {
protected BaseMapper<Icon, String> mapper() {
return mapper;
}
@Override
public AllResponse listAll(Long latest) {
List<Icon> list = mapper.listAll();
long dbLatest = -1;
for (int i = 0; i < list.size(); i++) {
if (dbLatest < list.get(i).getCreatedAt()) {
dbLatest = list.get(i).getCreatedAt();
}
if (list.get(i).getUpdatedAt() != null && dbLatest < list.get(i).getUpdatedAt()) {
dbLatest = list.get(i).getUpdatedAt();
}
}
AllResponse response = new AllResponse();
response.setLatest(dbLatest);
if (latest < dbLatest) {
// 存在更新
response.setIcons(list);
}
return response;
public List<String> listName() {
return allValid().map(Icon::getName).toList();
}
@Override
public PageResult<Icon> pageByName(NamePage page) {
PageResult<Icon> result = new PageResult<>();
result.setList(mapper.listByName(page.getName(), page.getOffset(), page.getLimit()));
result.setTotal(mapper.countByName(page.getName()));
return result;
public Map<String, String> mapSvg() {
return allValid().collect(Collectors.toMap(Icon::getName, Icon::getSvg));
}
@Override
public PageResult<Icon> pageByUnicode(UnicodePage page) {
String unicode = page.getUnicode().toLowerCase();
if (unicode.startsWith("0x") || unicode.startsWith("&#") || unicode.startsWith("\\u")) {
unicode = unicode.substring(2);
}
PageResult<Icon> result = new PageResult<>();
result.setList(mapper.listByUnicode(unicode, page.getOffset(), page.getLimit()));
result.setTotal(mapper.countByUnicode(unicode));
return result;
public Map<String, String> mapUnicode() {
return allValid().collect(Collectors.toMap(Icon::getName, Icon::getUnicode));
}
@Override
public byte[] exportTtf() {
List<IconGlyph> icons = allValid().map(icon -> IconGlyph.of(icon.getName(), icon.getUnicode(), icon.getSvg())).toList();
return IconFontBuilder.build(TTF_OPTIONS, icons);
}
private static IconFontOptions createTtfOptions() {
IconFontOptions options = IconFontOptions.defaults("timi-icon")
.withSourceBounds(0, 0, 16, 16)
.withAlignment(GlyphAlignment.GLYPH_CENTER, GlyphAlignment.GLYPH_CENTER)
.withSnapOffsetToGrid(true);
options.setGlyphPadding(0);
options.setAscent(960);
options.setDescent(-64);
return options;
}
private Stream<Icon> allValid() {
return mapper.selectAll()
.stream()
.filter(icon -> TimiJava.isNotEmpty(icon.getName()))
.filter(icon -> TimiJava.isNotEmpty(icon.getUnicode()))
.filter(icon -> TimiJava.isNotEmpty(icon.getSvg()))
.sorted(Comparator.comparing(Icon::getName));
}
}
@@ -1,14 +1,8 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.mapper.MultilingualMapper;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.spring.util.Redis;
@@ -17,6 +11,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
/**
@@ -26,70 +21,62 @@ import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class MultilingualServiceImplement extends AbstractEntityService<Multilingual, Long> implements MultilingualService {
public class MultilingualServiceImplement extends AbstractEntityService<Multilingual, String> implements MultilingualService {
private final SettingService settingService;
private static final String KEY_PREFIX = "MULTILINGUAL:";
private final MultilingualMapper mapper;
private final Redis<String, Long> redisLanguageMap;
private final Redis<Long, Multilingual> redisLanguage;
private final Redis<String, Multilingual> redisLanguage;
@Override
protected BaseMapper<Multilingual, Long> mapper() {
protected BaseMapper<Multilingual, String> mapper() {
return mapper;
}
@Override
public Long create(String key, String zhCN) {
Multilingual multilingual = new Multilingual();
multilingual.setKey(key);
multilingual.setZhCN(zhCN);
mapper.insert(multilingual);
public Multilingual get(String id) {
Multilingual result = redisLanguage.get(getKey(id));
if (result == null) {
result = super.get(id);
redisLanguage.set(getKey(id), result, Time.D * 7);
}
return result;
}
@Override
public String createIfDifferent(String id, String zhCN) {
Multilingual multilingual = mapper.select(id);
if (!multilingual.getZhCN().equals(zhCN)) {
return create(zhCN);
}
return multilingual.getId();
}
@Override
public Long createIfNotExist(String key, String zhCN) {
Multilingual existItem = mapper.selectByZhCN(zhCN);
if (TimiJava.isNotEmpty(existItem)) {
return existItem.getId();
}
return create(key, zhCN);
}
public List<Multilingual> listByIdList(List<String> idList) {
List<Multilingual> result = new ArrayList<>();
@Override
public String get(Language.Enum language, Long id) {
Multilingual result = redisLanguage.get(id);
if (result == null) {
result = mapper.select(id);
if (result == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not found language");
List<String> notInCacheIdList = new ArrayList<>();
for (String id : idList) {
Multilingual cache = redisLanguage.get(getKey(id));
if (cache == null) {
notInCacheIdList.add(id);
} else {
result.add(cache);
}
redisLanguage.set(id, result, Time.D * settingService.getAsInt(SettingKey.TTL_MULTILINGUAL));
}
return result.getValue(language);
List<Multilingual> notInCache = mapper.selectByIdList(notInCacheIdList);
for (Multilingual multilingual : notInCache) {
redisLanguage.set(getKey(multilingual.getId()), multilingual, Time.D * 7);
}
result.addAll(notInCache);
return result;
}
@Override
public String getByKey(Language.Enum language, String key) {
Long languageId = redisLanguageMap.get(key);
if (languageId == null) {
Multilingual result = mapper.selectByKey(key);
if (result == null) {
log.warn("not found language for key: {}", key);
return key;
}
long ttl = Time.D * settingService.getAsInt(SettingKey.TTL_MULTILINGUAL);
redisLanguage.set(result.getId(), result, ttl);
redisLanguageMap.set(result.getKey(), result.getId(), ttl);
return result.getValue(language);
}
return get(language, languageId);
}
@Override
public List<Multilingual> listByNotTranslate() {
return mapper.selectByNotTranslate();
private String getKey(String id) {
return "%s%s".formatted(KEY_PREFIX, id);
}
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.NotifyDetail;
import com.imyeyu.api.modules.common.mapper.NotifyDetailMapper;
import com.imyeyu.api.modules.common.service.NotifyDetailService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 通知详情服务实现
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Service
@RequiredArgsConstructor
public class NotifyDetailServiceImplement extends AbstractEntityService<NotifyDetail, String> implements NotifyDetailService {
private final NotifyDetailMapper mapper;
@Override
protected BaseMapper<NotifyDetail, String> mapper() {
return mapper;
}
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.Notify;
import com.imyeyu.api.modules.common.mapper.NotifyMapper;
import com.imyeyu.api.modules.common.service.NotifyService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 通知服务实现
*
* @author 夜雨
* @since 2026-05-13 14:46
*/
@Service
@RequiredArgsConstructor
public class NotifyServiceImplement extends AbstractEntityService<Notify, String> implements NotifyService {
private final NotifyMapper mapper;
@Override
protected BaseMapper<Notify, String> mapper() {
return mapper;
}
}
@@ -1,29 +1,22 @@
package com.imyeyu.api.modules.common.service.implement;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
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.modules.common.bean.SettingKey;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.mapper.SettingMapper;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
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.stereotype.Service;
import org.yaml.snakeyaml.Yaml;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 系统配置服务实现
@@ -37,9 +30,6 @@ import java.util.List;
public class SettingServiceImplement extends AbstractEntityService<Setting, String> implements SettingService {
private final SettingMapper mapper;
private final Redis<String, String> redisSetting;
private final ObjectMapper jackson;
@Override
protected BaseMapper<Setting, String> mapper() {
@@ -47,132 +37,74 @@ public class SettingServiceImplement extends AbstractEntityService<Setting, Stri
}
@Override
public void update(Setting setting) {
super.update(setting);
clearCache(setting.getKey());
public Setting getByCode(String bizType, String bizId, String code) {
Setting example = new Setting();
example.setBizType(bizType);
example.setBizId(bizId);
example.setCode(code);
return mapper.selectByExample(example);
}
public Setting getByKey(SettingKey key) {
if (key == null) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("key can not be null");
@Override
public List<Setting> listByBizId(String bizType, String bizId) {
Setting example = new Setting();
example.setBizType(bizType);
example.setBizId(bizId);
return mapper.selectAllByExample(example);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void updateByBizId(String bizType, String bizId, List<Setting> list) {
TimiException.required(bizType, "not found bizType");
list = TimiJava.defaultIfNull(list, new ArrayList<>());
List<Setting> dbList = listByBizId(bizType, bizId);
Map<String, Setting> dbMap = new HashMap<>(dbList.size());
for (Setting item : dbList) {
dbMap.put(item.getCode(), item);
}
String cacheValue = redisSetting.get(key.toString());
if (TimiJava.isNotEmpty(cacheValue)) {
try {
return jackson.readValue(cacheValue, Setting.class);
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "read setting cache error", e);
Map<String, Setting> map = new HashMap<>(list.size());
for (Setting item : list) {
TimiException.required(item.getCode(), "not found code");
item.setBizType(bizType);
item.setBizId(bizId);
map.put(item.getCode(), item);
}
for (Setting dbSetting : dbList) {
if (!map.containsKey(dbSetting.getCode())) {
// 删除
delete(dbSetting.getId());
}
}
Setting setting = mapper.selectByKey(key);
if (TimiJava.isEmpty(setting.getValue())) {
return setting;
}
int settingTTL;
if (key == SettingKey.TTL_SETTING) {
settingTTL = Integer.parseInt(setting.getValue());
} else {
settingTTL = Integer.parseInt(getByKey(SettingKey.TTL_SETTING).getValue());
}
if (0 < settingTTL) {
try {
redisSetting.set(key.toString(), jackson.writeValueAsString(setting), Time.M * settingTTL);
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "write setting cache error", e);
for (Setting setting : map.values()) {
Setting dbSetting = dbMap.get(setting.getCode());
if (dbSetting == null) {
// 新增
create(setting);
continue;
}
}
return setting;
}
@Override
public List<Setting> listByKeys(List<SettingKey> keyList) {
List<Setting> result = new ArrayList<>();
for (int i = 0; i < keyList.size(); i++) {
result.add(getByKey(keyList.get(i)));
}
return result;
}
@Override
public String getAsString(SettingKey key) {
return getByKey(key).getValue();
}
@Override
public int getAsInt(SettingKey key) {
return Integer.parseInt(getAsString(key));
}
@Override
public long getAsLong(SettingKey key) {
return Long.parseLong(getAsString(key));
}
@Override
public double getAsDouble(SettingKey key) {
return Double.parseDouble(getAsString(key));
}
@Override
public boolean is(SettingKey key) {
return Boolean.parseBoolean(getAsString(key));
}
@Override
public boolean not(SettingKey key) {
return !is(key);
}
@Override
public JsonNode getAsJsonNode(SettingKey key) {
try {
return jackson.readTree(getAsString(key));
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "read setting json error", e);
// 更新
Setting update = new Setting();
update.setId(dbSetting.getId());
update.setBizType(bizType);
update.setBizId(bizId);
update.setCode(setting.getCode());
update.setValue(setting.getValue());
update.setValueType(setting.getValueType());
update.setValueArgs(setting.getValueArgs());
update.setDescription(setting.getDescription());
update.setIsPrivate(setting.getIsPrivate());
mapper.update(update);
}
}
@Override
public ObjectNode getAsJsonObject(SettingKey key) {
return (ObjectNode) getAsJsonNode(key);
}
@Override
public ArrayNode getAsArrayNode(SettingKey key) {
return (ArrayNode) getAsJsonNode(key);
}
@Override
public <T> T fromJson(SettingKey key, Class<T> clazz) {
try {
return jackson.treeToValue(getAsJsonNode(key), clazz);
} catch (JsonProcessingException e) {
throw new TimiException(TimiCode.ERROR, "read setting json error", e);
}
}
@Override
public <T> T fromJson(SettingKey key, TypeReference<T> typeReference) {
return jackson.convertValue(getAsJsonNode(key), typeReference);
}
public <T> T fromYaml(SettingKey key, Class<T> clazz) {
return new Yaml().loadAs(getAsString(key), clazz);
}
@Override
public List<Setting> listAll() {
return mapper.listAll();
}
@Override
public void clearCache(SettingKey key) {
redisSetting.destroy(key.toString());
}
@Override
public void flushCache() {
redisSetting.flushAll();
public void deleteByBizId(String bizType, String bizId) {
Setting example = new Setting();
example.setBizType(bizType);
example.setBizId(bizId);
mapper.deleteAllByExample(example);
}
}
@@ -1,9 +1,7 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.entity.Tag;
import com.imyeyu.api.modules.common.mapper.TagMapper;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.api.modules.common.service.TagService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
@@ -11,9 +9,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
/**
* @author 夜雨
* @since 2025-05-30 22:47
@@ -21,29 +16,12 @@ import java.util.UUID;
@Slf4j
@Service
@RequiredArgsConstructor
public class TagServiceImplement extends AbstractEntityService<Tag, Long> implements TagService {
private final MultilingualService multilingualService;
public class TagServiceImplement extends AbstractEntityService<Tag, String> implements TagService {
private final TagMapper mapper;
@Override
protected BaseMapper<Tag, Long> mapper() {
protected BaseMapper<Tag, String> mapper() {
return mapper;
}
@Override
public void create(Tag tag) {
Long langId = multilingualService.createIfNotExist(UUID.randomUUID().toString(), tag.getValue());
tag.setValue(String.valueOf(langId));
super.create(tag);
}
@Override
public List<Tag> listByBizID(Tag.BizType bizType, String bizID) throws TimiException {
Tag example = new Tag();
example.setBizType(bizType);
example.setBizID(bizID);
return mapper.selectAllByExample(example);
}
}
@@ -1,28 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.entity.Task;
import com.imyeyu.api.modules.common.mapper.TaskMapper;
import com.imyeyu.api.modules.common.service.TaskService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.Comparator;
import java.util.List;
/**
* 任务服务
*
* @author 夜雨
* @since 2022-04-03 15:37
*/
@Service
@RequiredArgsConstructor
public class TaskServiceImplement implements TaskService {
private final TaskMapper mapper;
@Override
public List<Task> listAll4Public() {
return mapper.listAll4Public().stream().sorted(Comparator.comparingInt(c -> c.getStatus().getSort())).toList();
}
}
@@ -1,10 +1,17 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.Setting;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.common.service.TempFileService;
import com.imyeyu.api.modules.common.vo.TempFileResponse;
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
import com.imyeyu.api.modules.common.vo.attach.TempFileSaveReq;
import com.imyeyu.api.modules.system.entity.Logger;
import com.imyeyu.api.modules.system.service.LoggerService;
import com.imyeyu.io.IOSize;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
@@ -29,62 +36,138 @@ import java.util.List;
@RequiredArgsConstructor
public class TempFileServiceImplement implements TempFileService {
private static final Attachment.BizType BIZ_TYPE = Attachment.BizType.TEMP_FILE;
private final ObjectMapper jackson;
private final LoggerService loggerService;
private final SettingService settingService;
private final AttachmentService attachmentService;
private final AttachmentService service;
public List<TempFileResponse> store(List<MultipartFile> files, Long ttl) throws TimiException {
String ttlMinStr = settingService.getAsString(SettingKey.TEMP_FILE_TTL_MIN);
String ttlMaxStr = settingService.getAsString(SettingKey.TEMP_FILE_TTL_MAX);
String limitStr = settingService.getAsString(SettingKey.TEMP_FILE_LIMIT);
long minTTL = Time.parseToMS(ttlMinStr);
long maxTTL = Time.parseToMS(ttlMaxStr);
long defaultTTL = Time.parseToMS(settingService.getAsString(SettingKey.TEMP_FILE_TTL_DEFAULT));
long limit = IOSize.parse(limitStr);
long residueTime = Time.parseToMS(settingService.getAsString(SettingKey.TEMP_FILE_RESIDUE_TIME));
ttl = TimiJava.defaultIfNull(ttl, defaultTTL);
TimiException.requiredTrue(minTTL < ttl && ttl <= maxTTL, String.format("ttl must be between %s and %s", ttlMinStr, ttlMaxStr));
long newFileSize = files.stream().mapToLong(MultipartFile::getSize).sum();
{
// 缓存池大小限制
Page<Attachment> page = new Page<>();
page.setIndex(0);
page.setSize(Long.MAX_VALUE);
{
Attachment example = new Attachment();
example.setUploaderIp(TimiSpring.getRequestIP());
page.setEqualsExample(example);
}
long currentUsage = attachmentService.page(page).getList().stream().mapToLong(Attachment::getSize).sum();
TimiException.requiredTrue(currentUsage + newFileSize < limit, "out of storage limit(%s)".formatted(limitStr));
}
public List<TempFileResp> create(List<MultipartFile> files, Long ttl) throws TimiException {
Logger logger = new Logger();
logger.setModule("TEMP_FILE");
logger.setOperation("STORE");
logger.setIp(TimiSpring.getRequestIP());
try {
List<TempFileResponse> result = new ArrayList<>();
logger.setContent(jackson.writeValueAsString(logRequestArgs(files, ttl)));
Setting ttlMin = settingService.getSystem(Setting.Module.TempFile.TTL_MIN);
Setting ttlMax = settingService.getSystem(Setting.Module.TempFile.TTL_MAX);
Setting ttlDefault = settingService.getSystem(Setting.Module.TempFile.TTL_DEFAULT);
Setting residueTime = settingService.getSystem(Setting.Module.TempFile.RESIDUE_TTL);
String limitStr = settingService.getSystem(Setting.Module.TempFile.LIMIT_SIZE_OF_IP).asString();
long limit = IOSize.parse(limitStr);
ttl = TimiJava.defaultIfNull(ttl, ttlDefault.asTime());
TimiException.requiredTrue(ttlMin.asTime() < ttl && ttl <= ttlMax.asTime(), String.format("ttl must be between %s and %s", ttlMin.asString(), ttlMax.asString()));
long newFileSize = files.stream().mapToLong(MultipartFile::getSize).sum();
{
// 缓存池大小限制
Page<Attachment> page = new Page<>();
page.setIndex(0);
page.setSize(Long.MAX_VALUE);
{
Attachment example = new Attachment();
example.setUploaderIp(TimiSpring.getRequestIP());
page.setEqualsExample(example);
}
long currentUsage = service.page(page).getList().stream().mapToLong(Attachment::getSize).sum();
TimiException.requiredTrue(currentUsage + newFileSize < limit, "out of storage limit(%s)".formatted(limitStr));
}
List<TempFileResp> result = new ArrayList<>();
long deletedAt = Time.now() + ttl;
long destroyAt = deletedAt + residueTime;
for (int i = 0; i < files.size(); i++) {
MultipartFile file = files.get(i);
long destroyAt = deletedAt + residueTime.asTime();
for (MultipartFile file : files) {
Attachment attachment = new Attachment();
attachment.setBizType(BIZ_TYPE);
attachment.setName(file.getOriginalFilename());
attachment.setDeletedAt(deletedAt);
attachment.setDestroyAt(destroyAt);
attachment.setInputStream(file.getInputStream());
service.create(attachment);
Attachment attach = new Attachment();
attach.setBizType(Attachment.BizType.TEMP_FILE);
attach.setName(file.getOriginalFilename());
attach.setDeletedAt(deletedAt);
attach.setDestroyAt(destroyAt);
attach.setInputStream(file.getInputStream());
attachmentService.create(attach);
TempFileResponse resp = new TempFileResponse();
resp.setId(attach.getMongoId());
TempFileResp resp = new TempFileResp();
resp.setId(attachment.getId());
resp.setExpireAt(deletedAt);
result.add(resp);
}
logger.setLevel(Logger.Level.INFO);
logger.setResult(jackson.writeValueAsString(result));
return result;
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("store temp file error", e);
throw new TimiException(TimiCode.ERROR, "store temp file error", e);
} finally {
loggerService.create(logger);
}
}
@Override
public void save(TempFileSaveReq request) {
Logger logger = new Logger();
logger.setModule("TEMP_FILE");
logger.setOperation("SAVE");
logger.setIp(TimiSpring.getRequestIP());
try {
logger.setContent(jackson.writeValueAsString(request));
String[] tempFileIds = request.getTempFileIds();
if (TimiJava.isEmpty(tempFileIds)) {
return;
}
for (String tempFileId : tempFileIds) {
Attachment tempFileAttach = service.get(tempFileId);
if (tempFileAttach == null) {
continue;
}
Attachment attachment = new Attachment();
attachment.setName(tempFileAttach.getName());
attachment.setBizType(request.getBizType());
attachment.setBizId(request.getBizId());
attachment.setInputStream(tempFileAttach.openInputStream());
service.create(attachment);
}
logger.setLevel(Logger.Level.INFO);
} catch (TimiException e) {
logger.setLevel(Logger.Level.WARN);
logger.setException(e.getMessage());
throw e;
} catch (Exception e) {
logger.setLevel(Logger.Level.ERROR);
logger.setException(TimiJava.serializeThrowable(e));
log.error("save temp file error", e);
throw new TimiException(TimiCode.ERROR, "save temp file error", e);
} finally {
loggerService.create(logger);
}
}
private ObjectNode logRequestArgs(List<MultipartFile> files, Long ttl) {
ObjectNode result = jackson.createObjectNode();
result.put("ttl", ttl);
{
ArrayNode filesArr = jackson.createArrayNode();
for (MultipartFile file : files) {
ObjectNode fileObj = jackson.createObjectNode();
fileObj.put("name", file.getName());
fileObj.put("originalFilename", file.getOriginalFilename());
fileObj.put("contentType", file.getContentType());
fileObj.put("size", file.getSize());
filesArr.add(fileObj);
}
result.set("files", filesArr);
}
return result;
}
}
@@ -1,31 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.common.entity.Template;
import com.imyeyu.api.modules.common.mapper.TemplateMapper;
import com.imyeyu.api.modules.common.service.TemplateService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author 夜雨
* @since 2023-09-22 16:40
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class TemplateServiceImplement implements TemplateService {
private final TemplateMapper mapper;
@Override
public Template get(Template.BizType bizType, String bizCode) {
Template template = mapper.query(bizType, bizCode);
if (template == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("not found template");
}
return template;
}
}
@@ -1,45 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.api.modules.common.mapper.UserConfigMapper;
import com.imyeyu.api.modules.common.service.UserConfigService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户设置服务实现
*
* @author 夜雨
* @since 2021-08-12 16:24
*/
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class UserConfigServiceImplement extends AbstractEntityService<UserConfig, Long> implements UserConfigService {
private final UserService userService;
private final UserConfigMapper mapper;
@Override
protected UserConfigMapper mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(UserConfig config) {
User user = userService.getLoginUser();
UserConfig dbConfig = get(user.getId());
dbConfig.setUserId(user.getId());
dbConfig.setEmailReplyRemind(config.isEmailReplyRemind());
dbConfig.setUpdatedAt(Time.now());
super.update(dbConfig);
}
}
@@ -1,50 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.entity.UserPrivacy;
import com.imyeyu.api.modules.common.mapper.UserPrivacyMapper;
import com.imyeyu.api.modules.common.service.UserPrivacyService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户隐私控制服务实现
*
* @author 夜雨
* @since 2021-07-27 17:19
*/
@Service
@RequiredArgsConstructor
public class UserPrivacyServiceImplement extends AbstractEntityService<UserPrivacy, Long> implements UserPrivacyService {
private final UserService userService;
private final UserPrivacyMapper mapper;
@Override
protected BaseMapper<UserPrivacy, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(UserPrivacy privacy) {
User user = userService.getLoginUser();
UserPrivacy dbPrivacy = get(user.getId());
dbPrivacy.setEmail(privacy.isEmail());
dbPrivacy.setSex(privacy.isSex());
dbPrivacy.setBirthdate(privacy.isBirthdate());
dbPrivacy.setQq(privacy.isQq());
dbPrivacy.setLastLoginAt(privacy.isLastLoginAt());
dbPrivacy.setCreatedAt(privacy.isCreatedAt());
dbPrivacy.setUserId(user.getId());
dbPrivacy.setUpdatedAt(Time.now());
super.update(dbPrivacy);
}
}
@@ -1,104 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.entity.UserProfile;
import com.imyeyu.api.modules.common.mapper.UserProfileMapper;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.UserProfileService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.common.vo.user.UserRequest;
import com.imyeyu.io.IOSize;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
/**
* 用户数据服务实现
*
* @author 夜雨
* @since 2021-07-27 17:08
*/
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class UserProfileServiceImplement extends AbstractEntityService<UserProfile, Long> implements UserProfileService {
private final UserService userService;
private final AttachmentService attachmentService;
private final UserProfileMapper mapper;
@Override
protected BaseMapper<UserProfile, Long> mapper() {
return mapper;
}
@Override
public void update(UserProfile profile) {
UserProfile dbProfile = get(profile.getUserId());
dbProfile.setWrapperType(profile.getWrapperType());
dbProfile.setAvatarType(profile.getAvatarType());
dbProfile.setSex(profile.getSex());
dbProfile.setBirthdate(profile.getBirthdate());
dbProfile.setQq(profile.getQq());
dbProfile.setDescription(profile.getDescription());
super.update(dbProfile);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(UserRequest request) {
try {
User user = userService.getLoginUser();
if (TimiJava.isNotEmpty(request.getProfile().getWrapper())) {
Attachment dbWrapper = attachmentService.getByAttachType(Attachment.BizType.USER, request.getId(), User.AttachType.WRAPPER);
// TODO 限制 PNG
if (dbWrapper != null) {
attachmentService.delete(dbWrapper.getId());
}
MultipartFile wrapper = request.getProfile().getWrapper();
// 字节数据
byte[] bytes = wrapper.getInputStream().readAllBytes();
if (IOSize.MB < bytes.length) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("限制上传文件大小 1 MB");
}
Attachment wrapperAttach = new Attachment();
wrapperAttach.setName(request.getId() + ".png");
wrapperAttach.setBizType(Attachment.BizType.USER);
wrapperAttach.setBizId(request.getId());
wrapperAttach.setAttachTypeValue(User.AttachType.WRAPPER);
wrapperAttach.setSize(wrapper.getSize());
wrapperAttach.setInputStream(wrapper.getInputStream());
attachmentService.create(wrapperAttach);
}
if (TimiJava.isNotEmpty(request.getProfile().getAvatar())) {
Attachment dbAvatar = attachmentService.getByAttachType(Attachment.BizType.USER, request.getId(), User.AttachType.AVATAR);
if (dbAvatar != null) {
attachmentService.delete(dbAvatar.getId());
}
MultipartFile avatar = request.getProfile().getAvatar();
Attachment avatarAttach = new Attachment();
avatarAttach.setName(request.getId() + ".png");
avatarAttach.setBizType(Attachment.BizType.USER);
avatarAttach.setBizId(request.getId());
avatarAttach.setAttachTypeValue(User.AttachType.AVATAR);
avatarAttach.setSize(avatar.getSize());
avatarAttach.setInputStream(avatar.getInputStream());
attachmentService.create(avatarAttach);
}
update(request.getProfile());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
@@ -1,403 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.blog.util.UserToken;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.CommentReply;
import com.imyeyu.api.modules.common.entity.EmailQueue;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.entity.UserConfig;
import com.imyeyu.api.modules.common.entity.UserPrivacy;
import com.imyeyu.api.modules.common.entity.UserProfile;
import com.imyeyu.api.modules.common.mapper.CommentMapper;
import com.imyeyu.api.modules.common.mapper.CommentReplyMapper;
import com.imyeyu.api.modules.common.mapper.UserMapper;
import com.imyeyu.api.modules.common.mapper.UserProfileMapper;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.EmailQueueService;
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.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.UserProfileView;
import com.imyeyu.api.modules.common.vo.user.UserView;
import com.imyeyu.api.modules.git.entity.Developer;
import com.imyeyu.api.modules.git.service.DeveloperService;
import com.imyeyu.api.modules.minecraft.service.PlayerService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Logic;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.spring.util.Redis;
import com.imyeyu.utils.Calc;
import com.imyeyu.utils.Digest;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.UUID;
/**
* 用户管理服务实现
*
* @author 夜雨
* @since 2021-02-23 21:43
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class UserServiceImplement extends AbstractEntityService<User, Long> implements UserService, TimiJava {
private final PlayerService mcPlayerService;
private final DeveloperService gitDeveloperService;
private final EmailQueueService emailQueueService;
private final UserConfigService configService;
private final AttachmentService attachmentService;
private final UserProfileService profileService;
private final UserPrivacyService privacyService;
private final UserMapper mapper;
private final CommentMapper commentMapper;
private final UserProfileMapper userProfileMapper;
private final CommentReplyMapper commentReplyMapper;
private final Redis<Long, String> redisUserExpFlag;
private final Redis<String, Long> redisUserEmailVerify;
private final Redis<String, Long> redisUserResetPWVerify;
private final UserToken userToken;
@Override
protected BaseMapper<User, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(User user) {
User userByName = getByName(user.getName());
if (userByName != null && !userByName.getId().equals(user.getId())) {
throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.name.exist");
}
User userByEmail = getByEmail(user.getEmail());
if (userByEmail != null && !userByEmail.getId().equals(user.getId())) {
throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.email.exist_and_verified");
}
User dbUser = get(user.getId());
if (TimiJava.isNotEmpty(user.getEmail())) {
if (!user.getEmail().equals(dbUser.getEmail())) {
// 重新验证
dbUser.setEmailVerifyAt(null);
}
}
dbUser.setEmail(user.getEmail());
dbUser.setName(user.getName());
super.update(dbUser);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public LoginResponse register(RegisterRequest request) {
if (getByName(request.getName()) != null) {
throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.register.exist_name");
}
if (getByEmail(request.getEmail()) != null) {
throw new TimiException(TimiCode.DATA_EXIST).msgKey("user.register.exist_email");
}
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setPassword(digestPassword(request.getPassword(), Text.randomString(16)));
user.setCreatedAt(Time.now());
// 注册账号
create(user);
// 初始化资料
profileService.create(new UserProfile(user.getId()));
// 初始化隐私控制
privacyService.create(new UserPrivacy(user.getId()));
// 初始化设置
configService.create(new UserConfig(user.getId()));
// 初始化开发者
gitDeveloperService.create(new Developer(user.getId()));
// 自动登录
return login(new LoginRequest(String.valueOf(user.getId()), request.getPassword()));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public LoginResponse login(LoginRequest request) {
if (TimiJava.isEmpty(request)) {
throw new TimiException(TimiCode.ARG_MISS);
}
// 用户
if (TimiJava.isEmpty(request.getUser())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("user.login.user.empty");
}
// 密码
if (TimiJava.isEmpty(request.getPassword())) {
throw new TimiException(TimiCode.ARG_MISS).msgKey("user.login.password.empty");
}
User result = get(request.getUser());
if (request.getUser().contains("@")) {
if (!result.emailVerified()) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.login.email.not_verify");
}
}
if (TimiJava.isEmpty(result)) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("user.login.not_found");
}
if (result.isBanning()) {
throw new TimiException(TimiCode.RESULT_BAN).msgKey("user.login.baned");
}
if (isInvalidPassword(result.getPassword(), request.getPassword())) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.login.password.mismatch");
}
// 用户数据
UserProfile profile = profileService.get(result.getId());
profile.setLastLoginIP(TimiSpring.getRequestIP());
profile.setLastLoginAt(Time.now());
updateExp(profile.getUserId());
userProfileMapper.update(profile);
// 生成并缓存 Token
String token = UUID.randomUUID().toString();
Long expireAt = userToken.set(token, result.getId());
return new LoginResponse(result.getId(), token, expireAt);
}
@Override
public UserView view(Long userId) {
UserProfileView profile = new UserProfileView();
{
// 附件
List<Attachment> attachmentList = attachmentService.listByBizId(Attachment.BizType.USER, userId);
boolean hasAvatar = false, hasWrapper = false;
for (int i = 0; i < attachmentList.size(); i++) {
if (!hasAvatar && User.AttachType.AVATAR.toString().equals(attachmentList.get(i).getAttachType())) {
hasAvatar = true;
}
if (!hasWrapper && User.AttachType.WRAPPER.toString().equals(attachmentList.get(i).getAttachType())) {
hasWrapper = true;
}
}
if (!hasAvatar) {
attachmentList.add(attachmentService.getByAttachType(Attachment.BizType.USER, 0, User.AttachType.DEFAULT_AVATAR));
}
if (!hasWrapper) {
attachmentList.add(attachmentService.getByAttachType(Attachment.BizType.USER, 0, User.AttachType.DEFAULT_WRAPPER));
}
profile.setAttachmentList(attachmentList);
}
BeanUtils.copyProperties(profileService.get(userId), profile);
UserView view = new UserView();
view.setProfile(profile);
BeanUtils.copyProperties(get(userId), view);
return view;
}
@Override
public boolean isInvalidPassword(String digestPassword, String password) {
// "$SHA$盐$摘要(摘要(明文) + 盐)
String[] arr = digestPassword.split("\\$");
return digestPassword(digestPassword, arr[2]).equals(password);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public LoginResponse login4Token() {
String token = TimiSpring.getToken();
try {
User user = getLoginUser();
updateExp(user.getId());
return new LoginResponse(user.getId(), token, userToken.getExpireAt(token));
} catch (Exception e) {
throw new TimiException(TimiCode.IGNORE);
}
}
@Override
public void logout() {
userToken.clear(TimiSpring.getToken());
}
@Override
public User get(String user) {
User result;
if (Calc.isNumber(user)) {
result = get(Long.parseLong(user));
} else if (user.contains("@")) {
result = getByEmail(user);
} else {
result = getByName(user);
}
return result;
}
@Override
public boolean isLogged() {
return userToken.isValid(TimiSpring.getToken());
}
@Override
public User getLoginUser() {
String token = TimiSpring.getToken();
if (TimiJava.isEmpty(token)) {
return null;
}
return userToken.getUser(token);
}
@Override
public User getByName(String name) {
return mapper.selectByName(name);
}
@Override
public User getByEmail(String email) {
return mapper.selectByEmail(email);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void sendEmailVerify() {
User user = getLoginUser();
if (TimiJava.isEmpty(user.getEmail())) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.email.empty");
}
if (user.emailVerified()) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.email.verified");
}
EmailQueue emailQueue = new EmailQueue();
emailQueue.setBizType(EmailQueue.BizType.EMAIL_VERIFY);
emailQueue.setBizId(user.getId());
emailQueue.setSendAt(Time.now());
emailQueueService.create(emailQueue);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void emailVerifyCallback(String key) {
User user = getLoginUser();
if (user.emailVerified()) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.email.verified");
}
Long userId = redisUserEmailVerify.get(key);
if (userId == null) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("user.email.illegal_key");
}
if (!userId.equals(user.getId())) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("user.email.illegal_key");
}
redisUserEmailVerify.destroy(key);
user.setEmailVerifyAt(null);
update(user);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void updatePassword(String oldPassword, String newPassword) {
String token = TimiSpring.getToken();
User user = getLoginUser();
if (isInvalidPassword(user.getPassword(), oldPassword)) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("user.password.mismatch");
}
// 更新密码
user.setPassword(digestPassword(newPassword, Text.randomString(16)));
user.setUpdatedAt(Time.now());
mapper.update(user);
// 清除登录会话
userToken.clear(token);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void sendPasswordForgetVerify(String user) {
User result = get(user);
if (result == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("user.password.forget.not_found");
}
if (TimiJava.isEmpty(result.getEmail()) || !result.emailVerified()) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("user.password.forget.not_valid_email");
}
EmailQueue emailQueue = new EmailQueue();
emailQueue.setBizType(EmailQueue.BizType.RESET_PASSWORD);
emailQueue.setBizId(result.getId());
emailQueue.setSendAt(Time.now());
emailQueueService.create(emailQueue);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void resetPasswordByKey(String key, String password) {
Long userId = redisUserResetPWVerify.get(key);
if (userId == null) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("user.password.forget.illegal_key");
}
User user = get(userId);
redisUserResetPWVerify.destroy(key);
user.setPassword(digestPassword(password, Text.randomString(16)));
user.setUpdatedAt(Time.now());
mapper.update(user);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void cancel(String password) {
String token = TimiSpring.getToken();
User user = getLoginUser();
if (isInvalidPassword(user.getPassword(), password)) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("user.cancel.password_error");
}
// 删除评论
commentMapper.deleteByUserId(user.getId());
{
// 删除回复
CommentReply example = new CommentReply();
example.setSenderId(user.getId());
example.setReceiverId(user.getId());
commentReplyMapper.deleteAllByExample(example, Logic.OR);
}
// 删除账号
delete(user.getId());
// 清除登录会话
userToken.clear(token);
}
private void updateExp(Long userId) {
if (!redisUserExpFlag.has(userId)) {
// 当天无登录标记,加经验
UserProfile profile = profileService.get(userId);
profile.setExp(profile.getExp() + 2);
redisUserExpFlag.set(profile.getUserId(), "", Time.tomorrow() - Time.now());
profileService.update(profile);
}
}
/**
* 生成密码摘要
*
* @param password 原始密码
* @param salt 盐值
* @return 密码摘要
*/
private String digestPassword(String password, String salt) {
try {
return "$SHA$%s$%s".formatted(salt, Digest.sha256(Digest.sha256(password) + salt));
} catch (Exception e) {
log.error("digest password error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO digest password error");
}
}
}
@@ -1,25 +0,0 @@
package com.imyeyu.api.modules.common.service.implement;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.modules.common.entity.Version;
import com.imyeyu.api.modules.common.mapper.VersionMapper;
import com.imyeyu.api.modules.common.service.VersionService;
import org.springframework.stereotype.Service;
/**
* 版本服务实现
*
* @author 夜雨
* @since 2021-06-10 16:07
*/
@Service
@RequiredArgsConstructor
public class VersionServiceImplement implements VersionService {
private final VersionMapper mapper;
@Override
public Version getByName(String name) {
return mapper.queryByName(name);
}
}