Compare commits

...

30 Commits

Author SHA1 Message Date
fa11b388db refactor TempFileService, use Attachment and mongodb 2026-01-05 14:35:38 +08:00
b6eb7980e4 remove AttachmentRequest and AttachmentView 2026-01-04 19:17:42 +08:00
3410c540ab remove Attachment.ext 2026-01-04 19:13:02 +08:00
040a46934d update BaseMapper list rename to select 2026-01-04 19:12:47 +08:00
7eb409f6d0 add MALL travel location type 2025-12-23 14:46:21 +08:00
7b3d4e2a65 nullable ttl arg for temp file 2025-12-22 11:01:21 +08:00
4e113e8fae update firstNotNull 2025-12-22 11:00:52 +08:00
7a34a481aa add ttl arg for tempFileUpload 2025-12-19 20:15:48 +08:00
a402ea86ef fix circular dependency 2025-12-19 19:47:45 +08:00
3eaa560aec sync travel updatedAt 2025-12-18 21:09:21 +08:00
e61da7d8f9 fix AttachmentService.createMedia fail 2025-12-18 14:46:10 +08:00
355b192071 page transportationType and days 2025-12-18 14:45:50 +08:00
c732f7cead add FOOD,PLAY,LIFE and remove RESTAURANT 2025-12-18 14:45:37 +08:00
812e33693b nullable TravelLocation.score and importance 2025-12-18 14:44:58 +08:00
a7b490240c fix fileName for temp file download 2025-12-18 14:44:18 +08:00
959fc30758 remove travelLocation.firstTraveledAt,lastTraveledAt,travelCount 2025-12-17 10:21:42 +08:00
fe86a84204 fix travel location update fail 2025-12-17 10:21:16 +08:00
42bcfcf70e add db.sql 2025-12-15 10:30:55 +08:00
d82c5200e7 refactor travel 2025-12-15 10:30:45 +08:00
658765df6f refactor travel 2025-12-13 18:45:50 +08:00
d12c76fe03 add Attachment.mimeType,metadata 2025-12-11 18:41:29 +08:00
2262eabe07 fix JournalController.delete 2025-12-10 15:37:48 +08:00
81c1a14228 add JournalController.listByIds 2025-12-10 13:37:44 +08:00
fdfb0439d9 update Journal 2025-12-09 17:43:54 +08:00
e2a3c193d8 remove pageByBizId and add deleteByBizId 2025-12-09 17:43:27 +08:00
40760ed517 fix GsonHandler dbConfig 2025-12-08 16:59:11 +08:00
6839dafdb9 update Page.size to long 2025-12-08 16:58:42 +08:00
bc2c920705 update timi-lang 2025-12-08 16:58:13 +08:00
59b0153f3e fix not match key exception code 2025-12-08 16:34:13 +08:00
ffaf7f84b4 upper GsonHandler.java to timi-spring 2025-12-05 15:33:29 +08:00
77 changed files with 2152 additions and 1114 deletions

2
.gitignore vendored
View File

@ -1,8 +1,10 @@
/config
/data
/docs
/logs
/target
/temp
CLAUDE.md
multilingualField/
!.mvn/wrapper/maven-wrapper.jar

16
pom.xml
View File

@ -39,6 +39,20 @@
<properties>
<native.classifier>windows-x86_64</native.classifier>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>7.1.1-1.5.12</version>
<classifier>${native.classifier}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>4.11.0-1.5.12</version>
<classifier>${native.classifier}</classifier>
</dependency>
</dependencies>
</profile>
<profile>
<id>prod-linux</id>
@ -227,8 +241,6 @@
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>7.1.1-1.5.12</version>
<scope>provided</scope>
<classifier>windows-x86_64</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>

View File

@ -40,13 +40,13 @@ public class TimiServerAPI implements OS.FileSystem, ApplicationContextAware {
TimiServerAPI.applicationContext = applicationContext;
}
public static Language getUserLanguage() {
Language userLanguage = TimiSpring.getLanguage();
public static Language.Enum getUserLanguage() {
Language.Enum userLanguage = TimiSpring.getLanguage();
Environment env = applicationContext.getBean(Environment.class);
if (env.containsProperty(DEV_LANG_CONFIG)) {
String property = env.getProperty(DEV_LANG_CONFIG);
if (TimiJava.isNotEmpty(property)) {
userLanguage = Ref.toType(Language.class, property);
userLanguage = Ref.toType(Language.Enum.class, property);
}
}
return userLanguage;

View File

@ -0,0 +1,29 @@
package com.imyeyu.api.bean;
import com.imyeyu.spring.bean.Page;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author 夜雨
* @since 2025-12-12 23:07
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PreviewPage<T> extends Page<T> {
/**
*
*
* @author 夜雨
* @since 2025-12-12 23:09
*/
public enum Type {
NORMAL,
PREVIEW
}
protected Type type;
}

View File

@ -78,7 +78,7 @@ public class GiteaDBConfig {
"com.imyeyu.api.modules.gitea.entity",
};
String[] typeHandlers = {
"com.imyeyu.api.handler"
"com.imyeyu.spring.handler"
};
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();

View File

@ -81,6 +81,7 @@ public class TimiServerDBConfig {
mapperLocations.add("classpath:mapper/blog/**/*.xml");
mapperLocations.add("classpath:mapper/common/**/*.xml");
mapperLocations.add("classpath:mapper/system/**/*.xml");
mapperLocations.add("classpath:mapper/journal/**/*.xml");
mapperLocations.add("classpath:mapper/minecraft/**/*.xml");
for (int i = 0; i < mapperLocations.size(); i++) {
resources.addAll(List.of(resourceResolver.getResources(mapperLocations.get(i))));
@ -98,7 +99,7 @@ public class TimiServerDBConfig {
"com.imyeyu.api.modules.minecraft.entity"
};
String[] typeHandlers = {
"com.imyeyu.api.handler"
"com.imyeyu.spring.handler"
};
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();

View File

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

View File

@ -19,11 +19,11 @@ public interface ArticleMapper extends BaseMapper<Article, Long> {
long countByPage(Page page);
@Override
List<Article> listByPage(Page page);
List<Article> selectByPage(Page page);
long countByKeyword(String keyword);
List<Article> selectByKeyword(String keyword, Long offset, int limit);
List<Article> selectByKeyword(String keyword, Long offset, long limit);
@Select("UPDATE `article` SET `likes` = `likes` + 1 WHERE `id` = #{articleId}")
void like(Long articleId);

View File

@ -58,7 +58,7 @@ public class ArticleServiceImplement extends AbstractEntityService<Article, Long
@Override
public PageResult<Article> page(Page page) {
PageResult<Article> result = new PageResult<>();
result.setList(mapper.listByPage(page));
result.setList(mapper.selectByPage(page));
result.setTotal(mapper.countByPage(page));
return result;
}

View File

@ -1,7 +1,5 @@
package com.imyeyu.api.modules.common.bean;
import lombok.Data;
/**
* @author 夜雨
* @since 2025-10-20 15:04
@ -18,22 +16,4 @@ public class MediaAttach {
THUMB
}
/**
*
*
* @author 夜雨
* @since 2025-10-20 15:04
*/
@Data
public static class ExtData {
private Long sourceId;
private String sourceMongoId;
private boolean isImage;
private boolean isVideo;
}
}

View File

@ -0,0 +1,42 @@
package com.imyeyu.api.modules.common.bean;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author 夜雨
* @since 2025-12-11 18:14
*/
public class Metadata {
/**
* 图片
*
* @author 夜雨
* @since 2025-12-11 18:15
*/
@Data
public static class Image {
private int width;
private int height;
}
/**
* 缩略图
*
* @author 夜雨
* @since 2026-01-04 18:10
*/
@Data
@EqualsAndHashCode(callSuper = true)
public static class ThumbImage extends Image {
private long sourceId;
private String sourceMongoId;
private String sourceMimeType;
}
}

View File

@ -131,7 +131,7 @@ public enum SettingKey {
MUSIC_CONTROLLER_URI,
// ---------- ----------
// ---------- 日记 ----------
JOURNAL_KEY,
@ -139,7 +139,22 @@ public enum SettingKey {
JOURNAL_APP_SECRET,
JOURNAL_TRAVEL,
// ---------- 临时文件 ----------
/** 临时文件最小缓存时间 */
TEMP_FILE_TTL_MIN,
/** 临时文件最长缓存时间 */
TEMP_FILE_TTL_MAX,
/** 临时文件默认缓存时间 */
TEMP_FILE_TTL_DEFAULT,
/** 已过期的临时文件保留时间 */
TEMP_FILE_RESIDUE_TIME,
/** 每个 IP 限制有效临时文件容量 */
TEMP_FILE_LIMIT,
// ---------- 系统 ----------

View File

@ -1,23 +0,0 @@
package com.imyeyu.api.modules.common.bean;
import lombok.Data;
import java.nio.file.Path;
/**
* @author 夜雨
* @since 2025-09-27 01:47
*/
@Data
public class TempFileMetaData {
private String id;
private String name;
private String originalName;
private Path path;
private Long lastAccessAt;
}

View File

@ -19,7 +19,6 @@ import com.imyeyu.api.modules.common.service.TemplateService;
import com.imyeyu.api.modules.common.service.VersionService;
import com.imyeyu.api.modules.common.vo.FeedbackRequest;
import com.imyeyu.api.modules.common.vo.TempFileResponse;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentView;
import com.imyeyu.api.modules.system.util.ResourceHandler;
import com.imyeyu.api.util.CaptchaManager;
import com.imyeyu.io.IO;
@ -63,12 +62,8 @@ import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -263,8 +258,8 @@ public class CommonController {
@AOPLog
@RequestRateLimit
@GetMapping("/attachment/{mongoId}")
public AttachmentView getAttachment(@PathVariable String mongoId) {
return attachmentService.viewByMongoId(mongoId);
public Attachment getAttachment(@PathVariable String mongoId) {
return attachmentService.getByMongoId(mongoId);
}
@AOPLog
@ -376,67 +371,87 @@ public class CommonController {
}
}
/**
* 上传临时文件
*
* @param files 文件列表
* @param ttl 缓存时长(毫秒),最长 3 天259200000ms
* @return 临时文件响应列表
*/
@AOPLog
@PostMapping("/temp/file/upload")
public List<TempFileResponse> uploadFile(@RequestParam("file") List<MultipartFile> files) {
return tempFileService.store(files);
public List<TempFileResponse> tempFileUpload(@RequestParam("file") List<MultipartFile> files, @RequestParam(value = "ttl", required = false) Long ttl) {
return tempFileService.store(files, ttl);
}
/**
* 读取临时文件
*
* @param mongoId mongoId
* @param req 请求
* @param resp 返回
*/
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@GetMapping("/temp/file/read/{fileId}")
public void tempFileRead(@PathVariable String fileId, HttpServletRequest req, HttpServletResponse resp) {
@GetMapping("/temp/file/read/{mongoId}")
public void tempFileRead(@PathVariable String mongoId, HttpServletRequest req, HttpServletResponse resp) {
try {
File file = tempFileService.get(fileId);
if (TimiJava.isEmpty(file) && file.exists()) {
Attachment attach = attachmentService.getByMongoId(mongoId);
if (TimiJava.isEmpty(attach)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Path filePath = file.toPath();
resp.setContentLengthLong(Files.size(filePath));
String mimeType = new Tika().detect(filePath);
if (TimiJava.isNotEmpty(mimeType)) {
resp.setContentType(mimeType);
}
req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.FILE);
req.setAttribute(ResourceHandler.ATTR_VALUE, filePath);
GridFSFile file = attachmentService.readByMongoId(attach.getMongoId());
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
GridFsResource gridFsResource = new GridFsResource(file, downloadStream);
req.setAttribute(ResourceHandler.ATTR_TYPE, ResourceHandler.Type.MONGO);
req.setAttribute(ResourceHandler.ATTR_VALUE, gridFsResource);
resp.setContentType(attach.getMimeType());
resourceHandler.handleRequest(req, resp);
} catch (Exception e) {
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
/**
* 下载临时文件
*
* @param mongoId
* @param resp
*/
@AOPLog
@RequestRateLimit
@IgnoreGlobalReturn
@RequestMapping("/temp/file/download/{fileId}")
public void tempFileDownload(@PathVariable String fileId, HttpServletResponse resp) {
@RequestMapping("/temp/file/download/{mongoId}")
public void tempFileDownload(@PathVariable String mongoId, HttpServletResponse resp) {
try {
File file = tempFileService.get(fileId);
if (TimiJava.isEmpty(file) && file.exists()) {
Attachment attach = attachmentService.getByMongoId(mongoId);
if (TimiJava.isEmpty(attach)) {
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
String mimeType = new Tika().detect(file);
resp.setContentType(mimeType);
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(file.getName()));
resp.setContentType(attach.getMimeType());
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
resp.setHeader("Accept-Ranges", "bytes");
RequestRange range = TimiSpring.requestRange(file.length());
GridFSFile file = attachmentService.readByMongoId(mongoId);
@Cleanup
GridFSDownloadStream downloadStream = gridFSBucket.openDownloadStream(file.getObjectId());
RequestRange range = TimiSpring.requestRange(attach.getSize());
if (range == null) {
// 完整文件
resp.setContentLengthLong(file.length());
resp.setContentLengthLong(attach.getSize());
resp.setStatus(HttpServletResponse.SC_OK);
IO.toOutputStream(resp.getOutputStream(), file);
IO.toOutputStream(downloadStream, resp.getOutputStream());
} else {
// 分片文件
resp.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
resp.setHeader("Content-Range", "bytes %s-%s/%s".formatted(range.getStart(), range.getEnd(), file.length()));
resp.setHeader("Content-Range", "bytes %s-%s/%s".formatted(range.getStart(), range.getEnd(), attach.getSize()));
resp.setContentLengthLong(range.getLength());
@Cleanup RandomAccessFile raf = new RandomAccessFile(file, "r");
raf.seek(range.getStart());
IO.toOutputStream(resp.getOutputStream(), raf, range.getStart(), range.getLength());
IO.toOutputStream(downloadStream, resp.getOutputStream(), range.getStart(), range.getEnd());
resp.flushBuffer();
}
} catch (Exception e) {
log.error("download error", e);

View File

@ -1,13 +1,19 @@
package com.imyeyu.api.modules.common.entity;
import com.google.gson.JsonObject;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.bean.MultilingualHandler;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.io.InputStream;
/**
* @author 夜雨
* @since 2023-08-15 10:17
@ -44,14 +50,20 @@ public class Attachment extends Entity implements MultilingualHandler {
/** 镜像 */
MIRROR,
/** 日记 */
JOURNAL,
/** 日记出行 */
JOURNAL_TRAVEL,
/** 日记瞬间 */
JOURNAL_MOMENT,
/** 系统 */
SYSTEM
SYSTEM,
/** 临时文件 */
TEMP_FILE
}
protected BizType bizType;
@ -67,14 +79,28 @@ public class Attachment extends Entity implements MultilingualHandler {
protected String name;
protected String mimeType;
protected JsonObject metadata;
protected Long size;
protected String md5;
protected String ext;
protected String uploaderIp;
protected Boolean isDestroyed;
protected Long destroyAt;
@Transient
protected InputStream inputStream;
public InputStream openInputStream() {
AttachmentService service = TimiServerAPI.applicationContext.getBean(AttachmentService.class);
return service.getInputStreamByMongoId(mongoId);
}
public void setAttachTypeValue(Enum<?> attachType) {
this.attachType = attachType.toString();
}
@ -82,5 +108,4 @@ public class Attachment extends Entity implements MultilingualHandler {
public <T extends Enum<T>> T getAttachTypeValue(Class<T> attachTypeClass) {
return Ref.toType(attachTypeClass, attachType);
}
}

View File

@ -1,14 +1,14 @@
package com.imyeyu.api.modules.common.entity;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.spring.entity.Entity;
import com.imyeyu.java.bean.Language;
import com.imyeyu.spring.entity.Creatable;
import com.imyeyu.spring.entity.Deletable;
import com.imyeyu.spring.entity.IDEntity;
import com.imyeyu.spring.entity.Updatable;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.lang.reflect.Field;
import java.io.Serializable;
/**
* @author 夜雨
@ -16,52 +16,13 @@ import java.lang.reflect.Field;
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Multilingual extends Entity {
public class Multilingual extends Language implements Serializable, IDEntity<Long>, Creatable, Updatable, Deletable {
protected String key;
protected Long id;
protected String zhCN;
protected Long createdAt;
protected String zhTW;
protected Long updatedAt;
protected String enUS;
protected String ruRU;
protected String koKR;
protected String jaJP;
protected String deDE;
/** @return 根据用户环境获取语言值 */
public String getValue() {
try {
Field field = Ref.getField(getClass(), TimiServerAPI.getUserLanguage().toString().replace("_", ""));
if (field == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language");
}
return Ref.getFieldValue(this, field, String.class);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
/**
* 获取指定语言值
*
* @param language 指定语言
* @return 值
*/
public String getValue(com.imyeyu.java.bean.Language language) {
try {
Field field = Ref.getField(getClass(), language.toString().replace("_", ""));
if (field == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not support language");
}
return Ref.getFieldValue(this, field, String.class);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
protected Long deletedAt;
}

View File

@ -3,6 +3,7 @@ package com.imyeyu.api.modules.common.mapper;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.mapper.RawMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@ -11,10 +12,10 @@ import java.util.List;
* @author 夜雨
* @since 2023-08-15 10:22
*/
public interface AttachmentMapper extends BaseMapper<Attachment, Long> {
public interface AttachmentMapper extends BaseMapper<Attachment, Long>, RawMapper<Attachment, Long> {
/** 有效条件,非删除和销毁 */
String VALID = NOT_DELETE + " AND destroy_at IS NULL";
String VALID = NOT_DELETE + " AND (`destroy_at` IS NULL OR " + UNIX_TIME + " < `destroy_at`)";
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} " + VALID + LIMIT_1)
Attachment selectByBizId(Attachment.BizType bizType, long bizId);
@ -25,9 +26,12 @@ public interface AttachmentMapper extends BaseMapper<Attachment, Long> {
@Select("SELECT * FROM attachment WHERE biz_type = #{bizType} AND biz_id = #{bizId} AND attach_type = #{attachType} " + VALID + LIMIT_1)
Attachment selectByAttachType(Attachment.BizType bizType, long bizId, Enum<?> attachType);
List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, Page page);
List<Attachment> listByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, Page<Attachment> page);
long countByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes);
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypes, List<String> md5s);
@Select("SELECT * FROM attachment WHERE `is_destroyed` = FALSE AND `destroy_at` < " + UNIX_TIME)
List<Attachment> selectNeedDestroy();
}

View File

@ -22,7 +22,7 @@ public interface CommentMapper extends BaseMapper<Comment, Long> {
long countByPage(Page page);
@Override
List<Comment> listByPage(Page page);
List<Comment> selectByPage(Page page);
@Select("SELECT * FROM comment WHERE id = #{id}" + NOT_DELETE)
Comment select(Long id);
@ -36,11 +36,11 @@ public interface CommentMapper extends BaseMapper<Comment, Long> {
long countAll(Comment.BizType bizType, Long bizId);
List<CommentView> list(Comment.BizType bizType, Long bizId, Long offset, int limit, LinkedHashMap<String, OrderType> orderMap);
List<CommentView> list(Comment.BizType bizType, Long bizId, Long offset, long limit, LinkedHashMap<String, OrderType> orderMap);
long countByUserId(Long userId);
List<CommentView> listByUserId(Long userId, Long offset, int limit, LinkedHashMap<String, OrderType> orderMap);
List<CommentView> listByUserId(Long userId, Long offset, long limit, LinkedHashMap<String, OrderType> orderMap);
@Update("UPDATE comment SET deleted_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE user_id = #{userId} ")
void deleteByUserId(Long userId);

View File

@ -25,7 +25,7 @@ public interface CommentReplyMapper extends BaseMapper<CommentReply, Long> {
long countByBizType(CommentReplyPage.BizType bizType, Long bizId);
@Select("SELECT * FROM comment_reply WHERE ${bizType.column} = #{bizId} AND ignored_at IS NULL" + NOT_DELETE + PAGE)
List<CommentReplyView> listByBizType(CommentReplyPage.BizType bizType, Long bizId, Long offset, int limit);
List<CommentReplyView> listByBizType(CommentReplyPage.BizType bizType, Long bizId, Long offset, long limit);
@Update("UPDATE comment_reply SET deleted_at = " + UNIX_TIME + " WHERE sender_id = #{userId} OR receiver_id = #{userId}")
void deleteByUserId(Long userId);

View File

@ -21,15 +21,15 @@ public interface IconMapper extends BaseMapper<Icon, Long> {
long countByName(String name);
@Select("SELECT * FROM icon WHERE name LIKE CONCAT('%', #{name}, '%')" + PAGE)
List<Icon> listByName(String name, long offset, int limit);
List<Icon> listByName(String name, long offset, long limit);
long countByLabel(String lang, String label);
List<Icon> listByLabel(String lang, String label, long offset, int limit);
List<Icon> listByLabel(String lang, String label, long offset, long limit);
@Select("SELECT COUNT(1) FROM icon WHERE unicode = #{unicode}" + NOT_DELETE)
long countByUnicode(String unicode);
@Select("SELECT * FROM icon WHERE unicode = #{unicode}" + PAGE)
List<Icon> listByUnicode(String unicode, long offset, int limit);
List<Icon> listByUnicode(String unicode, long offset, long limit);
}

View File

@ -1,16 +1,8 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentView;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.DeletableService;
import com.imyeyu.spring.service.DestroyableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.PageableService;
import com.imyeyu.spring.service.UpdatableService;
import com.imyeyu.spring.service.BaseService;
import com.mongodb.client.gridfs.model.GridFSFile;
import java.io.InputStream;
@ -24,22 +16,9 @@ import java.util.List;
* @author 夜雨
* @since 2023-08-15 10:21
*/
public interface AttachmentService extends GettableService<Attachment, Long>, PageableService<Attachment>, UpdatableService<Attachment>, DeletableService<Long>, DestroyableService<Long> {
public interface AttachmentService extends BaseService<Attachment, Long> {
/**
*
*
* @param request
*/
void create(AttachmentRequest request);
/**
* 创建媒体附件,同步创建缩略图
*
* @param request 附件请求
* @return 缩略图附件
*/
Attachment createMedia(AttachmentRequest request) throws TimiException;
Attachment createMedia(Attachment attachment);
void deleteMedia(Long thumbId) throws TimiException;
@ -49,8 +28,6 @@ public interface AttachmentService extends GettableService<Attachment, Long>, Pa
Attachment getByMongoId(String mongoId);
AttachmentView viewByMongoId(String mongoId);
GridFSFile readByMongoId(String mongoId);
InputStream getInputStreamByMongoId(String mongoId);
@ -79,7 +56,9 @@ public interface AttachmentService extends GettableService<Attachment, Long>, Pa
*/
long countByBizId(Attachment.BizType bizType, Long bizId, Enum<?> ...attachTypes);
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s) throws TimiException;
List<Attachment> listByMd5s(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, List<String> md5s);
PageResult<Attachment> pageByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, Page page) throws TimiException;
void deleteByBizId(Attachment.BizType bizType, long bizId, Enum<?> ...attachTypes);
List<Attachment> listNeedDestroy();
}

View File

@ -16,9 +16,9 @@ public interface MultilingualService extends UpdatableService<Multilingual> {
Long createIfNotExist(String key, String zhCN);
String get(Language language, Long id);
String get(Language.Enum language, Long id);
String getByKey(Language language, String key);
String getByKey(Language.Enum language, String key);
List<Multilingual> listByNotTranslate();
}

View File

@ -1,11 +1,8 @@
package com.imyeyu.api.modules.common.service;
import com.imyeyu.api.modules.common.bean.TempFileMetaData;
import com.imyeyu.api.modules.common.vo.TempFileResponse;
import com.imyeyu.java.bean.timi.TimiException;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.List;
/**
@ -14,9 +11,5 @@ import java.util.List;
*/
public interface TempFileService {
List<TempFileResponse> store(List<MultipartFile> files) throws TimiException;
File get(String id) throws TimiException;
TempFileMetaData metadata(String id) throws TimiException;
List<TempFileResponse> store(List<MultipartFile> files, Long ttl);
}

View File

@ -1,22 +1,21 @@
package com.imyeyu.api.modules.common.service.implement;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
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.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.attachment.AttachmentRequest;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentView;
import com.imyeyu.api.util.JavaCV;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.Network;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import com.imyeyu.utils.Time;
@ -26,13 +25,14 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import org.apache.tika.Tika;
import org.springframework.beans.BeanUtils;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.gridfs.GridFsTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
@ -66,44 +66,36 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void destroy(Long id) {
try {
Attachment attachment = get(id);
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attachment.getMongoId())));
attachment.setDeletedAt(Time.now());
attachment.setDestroyAt(Time.now());
mapper.update(attachment);
} catch (Exception e) {
log.error("delete mongo file error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO delete mongo file error");
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(AttachmentRequest request) {
TimiException.required(request.getBizType(), "not found request.bizType");
TimiException.required(request.getBizId(), "not found request.bizId");
TimiException.required(request.getName(), "not found request.name");
public void create(Attachment attachment) {
TimiException.required(attachment.getBizType(), "not found attachment.bizType");
TimiException.required(attachment.getName(), "not found attachment.name");
String mongoId = null;
try {
InputStream is = request.getInputStream();
TimiException.required(is, "not found request.inputStream");
TimiException.requiredTrue(is.available() != 0, "empty request.inputStream");
InputStream is = attachment.getInputStream();
TimiException.required(is, "not found attachment.inputStream");
StringBuilder mongoName = new StringBuilder(request.getBizType().toString());
if (TimiJava.isNotEmpty(request.getAttachType())) {
mongoName.append("_").append(request.getAttachType().toUpperCase()).append("_");
StringBuilder mongoName = new StringBuilder(attachment.getBizType().toString());
if (TimiJava.isNotEmpty(attachment.getAttachType())) {
mongoName.append("_").append(attachment.getAttachType().toUpperCase()).append("_");
}
mongoName.append(request.getName());
mongoName.append(attachment.getName());
mongoId = gridFsTemplate.store(request.getInputStream(), mongoName.toString()).toString();
mongoId = gridFsTemplate.store(attachment.getInputStream(), mongoName.toString()).toString();
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
request.setMongoId(mongoId);
request.setSize(gridFSFile.getLength());
request.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
mapper.insert(request);
attachment.setMongoId(mongoId);
attachment.setSize(gridFSFile.getLength());
attachment.setMd5(IO.md5(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setMimeType(new Tika().detect(gridFSBucket.openDownloadStream(gridFSFile.getObjectId())));
attachment.setUploaderIp(TimiSpring.getRequestIP());
attachment.setIsDestroyed(false);
if (attachment.getMimeType().startsWith("image")) {
BufferedImage image = ImageIO.read(gridFSBucket.openDownloadStream(gridFSFile.getObjectId()));
attachment.setMetadata(TimiJava.defaultIfNull(attachment.getMetadata(), new JsonObject()));
JsonObject metadata = attachment.getMetadata();
metadata.addProperty("width", image.getWidth());
metadata.addProperty("height", image.getHeight());
}
mapper.insert(attachment);
} catch (Exception e) {
if (mongoId != null) {
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(mongoId)));
@ -115,53 +107,66 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public Attachment createMedia(AttachmentRequest request) throws TimiException {
TimiException.required(request.getName(), "not found name");
TimiException.required(request.getBizType(), "not found bizType");
TimiException.required(request.getInputStream(), "not found inputStream");
public void destroy(Long id) {
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 (!attach.isDeleted()) {
attach.setDeletedAt(Time.now());
}
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");
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public Attachment createMedia(Attachment attachment) throws TimiException {
try {
// 储存源文件
request.setAttachTypeValue(MediaAttach.Type.SOURCE);
create(request);
attachment.setAttachTypeValue(MediaAttach.Type.SOURCE);
create(attachment);
// 生成缩略图
InputStream mimeStream = getInputStreamByMongoId(request.getMongoId());
InputStream mimeStream = attachment.openInputStream();
String mimeType = new Tika().detect(mimeStream);
boolean isImage = false;
InputStream sourceStream = getInputStreamByMongoId(request.getMongoId());
InputStream sourceStream = attachment.openInputStream();
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
switch (mimeType) {
case "image/png", "image/bmp", "image/jpeg" -> {
isImage = true;
Thumbnails.of(sourceStream).width(256).keepAspectRatio(true).toOutputStream(thumbStream);
}
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: {}", request.getName());
log.info("capturing thumbnail: {}", attachment.getName());
long start = Time.now();
File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID().toString(), request.getName()));
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", request.getName(), Time.now() - start);
log.info("captured thumbnail: {} at {} ms", attachment.getName(), Time.now() - start);
} finally {
IO.destroy(tempFile);
}
}
}
MediaAttach.ExtData extData = new MediaAttach.ExtData();
extData.setImage(isImage);
extData.setVideo(!isImage);
extData.setSourceId(request.getId());
extData.setSourceMongoId(request.getMongoId());
Metadata.ThumbImage thumbMetadata = new Metadata.ThumbImage();
thumbMetadata.setSourceId(attachment.getId());
thumbMetadata.setSourceMongoId(attachment.getMongoId());
thumbMetadata.setSourceMimeType(mimeType);
AttachmentRequest thumbAttach = new AttachmentRequest();
thumbAttach.setName(Network.simpleURIFileName(request.getName()) + ".png");
thumbAttach.setBizType(request.getBizType());
thumbAttach.setBizId(request.getBizId());
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.setExt(gson.toJson(extData));
thumbAttach.setMetadata(gson.toJsonTree(thumbMetadata).getAsJsonObject());
thumbAttach.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray()));
create(thumbAttach);
@ -177,8 +182,8 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
public void deleteMedia(Long thumbId) throws TimiException {
Attachment attachment = get(thumbId);
delete(attachment.getId());
MediaAttach.ExtData data = gson.fromJson(attachment.getExt(), MediaAttach.ExtData.class);
delete(data.getSourceId());
Metadata.ThumbImage thumbMetadata = gson.fromJson(attachment.getMetadata(), Metadata.ThumbImage.class);
delete(thumbMetadata.getSourceId());
}
@Override
@ -196,22 +201,11 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
return mapper.selectByMongoId(mongoId);
}
@Override
public AttachmentView viewByMongoId(String mongoId) {
Attachment attachment = getByMongoId(mongoId);
if (attachment == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("TODO not found attachment");
}
AttachmentView view = new AttachmentView();
BeanUtils.copyProperties(attachment, view);
return view;
}
@Override
public GridFSFile readByMongoId(String mongoId) {
Attachment view = mapper.selectByMongoId(mongoId);
TimiException.required(view, "not found attachment: %s".formatted(mongoId));
return gridFsTemplate.findOne(new Query(Criteria.where("_id").is(view.getMongoId())));
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
@ -222,7 +216,7 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
@Override
public byte[] readAllByMongoId(String mongoId) {
try {
return IO.toBytes(getInputStreamByMongoId(mongoId));
return IO.toBytes(getByMongoId(mongoId).openInputStream());
} catch (IOException e) {
throw new TimiException(TimiCode.ERROR, "TODO 读取失败");
}
@ -244,10 +238,22 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
}
@Override
public PageResult<Attachment> pageByBizId(Attachment.BizType bizType, Long bizId, List<Enum<?>> attachTypeList, Page page) throws TimiException {
PageResult<Attachment> result = new PageResult<>();
result.setList(mapper.listByBizId(bizType, bizId, attachTypeList, page));
result.setTotal(mapper.countByBizId(bizType, bizId, attachTypeList));
return result;
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);
}
}
}
@Override
public List<Attachment> listNeedDestroy() {
return mapper.selectNeedDestroy();
}
}

View File

@ -59,7 +59,7 @@ public class MultilingualServiceImplement extends AbstractEntityService<Multilin
}
@Override
public String get(Language language, Long id) {
public String get(Language.Enum language, Long id) {
Multilingual result = redisLanguage.get(id);
if (result == null) {
result = mapper.select(id);
@ -72,7 +72,7 @@ public class MultilingualServiceImplement extends AbstractEntityService<Multilin
}
@Override
public String getByKey(Language language, String key) {
public String getByKey(Language.Enum language, String key) {
Long languageId = redisLanguageMap.get(key);
if (languageId == null) {
Multilingual result = mapper.selectByKey(key);

View File

@ -1,36 +1,27 @@
package com.imyeyu.api.modules.common.service.implement;
import com.google.gson.Gson;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.bean.TempFileMetaData;
import com.imyeyu.api.modules.common.entity.Attachment;
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.io.IO;
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.network.Network;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.utils.Time;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author 夜雨
@ -41,56 +32,58 @@ import java.util.concurrent.ConcurrentHashMap;
@RequiredArgsConstructor
public class TempFileServiceImplement implements TempFileService {
private static final Long TTL = Time.H * 6;
private static final Long LIMIT = IOSize.GB * 10;
private final Gson gson;
private final SettingService settingService;
private final AttachmentService attachmentService;
private Path storagePath;
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);
private final Map<String, TempFileMetaData> metadataMap = new ConcurrentHashMap<>();
private final Map<String, Long> usageMap = new ConcurrentHashMap<>();
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));
@PostConstruct
public void init() throws IOException {
storagePath = Paths.get(settingService.getAsString(SettingKey.TEMP_FILE_PATH));
IO.destroy(storagePath.toFile());
Files.createDirectories(storagePath);
}
ttl = TimiJava.defaultIfNull(ttl, defaultTTL);
TimiException.requiredTrue(minTTL < ttl && ttl <= maxTTL, String.format("ttl must be between %s and %s", ttlMinStr, ttlMaxStr));
public List<TempFileResponse> store(List<MultipartFile> files) throws TimiException {
long newFileSize = files.stream().mapToLong(MultipartFile::getSize).sum();
long currentUsage = usageMap.getOrDefault(TimiSpring.getRequestIP(), 0L);
TimiException.requiredTrue(currentUsage + newFileSize < LIMIT, "out of storage limit(10 GB)");
{
// 缓存池大小限制
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));
}
try {
List<TempFileResponse> 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);
String fileId = UUID.randomUUID().toString();
String fileName = fileId;
if (TimiJava.isNotEmpty(file.getOriginalFilename())) {
fileName = fileId + "." + Network.uriFileExtension(file.getOriginalFilename());
}
Path filePath = storagePath.resolve(fileName);
Files.copy(file.getInputStream(), filePath);
// 创建元数据
TempFileMetaData metadata = new TempFileMetaData();
metadata.setId(fileId);
metadata.setPath(filePath);
metadata.setName(fileName);
metadata.setOriginalName(file.getOriginalFilename());
metadata.setLastAccessAt(Time.now());
metadataMap.put(metadata.getId(), metadata);
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(metadata.getId());
resp.setExpireAt(metadata.getLastAccessAt() + TTL);
usageMap.put(TimiSpring.getRequestIP(), currentUsage + newFileSize);
resp.setId(attach.getMongoId());
resp.setExpireAt(deletedAt);
result.add(resp);
}
return result;
@ -100,33 +93,20 @@ public class TempFileServiceImplement implements TempFileService {
}
}
@Override
public File get(String id) throws TimiException {
TempFileMetaData metaData = metadataMap.get(id);
TimiException.required(metaData, "not found temp file");
metaData.setLastAccessAt(Time.now());
return metaData.getPath().toFile();
}
@Override
public TempFileMetaData metadata(String id) throws TimiException {
return metadataMap.get(id);
}
@Scheduled(fixedRate = 3600000)
public void cleanup() {
List<String> expiredIds = metadataMap.values()
.stream()
.filter(metadata -> metadata.getLastAccessAt() + TTL < Time.now())
.map(TempFileMetaData::getId)
.toList();
for (int i = 0; i < expiredIds.size(); i++) {
TempFileMetaData removed = metadataMap.remove(expiredIds.get(i));
if (TimiJava.isNotEmpty(removed)) {
File file = removed.getPath().toFile();
IO.destroy(file);
usageMap.computeIfPresent(TimiSpring.getRequestIP(), (ip, usage) -> usage - file.length());
}
}
// List<String> expiredIds = metadataMap.values()
// .stream()
// .filter(metadata -> metadata.getLastAccessAt() + metadata.getTtl() < Time.now())
// .map(TempFileMetaData::getId)
// .toList();
// for (int i = 0; i < expiredIds.size(); i++) {
// TempFileMetaData removed = metadataMap.remove(expiredIds.get(i));
// if (TimiJava.isNotEmpty(removed)) {
// File file = removed.getPath().toFile();
// IO.destroy(file);
// usageMap.computeIfPresent(TimiSpring.getRequestIP(), (ip, usage) -> usage - file.length());
// }
// }
}
}

View File

@ -1,9 +1,5 @@
package com.imyeyu.api.modules.common.service.implement;
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.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.User;
@ -12,8 +8,11 @@ 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.attachment.AttachmentRequest;
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;
@ -73,7 +72,7 @@ public class UserProfileServiceImplement extends AbstractEntityService<UserProfi
if (IOSize.MB < bytes.length) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("限制上传文件大小 1 MB");
}
AttachmentRequest wrapperAttach = new AttachmentRequest();
Attachment wrapperAttach = new Attachment();
wrapperAttach.setName(request.getId() + ".png");
wrapperAttach.setBizType(Attachment.BizType.USER);
wrapperAttach.setBizId(request.getId());
@ -88,7 +87,7 @@ public class UserProfileServiceImplement extends AbstractEntityService<UserProfi
attachmentService.delete(dbAvatar.getId());
}
MultipartFile avatar = request.getProfile().getAvatar();
AttachmentRequest avatarAttach = new AttachmentRequest();
Attachment avatarAttach = new Attachment();
avatarAttach.setName(request.getId() + ".png");
avatarAttach.setBizType(Attachment.BizType.USER);
avatarAttach.setBizId(request.getId());

View File

@ -0,0 +1,49 @@
package com.imyeyu.api.modules.common.task;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.util.List;
/**
* 附件清除定时任务。每天凌晨 1 点执行,清除已过期但未销毁的附件
*
* @author 夜雨
* @since 2026-01-05 12:37
*/
@Slf4j
@Configuration
@EnableScheduling
@RequiredArgsConstructor
public class AttachmentClearTask {
private final AttachmentService attachmentService;
@Scheduled(cron = "0 0 1 * * ?")
public void run() {
log.info("start clear expired attachments");
try {
List<Attachment> needDestroyList = attachmentService.listNeedDestroy();
if (needDestroyList.isEmpty()) {
log.info("nothing attachment need clear");
return;
}
for (Attachment attach : needDestroyList) {
try {
attachmentService.destroy(attach.getId());
log.info("clear attachment success: id[{}]", attach.getId());
} catch (Exception e) {
log.error("clear attachment error: id[{}]", attach.getId(), e);
}
}
log.info("end clear expired attachments");
} catch (Exception e) {
log.error("attachment clear task error", e);
}
}
}

View File

@ -50,16 +50,16 @@ public class MultilingualTranslateTask {
@AllArgsConstructor
public enum BaiduLanguage {
ZH(Language.zh_CN),
EN(Language.en_US),
JP(Language.ja_JP),
KOR(Language.ko_KR),
RU(Language.ru_RU),
DE(Language.de_DE),
CHT(Language.zh_TW);
ZH(Language.Enum.zh_CN),
EN(Language.Enum.en_US),
JP(Language.Enum.ja_JP),
KOR(Language.Enum.ko_KR),
RU(Language.Enum.ru_RU),
DE(Language.Enum.de_DE),
CHT(Language.Enum.zh_TW);
/** 标准映射 */
final Language language;
final Language.Enum language;
/**
* 获取排除语言列表
@ -104,7 +104,7 @@ public class MultilingualTranslateTask {
Map<String, String> result = doTranslate(sb.toString(), languageList.get(j));
for (Map.Entry<String, String> item : result.entrySet()) {
Multilingual multilingual = cnMap.get(item.getKey());
Language lang = languageList.get(j).language;
Language.Enum lang = languageList.get(j).language;
String value = multilingual.getValue(lang);
if (TimiJava.isEmpty(value)) {
Ref.setFieldValue(multilingual, lang.toString().replace("_", ""), item.getValue());

View File

@ -1,21 +0,0 @@
package com.imyeyu.api.modules.common.vo.attachment;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.annotation.table.Transient;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.InputStream;
/**
* @author 夜雨
* @since 2024-02-21 10:58
*/
@Data
@Transient
@EqualsAndHashCode(callSuper = true)
public class AttachmentRequest extends Attachment {
private InputStream inputStream;
}

View File

@ -1,15 +0,0 @@
package com.imyeyu.api.modules.common.vo.attachment;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.bean.MultilingualHandler;
import com.imyeyu.api.modules.common.entity.Attachment;
/**
* @author 夜雨
* @since 2024-02-21 10:55
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class AttachmentView extends Attachment implements MultilingualHandler {
}

View File

@ -56,7 +56,7 @@ public class ServerServiceImplement implements ServerService {
public void report(ReportRequest request) {
ServerStatus status = serverMap.get(request.getId());
if (status != null) {
request.setBaseInfo(TimiJava.firstNotNull(request.getBaseInfo(), status.getBaseInfo()));
request.setBaseInfo(TimiJava.defaultIfNull(request.getBaseInfo(), status.getBaseInfo()));
if (request.getBaseInfo() == null || TimiJava.isEmpty(request.getBaseInfo().getCore())) {
// 需要报告基本信息,与调用方约定返回代码为 IGNORE
throw new TimiException(TimiCode.IGNORE);

View File

@ -31,5 +31,5 @@ public interface IssueMapper extends BaseMapper<Issue, Long> {
* @param limit 数据量
* @return 反馈列表
*/
List<Issue> listByIssuePage(@Param("issuePage") IssuePage issuePage, long offset, int limit);
List<Issue> listByIssuePage(@Param("issuePage") IssuePage issuePage, long offset, long limit);
}

View File

@ -19,5 +19,5 @@ public interface MergeMapper extends BaseMapper<Merge, Long> {
long countByMergePage(@Param("mergePage") MergePage mergePage);
List<Merge> listByMergePage(@Param("mergePage") MergePage mergePage, long offset, int limit);
List<Merge> listByMergePage(@Param("mergePage") MergePage mergePage, long offset, long limit);
}

View File

@ -16,7 +16,7 @@ public interface ReleaseMapper extends BaseMapper<Release, Long> {
@Select("SELECT COUNT(1) FROM git_release WHERE repository_id = #{repositoryId}" + NOT_DELETE)
long countByRepositoryId(long repositoryId);
List<ReleaseView> listByRepositoryId(long repositoryId, long offset, int limit);
List<ReleaseView> listByRepositoryId(long repositoryId, long offset, long limit);
@Select("SELECT * FROM git_release WHERE repository_id = #{repositoryId}" + NOT_DELETE + "ORDER BY created_at DESC" + LIMIT_1)
Release queryLatestByRepositoryId(long repositoryId);

View File

@ -1,10 +1,10 @@
package com.imyeyu.api.modules.git.vo.issue;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.spring.bean.Page;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.api.modules.common.entity.Comment;
import com.imyeyu.spring.bean.Page;
/**
* @author 夜雨
@ -13,7 +13,7 @@ import com.imyeyu.spring.bean.Page;
@Data
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class CommentPage extends Page {
public class CommentPage extends Page<Comment> {
private Comment.BizType bizType;

View File

@ -12,5 +12,5 @@ public interface ActionMapper {
long count(Long repoId, String branch, ActionLogDTO.Operation operation);
List<ActionLogDTO> list(Long repoId, String branch, ActionLogDTO.Operation operation, long offset, int limit);
List<ActionLogDTO> list(Long repoId, String branch, ActionLogDTO.Operation operation, long offset, long limit);
}

View File

@ -0,0 +1,30 @@
package com.imyeyu.api.modules.journal.bean;
import com.imyeyu.api.modules.journal.entity.Journal;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 经纬度位置
*
* @author 夜雨
* @since 2025-12-05 15:26
*/
@Data
@NoArgsConstructor
public class Location {
/** 经度 */
private Double lng;
/** 维度 */
private Double lat;
private String text;
public Location(Journal journal) {
this.lat = journal.getLat();
this.lng = journal.getLng();
this.text = journal.getLocation();
}
}

View File

@ -1,59 +0,0 @@
package com.imyeyu.api.modules.journal.bean;
import lombok.Data;
import java.util.List;
/**
* @author 夜雨
* @since 2025-09-30 11:51
*/
@Data
public class Travel {
private Luggage luggage;
private List<Guide> guides;
/**
*
*
* @author 夜雨
* @since 2025-09-30 11:52
*/
@Data
public static class Luggage {
private List<Item> gao;
private List<Item> yu;
/**
*
*
* @author 夜雨
* @since 2025-09-30 11:52
*/
@Data
public static class Item {
private String name;
private boolean isTaken;
}
}
/**
*
*
* @author 夜雨
* @since 2025-09-30 11:54
*/
@Data
public static class Guide {
private String title;
private List<String> images;
}
}

View File

@ -1,20 +1,18 @@
package com.imyeyu.api.modules.journal.controller;
import com.google.gson.Gson;
import com.imyeyu.api.bean.PreviewPage;
import com.imyeyu.api.bean.wechat.InitCodeResponse;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.SettingService;
import com.imyeyu.api.modules.journal.bean.Travel;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.api.modules.journal.service.JournalService;
import com.imyeyu.api.modules.journal.vo.AppendRequest;
import com.imyeyu.api.modules.journal.vo.ArchiveRequest;
import com.imyeyu.api.modules.journal.vo.JournalPage;
import com.imyeyu.api.modules.journal.vo.JournalRequest;
import com.imyeyu.api.modules.journal.vo.JournalResponse;
import com.imyeyu.api.modules.journal.vo.journal.ArchiveRequest;
import com.imyeyu.api.modules.journal.vo.journal.JournalRequest;
import com.imyeyu.api.modules.journal.vo.journal.JournalResponse;
import com.imyeyu.api.modules.journal.vo.journal.UpdateRequest;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.network.ArgMap;
@ -22,19 +20,25 @@ import com.imyeyu.network.GsonRequest;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
* 微信小程序糕雨日记接口
*
* @author 夜雨
* @since 2025-09-26 15:55
*/
@ -44,11 +48,16 @@ import java.util.List;
@RequestMapping("/journal")
public class JournalController {
private final Gson gson;
private final JournalService service;
private final SettingService settingService;
private final AttachmentService attachmentService;
/**
* 初始化用户 OpenId
*
* @param code 微信授权码
* @return OpenId
*/
@AOPLog
@RequestRateLimit
@PostMapping("/openid")
@ -67,6 +76,31 @@ public class JournalController {
}
}
@AOPLog
@RequestRateLimit
@RequestMapping("/{id}")
public JournalResponse detail(@PathVariable Long id) {
Journal journal = service.get(id);
JournalResponse resp = new JournalResponse();
Page<Attachment> attachPage = new Page<>();
{
Attachment example = new Attachment();
example.setBizType(Attachment.BizType.JOURNAL);
example.setBizId(journal.getId());
attachPage.setEqualsExample(example);
}
attachPage.setIndex(0);
attachPage.setSize(Long.MAX_VALUE);
resp.setItems(attachmentService.page(attachPage).getList());
BeanUtils.copyProperties(journal, resp);
return resp;
}
/**
* 创建记录
*
* @param request
*/
@AOPLog
@RequestRateLimit
@PostMapping("/create")
@ -74,28 +108,58 @@ public class JournalController {
service.create(request);
}
@PostMapping("/append")
public void append(@RequestBody AppendRequest request) {
service.appendItems(request);
/**
* 更新记录(支持附件差分保存)
*
* @param request 更新请求,包含 id、基本信息、保留的附件 ID 列表和新上传的临时文件 ID 列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody @Valid UpdateRequest request) {
service.update(request);
}
/**
* 删除记录
*
* @param id 记录 ID
*/
@AOPLog
@PostMapping("/delete")
public void delete(@RequestBody Long thumbId) {
attachmentService.deleteMedia(thumbId);
public void delete(@RequestSingleParam Long id) {
service.delete(id);
}
/**
* 记录列表
*
* @param page 查询页面
* @return
*/
@AOPLog
@RequestRateLimit
@RequestMapping("/list")
public PageResult<JournalResponse> list(@RequestBody JournalPage page) {
public PageResult<JournalResponse> list(@RequestBody PreviewPage<Journal> page) {
PageResult<Journal> pageResult = service.page(page);
PageResult<JournalResponse> result = new PageResult<>();
result.setTotal(pageResult.getTotal());
result.setList(pageResult.getList().stream().map(item -> {
JournalResponse resp = new JournalResponse();
resp.setItems(attachmentService.listByBizId(Attachment.BizType.JOURNAL, item.getId()));
Page<Attachment> attachPage = new Page<>();
{
Attachment example = new Attachment();
example.setBizType(Attachment.BizType.JOURNAL);
example.setBizId(item.getId());
attachPage.setEqualsExample(example);
}
attachPage.setIndex(0);
attachPage.setSize(switch (page.getType()) {
case NORMAL -> Long.MAX_VALUE;
case PREVIEW -> 2; // 原图缩略图并存
});
resp.setItems(attachmentService.page(attachPage).getList());
BeanUtils.copyProperties(item, resp);
return resp;
}).toList());
@ -104,11 +168,35 @@ public class JournalController {
@AOPLog
@RequestRateLimit
@GetMapping("/list/date")
public Long[] listDate() {
return service.listDate();
@PostMapping("/list/ids")
public List<JournalResponse> listByIds(@RequestBody Long[] ids) {
List<JournalResponse> result = new ArrayList<>();
List<Journal> journals = service.listByIds(ids);
for (int i = 0; i < journals.size(); i++) {
Journal journal = journals.get(i);
JournalResponse resp = new JournalResponse();
Page<Attachment> attachPage = new Page<>();
{
Attachment example = new Attachment();
example.setBizType(Attachment.BizType.JOURNAL);
example.setBizId(journal.getId());
attachPage.setEqualsExample(example);
}
attachPage.setIndex(0);
attachPage.setSize(Long.MAX_VALUE);
resp.setItems(attachmentService.page(attachPage).getList());
BeanUtils.copyProperties(journal, resp);
result.add(resp);
}
return result;
}
/**
* 已记录照片、视频数量
*
* @return
*/
@RequestRateLimit
@GetMapping("/total")
public long total() {
@ -117,19 +205,12 @@ public class JournalController {
return journal + journalTravel;
}
@RequestRateLimit
@GetMapping("/travel")
public Travel getTravel() {
return service.getTravel();
}
@AOPLog
@RequestRateLimit
@PostMapping("/travel/luggage/update")
public void updateTravel(@RequestBody Travel.Luggage luggage) {
service.updateTravelLuggage(luggage);
}
/**
* 创建瞬间(上传的临时文件持久化储存,微信限制单次上传数量)
*
* @param tempFileIds
* @return
*/
@AOPLog
@RequestRateLimit
@PostMapping("/moment/create")
@ -137,13 +218,34 @@ public class JournalController {
return service.createMoment(tempFileIds);
}
/**
* 瞬间列表
*
* @return
*/
@AOPLog
@RequestRateLimit
@PostMapping("/moment/list")
public List<Attachment> listMoment() {
return service.listMoment();
Page<Attachment> page = new Page<>();
{
Attachment example = new Attachment();
example.setBizType(Attachment.BizType.JOURNAL_MOMENT);
example.setBizId(0L);
example.setAttachTypeValue(MediaAttach.Type.THUMB);
page.setEqualsExample(example);
}
page.setIndex(0);
page.setSize(Long.MAX_VALUE);
return attachmentService.page(page).getList();
}
/**
* 检查待上传文件是否已存在
*
* @param md5s
* @return 不存在的文件 md5 列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/moment/filter")
@ -151,6 +253,11 @@ public class JournalController {
return service.filterExistMoment(md5s);
}
/**
* 删除瞬间
*
* @param thumbIds 缩略图附件 ID 列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/moment/delete")
@ -158,6 +265,11 @@ public class JournalController {
service.deleteMoment(thumbIds);
}
/**
* 归档瞬间,归档记录 ID 不为空时表示归档到指定记录
*
* @param request
*/
@AOPLog
@RequestRateLimit
@PostMapping("/moment/archive")

View File

@ -0,0 +1,97 @@
package com.imyeyu.api.modules.journal.controller;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.journal.entity.Travel;
import com.imyeyu.api.modules.journal.service.TravelService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 旅行计划接口
*
* @author 夜雨
* @since 2025-12-12 14:50
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/journal/travel")
public class TravelController {
private final TravelService service;
private final AttachmentService attachmentService;
/**
* 创建旅行计划
*
* @param travel 旅行计划
*/
@AOPLog
@RequestRateLimit
@PostMapping("/create")
public void create(@RequestBody @Valid Travel travel) {
service.create(travel);
}
/**
* 更新旅行计划
*
* @param travel 旅行计划
*/
@AOPLog
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody @Valid Travel travel) {
service.update(travel);
}
/**
* 删除旅行计划(级联删除关联地点和附件)
*
* @param id 旅行 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/delete")
public void delete(@RequestSingleParam Long id) {
service.delete(id);
}
/**
* 查询旅行计划详情
*
* @param id 旅行 ID
* @return 旅行计划
*/
@AOPLog
@RequestRateLimit
@GetMapping("/{id}")
public Travel detail(@PathVariable Long id) {
return service.get(id);
}
/**
* 旅行计划列表
*
* @param page 分页参数
* @return 旅行列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list")
public PageResult<Travel> list(@RequestBody Page<Travel> page) {
return service.page(page);
}
}

View File

@ -0,0 +1,138 @@
package com.imyeyu.api.modules.journal.controller;
import com.imyeyu.api.bean.PreviewPage;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.journal.entity.TravelLocation;
import com.imyeyu.api.modules.journal.service.TravelLocationService;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequestSingleParam;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 旅行地点接口
*
* @author 夜雨
* @since 2025-12-12 14:50
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/journal/travel/location")
public class TravelLocationController {
private final AttachmentService attachmentService;
private final TravelLocationService service;
/**
* 创建旅行地点
*
* @param location 旅行地点
*/
@AOPLog
@RequestRateLimit
@PostMapping("/create")
public void create(@RequestBody @Valid TravelLocation location) {
service.create(location);
}
/**
* 更新旅行地点
*
* @param location 旅行地点
*/
@AOPLog
@RequestRateLimit
@PostMapping("/update")
public void update(@RequestBody @Valid TravelLocation location) {
service.update(location);
}
/**
* 删除旅行地点(级联删除附件)
*
* @param id 地点 ID
*/
@AOPLog
@RequestRateLimit
@PostMapping("/delete")
public void delete(@RequestSingleParam Long id) {
service.delete(id);
}
/**
* 查询旅行地点详情
*
* @param id 地点 ID
* @return 旅行地点
*/
@AOPLog
@RequestRateLimit
@GetMapping("/{id}")
public TravelLocation detail(@PathVariable Long id) {
TravelLocation location = service.get(id);
location.setItems(attachmentService.listByBizId(Attachment.BizType.JOURNAL_TRAVEL, location.getId()));
return location;
}
/**
* 旅行地点列表
*
* @param page 分页参数
* @return 地点列表
*/
@AOPLog
@RequestRateLimit
@PostMapping("/list")
public PageResult<TravelLocation> list(@RequestBody PreviewPage<TravelLocation> page) {
PageResult<TravelLocation> result = service.page(page);
for (TravelLocation location : result.getList()) {
Page<Attachment> attachPage = new Page<>();
{
Attachment example = new Attachment();
example.setBizType(Attachment.BizType.JOURNAL_TRAVEL);
example.setBizId(location.getId());
example.setAttachTypeValue(MediaAttach.Type.THUMB);
attachPage.setEqualsExample(example);
}
attachPage.setIndex(0);
attachPage.setSize(1); // 列表接口只允许预览
location.setItems(attachmentService.page(attachPage).getList());
}
return result;
}
@AOPLog
@RequestRateLimit
@PostMapping("/list/ids")
public List<TravelLocation> listByIds(@RequestBody Long[] ids) {
List<TravelLocation> locationList = service.listByIds(ids);
for (TravelLocation location : locationList) {
Page<Attachment> attachPage = new Page<>();
{
Attachment example = new Attachment();
example.setBizType(Attachment.BizType.JOURNAL_TRAVEL);
example.setBizId(location.getId());
attachPage.setEqualsExample(example);
}
attachPage.setIndex(0);
attachPage.setSize(Long.MAX_VALUE);
location.setItems(attachmentService.page(attachPage).getList());
}
return locationList;
}
}

View File

@ -0,0 +1,81 @@
package com.imyeyu.api.modules.journal.entity;
import com.imyeyu.spring.annotation.table.PageIgnore;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 旅行计划
*
* @author 夜雨
* @since 2025-12-12 14:30
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Travel extends Entity {
/**
* 交通类型
*
* @author 夜雨
* @since 2025-12-12 14:30
*/
public enum TransportationType {
/** 飞机 */
PLANE,
/** 火车 */
TRAIN,
/** 汽车 */
CAR,
/** 轮船 */
SHIP,
/** 自驾 */
SELF_DRIVING,
/** 其他 */
OTHER
}
/**
* 旅行状态
*
* @author 夜雨
* @since 2025-12-12 14:30
*/
public enum Status {
/** 计划中 */
PLANNING,
/** 进行中 */
ONGOING,
/** 已完成 */
COMPLETED,
}
/** 交通类型 */
private TransportationType transportationType;
/** 标题 */
private String title;
/** 内容 */
@PageIgnore
private String content;
/** 出行时间 */
private Long travelAt;
/** 天数 */
private Integer days;
/** 状态 */
private Status status;
}

View File

@ -0,0 +1,99 @@
package com.imyeyu.api.modules.journal.entity;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.spring.annotation.table.Transient;
import com.imyeyu.spring.entity.Entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
import java.util.List;
/**
* 旅行地点
*
* @author 夜雨
* @since 2025-12-12 14:30
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class TravelLocation extends Entity {
/**
* 地点类型
*
* @author 夜雨
* @since 2025-12-12 14:30
*/
public enum Type {
/** 美食 */
FOOD,
/** 酒店 */
HOTEL,
/** 交通站点 */
TRANSPORT,
/** 景点 */
ATTRACTION,
/** 商场 */
MALL,
/** 购物 */
SHOPPING,
/** 玩乐 */
PLAY,
/** 生活 */
LIFE,
}
/** 旅行计划 ID */
private Long travelId;
/** 类型 */
private Type type;
/** 标题 */
private String title;
/** 说明 */
private String description;
/** 纬度 */
private Double lat;
/** 经度 */
private Double lng;
/** 位置 */
private String location;
/** 费用 */
private BigDecimal amount;
/** true 为需要身份证 */
private Boolean requireIdCard;
/** true 为需要预约 */
private Boolean requireAppointment;
/** 评分 */
private Integer score;
/** 重要程度 */
private Integer importance;
@Transient
private Long[] attachmentIds;
@Transient
private String[] tempFileIds;
@Transient
private List<Attachment> items;
}

View File

@ -2,7 +2,6 @@ package com.imyeyu.api.modules.journal.mapper;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@ -12,12 +11,5 @@ import java.util.List;
*/
public interface JournalMapper extends BaseMapper<Journal, Long> {
@Select("SELECT COUNT(1) FROM `journal` WHERE `type` = #{type}")
long countByType(Journal.Type type);
@Select("SELECT * FROM `journal` WHERE `type` = #{type} AND `deleted_at` IS NULL ORDER BY `created_at` DESC LIMIT #{offset}, #{limit}")
List<Journal> listByType(Journal.Type type, long offset, int limit);
@Select("SELECT created_at FROM `journal` WHERE `deleted_at` IS NULL")
Long[] listDate();
List<Journal> listByIds(Long[] ids);
}

View File

@ -0,0 +1,17 @@
package com.imyeyu.api.modules.journal.mapper;
import com.imyeyu.api.modules.journal.entity.TravelLocation;
import com.imyeyu.spring.mapper.BaseMapper;
import java.util.List;
/**
* 旅行地点 Mapper
*
* @author 夜雨
* @since 2025-12-12 14:35
*/
public interface TravelLocationMapper extends BaseMapper<TravelLocation, Long> {
List<TravelLocation> listByIds(Long[] ids);
}

View File

@ -0,0 +1,32 @@
package com.imyeyu.api.modules.journal.mapper;
import com.imyeyu.api.modules.journal.entity.Travel;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 旅行计划 Mapper
*
* @author 夜雨
* @since 2025-12-12 14:35
*/
public interface TravelMapper extends BaseMapper<Travel, Long> {
/**
* 根据 ID 列表查询
*
* @param ids ID 列表
* @return 旅行列表
*/
List<Travel> listByIds(@Param("ids") Long[] ids);
/**
* 根据状态查询
*
* @param status 状态
* @return 旅行列表
*/
List<Travel> listByStatus(@Param("status") Travel.Status status);
}

View File

@ -1,15 +1,14 @@
package com.imyeyu.api.modules.journal.service;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.journal.bean.Travel;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.api.modules.journal.vo.AppendRequest;
import com.imyeyu.api.modules.journal.vo.ArchiveRequest;
import com.imyeyu.api.modules.journal.vo.JournalPage;
import com.imyeyu.api.modules.journal.vo.JournalRequest;
import com.imyeyu.api.modules.journal.vo.journal.ArchiveRequest;
import com.imyeyu.api.modules.journal.vo.journal.JournalRequest;
import com.imyeyu.api.modules.journal.vo.journal.UpdateRequest;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.DeletableService;
import com.imyeyu.spring.service.GettableService;
import com.imyeyu.spring.service.PageableService;
import java.util.List;
@ -17,27 +16,26 @@ import java.util.List;
* @author 夜雨
* @since 2025-09-26 15:53
*/
public interface JournalService extends DeletableService<Long> {
PageResult<Journal> page(JournalPage page) throws TimiException;
Long[] listDate() throws TimiException;
public interface JournalService extends PageableService<Journal>, GettableService<Journal, Long>, DeletableService<Long> {
void create(JournalRequest request) throws TimiException;
void appendItems(AppendRequest request) throws TimiException;
void update(UpdateRequest request) throws TimiException;
List<Journal> listByIds(Long... ids);
/**
* 过滤已存在瞬间
*
* @param md5s 原图文件 MD5 列表
* @return 未持久化储存的 MD5 列表
* @throws TimiException
*/
String[] filterExistMoment(String[] md5s) throws TimiException;
List<Attachment> createMoment(String[] tempFileIds) throws TimiException;
List<Attachment> listMoment() throws TimiException;
void deleteMoment(Long[] thumbIds) throws TimiException;
void archiveMoment(ArchiveRequest request) throws TimiException;
Travel getTravel() throws TimiException;
void updateTravelLuggage(Travel.Luggage luggage) throws TimiException;
}

View File

@ -0,0 +1,26 @@
package com.imyeyu.api.modules.journal.service;
import com.imyeyu.api.modules.journal.entity.TravelLocation;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.service.BaseService;
import java.util.List;
/**
* 旅行地点服务
*
* @author 夜雨
* @since 2025-12-12 14:40
*/
public interface TravelLocationService extends BaseService<TravelLocation, Long> {
List<TravelLocation> listByIds(Long... ids);
/**
* 根据旅行 ID 删除所有地点
*
* @param travelId 旅行 ID
* @throws TimiException 服务异常
*/
void deleteByTravelId(Long travelId) throws TimiException;
}

View File

@ -0,0 +1,13 @@
package com.imyeyu.api.modules.journal.service;
import com.imyeyu.api.modules.journal.entity.Travel;
import com.imyeyu.spring.service.BaseService;
/**
* 旅行计划服务
*
* @author 夜雨
* @since 2025-12-12 14:40
*/
public interface TravelService extends BaseService<Travel, Long> {
}

View File

@ -3,28 +3,20 @@ package com.imyeyu.api.modules.journal.service.implement;
import com.google.gson.Gson;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.bean.SettingKey;
import com.imyeyu.api.modules.common.bean.TempFileMetaData;
import com.imyeyu.api.modules.common.bean.Metadata;
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.attachment.AttachmentRequest;
import com.imyeyu.api.modules.journal.bean.Travel;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.api.modules.journal.mapper.JournalMapper;
import com.imyeyu.api.modules.journal.service.JournalService;
import com.imyeyu.api.modules.journal.vo.AppendRequest;
import com.imyeyu.api.modules.journal.vo.ArchiveRequest;
import com.imyeyu.api.modules.journal.vo.JournalPage;
import com.imyeyu.api.modules.journal.vo.JournalRequest;
import com.imyeyu.io.IO;
import com.imyeyu.api.modules.journal.vo.journal.ArchiveRequest;
import com.imyeyu.api.modules.journal.vo.journal.JournalRequest;
import com.imyeyu.api.modules.journal.vo.journal.UpdateRequest;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
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 lombok.RequiredArgsConstructor;
@ -33,10 +25,8 @@ import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@ -63,19 +53,6 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
return mapper;
}
@Override
public PageResult<Journal> page(JournalPage page) {
PageResult<Journal> result = new PageResult<>();
result.setTotal(mapper.countByType(page.getType()));
result.setList(mapper.listByType(page.getType(), page.getOffset(), page.getLimit()));
return result;
}
@Override
public Long[] listDate() throws TimiException {
return mapper.listDate();
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(JournalRequest request) throws TimiException {
@ -89,14 +66,13 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
return;
}
for (int i = 0; i < tempFileIds.length; i++) {
TempFileMetaData metadata = tempFileService.metadata(tempFileIds[i]);
File file = tempFileService.get(tempFileIds[i]);
Attachment tempFileAttach = attachmentService.getByMongoId(tempFileIds[i]);
AttachmentRequest sourceAttach = new AttachmentRequest();
sourceAttach.setName(metadata.getOriginalName());
Attachment sourceAttach = new Attachment();
sourceAttach.setName(tempFileAttach.getName());
sourceAttach.setBizType(Attachment.BizType.JOURNAL);
sourceAttach.setBizId(journal.getId());
sourceAttach.setInputStream(IO.getInputStream(file));
sourceAttach.setInputStream(tempFileAttach.openInputStream());
attachmentService.createMedia(sourceAttach);
}
} catch (Exception e) {
@ -105,28 +81,47 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void appendItems(AppendRequest request) throws TimiException {
TimiException.required(request.getTempFileIds(), "not found or empty tempFileIds");
try {
Journal journal = get(request.getId());
public void update(UpdateRequest request) throws TimiException {
TimiException.required(request.getId(), "not found request.id");
Journal journal = get(request.getId());
BeanUtils.copyProperties(request, journal);
super.update(journal);
String[] tempFileIds = request.getTempFileIds();
for (int i = 0; i < tempFileIds.length; i++) {
TempFileMetaData metadata = tempFileService.metadata(tempFileIds[i]);
File file = tempFileService.get(tempFileIds[i]);
AttachmentRequest sourceAttach = new AttachmentRequest();
sourceAttach.setName(metadata.getOriginalName());
sourceAttach.setBizType(Attachment.BizType.JOURNAL);
sourceAttach.setBizId(journal.getId());
sourceAttach.setInputStream(IO.getInputStream(file));
attachmentService.createMedia(sourceAttach);
}
} catch (Exception e) {
log.error("create journal error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO create journal error");
// 删除
Set<Long> dbAttachSet = attachmentService.listByBizId(Attachment.BizType.JOURNAL, journal.getId(), MediaAttach.Type.THUMB)
.stream()
.map(Attachment::getId)
.collect(Collectors.toSet());
Set<Long> retainIds = Set.of(TimiJava.firstNotEmpty(request.getAttachmentIds(), new Long[0]));
dbAttachSet.removeAll(retainIds);
for (Long removeId : dbAttachSet) {
attachmentService.deleteMedia(removeId);
}
// 新增
for (String tempFileId : TimiJava.defaultIfNull(request.getTempFileIds(), new String[0])) {
Attachment tempFileAttach = attachmentService.getByMongoId(tempFileId);
Attachment sourceAttach = new Attachment();
sourceAttach.setName(tempFileAttach.getName());
sourceAttach.setBizType(Attachment.BizType.JOURNAL);
sourceAttach.setBizId(journal.getId());
sourceAttach.setInputStream(tempFileAttach.openInputStream());
attachmentService.createMedia(sourceAttach);
}
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(Long id) {
super.delete(id);
attachmentService.deleteByBizId(Attachment.BizType.JOURNAL, id);
}
@Override
public List<Journal> listByIds(Long... ids) {
return mapper.listByIds(ids);
}
@Override
@ -149,16 +144,15 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
try {
List<Attachment> thumbResult = new ArrayList<>();
for (int i = 0; i < tempFileIds.length; i++) {
TempFileMetaData metadata = tempFileService.metadata(tempFileIds[i]);
File file = tempFileService.get(tempFileIds[i]);
Attachment tempFileAttach = attachmentService.getByMongoId(tempFileIds[i]);
AttachmentRequest sourceAttach = new AttachmentRequest();
sourceAttach.setName(metadata.getOriginalName());
Attachment sourceAttach = new Attachment();
sourceAttach.setName(tempFileAttach.getName());
sourceAttach.setBizType(Attachment.BizType.JOURNAL_MOMENT);
sourceAttach.setBizId(0L);
sourceAttach.setAttachTypeValue(MediaAttach.Type.SOURCE);
sourceAttach.setSize(file.length());
sourceAttach.setInputStream(IO.getInputStream(file));
sourceAttach.setSize(tempFileAttach.getSize());
sourceAttach.setInputStream(tempFileAttach.openInputStream());
thumbResult.add(attachmentService.createMedia(sourceAttach));
}
return thumbResult;
@ -168,15 +162,6 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
}
}
@Override
public List<Attachment> listMoment() throws TimiException {
Page page = new Page(0, Integer.MAX_VALUE);
LinkedHashMap<String, BaseMapper.OrderType> orderMap = new LinkedHashMap<>();
orderMap.put("created_at", BaseMapper.OrderType.DESC);
page.setOrderMap(orderMap);
return attachmentService.pageByBizId(Attachment.BizType.JOURNAL_MOMENT, 0L, List.of(MediaAttach.Type.THUMB), page).getList();
}
@Override
public void deleteMoment(Long[] thumbIds) throws TimiException {
for (int i = 0; i < thumbIds.length; i++) {
@ -187,10 +172,16 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
@Override
public void archiveMoment(ArchiveRequest request) throws TimiException {
try {
Journal journal = new Journal();
BeanUtils.copyProperties(request, journal);
super.create(journal);
Journal journal;
if (request.getId() == null) {
// 归档为新纪录
journal = new Journal();
BeanUtils.copyProperties(request, journal);
super.create(journal);
} else {
// 归档到指定记录
journal = get(request.getId());
}
Long[] thumbIds = request.getThumbIds();
if (TimiJava.isEmpty(thumbIds)) {
return;
@ -201,8 +192,8 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
thumbAttach.setBizId(journal.getId());
attachmentService.update(thumbAttach);
MediaAttach.ExtData extData = gson.fromJson(thumbAttach.getExt(), MediaAttach.ExtData.class);
Attachment sourceAttach = attachmentService.get(extData.getSourceId());
Metadata.ThumbImage thumbMetadata = gson.fromJson(thumbAttach.getMetadata(), Metadata.ThumbImage.class);
Attachment sourceAttach = attachmentService.get(thumbMetadata.getSourceId());
sourceAttach.setBizType(Attachment.BizType.JOURNAL);
sourceAttach.setBizId(journal.getId());
attachmentService.update(sourceAttach);
@ -212,19 +203,4 @@ public class JournalServiceImplement extends AbstractEntityService<Journal, Long
throw new TimiException(TimiCode.ERROR).msgKey("TODO archive journal error");
}
}
@Override
public Travel getTravel() throws TimiException {
return gson.fromJson(settingService.getAsJsonObject(SettingKey.JOURNAL_TRAVEL), Travel.class);
}
@Override
public void updateTravelLuggage(Travel.Luggage luggage) throws TimiException {
Travel dbTravel = getTravel();
dbTravel.setLuggage(luggage);
Setting setting = settingService.getByKey(SettingKey.JOURNAL_TRAVEL);
setting.setValue(gson.toJson(dbTravel));
settingService.update(setting);
settingService.clearCache(SettingKey.JOURNAL_TRAVEL);
}
}

View File

@ -0,0 +1,118 @@
package com.imyeyu.api.modules.journal.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.bean.MediaAttach;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.journal.entity.TravelLocation;
import com.imyeyu.api.modules.journal.mapper.TravelLocationMapper;
import com.imyeyu.api.modules.journal.service.TravelLocationService;
import com.imyeyu.api.modules.journal.service.TravelService;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 旅行地点服务实现
*
* @author 夜雨
* @since 2025-12-12 14:45
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class TravelLocationServiceImplement extends AbstractEntityService<TravelLocation, Long> implements TravelLocationService {
private final TravelService travelService;
private final AttachmentService attachmentService;
private final TravelLocationMapper mapper;
@Override
protected BaseMapper<TravelLocation, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void create(TravelLocation travelLocation) {
super.create(travelLocation);
for (String tempFileId : travelLocation.getTempFileIds()) {
Attachment tempFileAttach = attachmentService.getByMongoId(tempFileId);
Attachment attach = new Attachment();
attach.setName(tempFileAttach.getName());
attach.setBizType(Attachment.BizType.JOURNAL_TRAVEL);
attach.setBizId(travelLocation.getId());
attach.setInputStream(tempFileAttach.openInputStream());
attachmentService.createMedia(attach);
}
// 更新操作时间以保证排序
travelService.update(travelService.get(travelLocation.getTravelId()));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(TravelLocation travelLocation) {
// 使用 update 以允许设置 score 和 importance 为 null
mapper.update(travelLocation);
// 删除
if (TimiJava.isNotEmpty(travelLocation.getAttachmentIds())) {
Set<Long> dbAttachSet = attachmentService.listByBizId(Attachment.BizType.JOURNAL_TRAVEL, travelLocation.getId(), MediaAttach.Type.THUMB)
.stream()
.map(Attachment::getId)
.collect(Collectors.toSet());
Set<Long> retainIds = Set.of(travelLocation.getAttachmentIds());
dbAttachSet.removeAll(retainIds);
for (Long removeId : dbAttachSet) {
attachmentService.deleteMedia(removeId);
}
}
// 新增
for (String tempFileId : TimiJava.defaultIfNull(travelLocation.getTempFileIds(), new String[0])) {
Attachment tempFileAttach = attachmentService.getByMongoId(tempFileId);
Attachment attach = new Attachment();
attach.setName(tempFileAttach.getName());
attach.setBizType(Attachment.BizType.JOURNAL_TRAVEL);
attach.setBizId(travelLocation.getId());
attach.setInputStream(tempFileAttach.openInputStream());
attachmentService.createMedia(attach);
}
// 更新操作时间以保证排序
travelService.update(travelService.get(travelLocation.getTravelId()));
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(Long id) {
super.delete(id);
attachmentService.deleteByBizId(Attachment.BizType.JOURNAL_TRAVEL, id);
}
@Override
public List<TravelLocation> listByIds(Long... ids) {
return mapper.listByIds(ids);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void deleteByTravelId(Long travelId) throws TimiException {
TravelLocation example = new TravelLocation();
example.setTravelId(travelId);
List<TravelLocation> locationList = mapper.selectAllByExample(example);
for (TravelLocation location : locationList) {
delete(location.getId());
}
}
}

View File

@ -0,0 +1,48 @@
package com.imyeyu.api.modules.journal.service.implement;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.journal.entity.Travel;
import com.imyeyu.api.modules.journal.mapper.TravelMapper;
import com.imyeyu.api.modules.journal.service.TravelLocationService;
import com.imyeyu.api.modules.journal.service.TravelService;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 旅行计划服务实现
*
* @author 夜雨
* @since 2025-12-12 14:45
*/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = {@Lazy})
public class TravelServiceImplement extends AbstractEntityService<Travel, Long> implements TravelService {
private final TravelMapper mapper;
private final TravelLocationService travelLocationService;
@Override
protected BaseMapper<Travel, Long> mapper() {
return mapper;
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void update(Travel travel) {
// 使用 update 以允许设置 travelAt 和 days 为 null
mapper.update(travel);
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public void delete(Long id) {
super.delete(id);
travelLocationService.deleteByTravelId(id);
}
}

View File

@ -42,6 +42,6 @@ public class JournalAPIInterceptor implements HandlerInterceptor {
return true;
}
}
throw new TimiException(TimiCode.ARG_MISS).msgKey("invalid.key");
throw new TimiException(TimiCode.PERMISSION_MISS).msgKey("invalid.key");
}
}

View File

@ -1,15 +0,0 @@
package com.imyeyu.api.modules.journal.vo;
import lombok.Data;
/**
* @author 夜雨
* @since 2025-09-30 20:37
*/
@Data
public class AppendRequest {
private Long id;
private String[] tempFileIds;
}

View File

@ -1,17 +0,0 @@
package com.imyeyu.api.modules.journal.vo;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.spring.bean.Page;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @author 夜雨
* @since 2025-10-09 18:48
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class JournalPage extends Page {
private Journal.Type type;
}

View File

@ -1,4 +1,4 @@
package com.imyeyu.api.modules.journal.vo;
package com.imyeyu.api.modules.journal.vo.journal;
import com.imyeyu.api.modules.journal.entity.Journal;
import lombok.Data;

View File

@ -1,4 +1,4 @@
package com.imyeyu.api.modules.journal.vo;
package com.imyeyu.api.modules.journal.vo.journal;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.spring.annotation.table.Transient;
@ -6,6 +6,8 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 创建日志记录
*
* @author 夜雨
* @since 2025-09-26 15:56
*/
@ -13,6 +15,7 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public class JournalRequest extends Journal {
/** 临时文件 ID 列表 */
@Transient
private String[] tempFileIds;
}

View File

@ -1,4 +1,4 @@
package com.imyeyu.api.modules.journal.vo;
package com.imyeyu.api.modules.journal.vo.journal;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.journal.entity.Journal;

View File

@ -0,0 +1,25 @@
package com.imyeyu.api.modules.journal.vo.journal;
import com.imyeyu.api.modules.journal.entity.Journal;
import com.imyeyu.spring.annotation.table.Transient;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 更新日志记录请求
*
* @author 夜雨
* @since 2025-12-08
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UpdateRequest extends Journal {
/** 临时文件 ID 列表(新上传的附件) */
@Transient
private String[] tempFileIds;
/** 保留的附件 ID 列表(用于差分删除) */
@Transient
private Long[] attachmentIds;
}

View File

@ -1,18 +1,17 @@
package com.imyeyu.api.modules.lyric.service.implement;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
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.service.AttachmentService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.lyric.entity.LyricCorrect;
import com.imyeyu.api.modules.lyric.mapper.LyricCorrectMapper;
import com.imyeyu.api.modules.lyric.service.LyricCorrectService;
import com.imyeyu.api.modules.lyric.vo.LyricCorrectRequest;
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;
@ -76,7 +75,7 @@ public class LyricCorrectServiceImplement extends AbstractEntityService<LyricCor
create(correct);
try {
MultipartFile file = request.getFile();
AttachmentRequest attachment = new AttachmentRequest();
Attachment attachment = new Attachment();
attachment.setBizType(Attachment.BizType.LYRIC);
attachment.setBizId(correct.getId());
attachment.setName(correct.getId() + ".lrc");

View File

@ -95,7 +95,7 @@ public class PlayerController {
@RequestRateLimit
@PostMapping("/list")
public List<MinecraftPlayer> listPlayer(@RequestHeader("Token") String token) {
Long userId = TimiJava.firstNotNull(redis.get(token), userToken.getUserId(token));
Long userId = TimiJava.defaultIfNull(redis.get(token), userToken.getUserId(token));
if (userId == null) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("token.illegal");
}

View File

@ -1,12 +1,11 @@
package com.imyeyu.api.modules.mirror;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.mirror.entity.Mirror;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
@ -42,7 +41,7 @@ public abstract class AttachmentMirror extends AbstractMirror {
* @return 新增附件
* @throws Exception 处理异常
*/
protected abstract List<AttachmentRequest> diffAdd(List<Attachment> dbFiles) throws Exception;
protected abstract List<Attachment> diffAdd(List<Attachment> dbFiles) throws Exception;
/**
* 差分移除附件
@ -70,10 +69,10 @@ public abstract class AttachmentMirror extends AbstractMirror {
attachmentService.destroy(diffRemoveList.get(i).getId());
}
List<AttachmentRequest> diffAddList = diffAdd(dbFiles);
List<Attachment> diffAddList = diffAdd(dbFiles);
syncAdded = diffAddList.size();
for (int i = 0; i < diffAddList.size(); i++) {
AttachmentRequest request = diffAddList.get(i);
Attachment request = diffAddList.get(i);
request.setBizType(Attachment.BizType.MIRROR);
request.setBizId(mirror.getId());
attachmentService.create(request);

View File

@ -3,7 +3,6 @@ package com.imyeyu.api.modules.mirror;
import com.google.gson.Gson;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.mirror.bean.AttachType;
import com.imyeyu.api.modules.mirror.data.FabricAPI;
import com.imyeyu.api.modules.mirror.entity.Mirror;
@ -106,10 +105,10 @@ public class FabricAPIMirror extends AttachmentMirror {
}
@Override
protected List<AttachmentRequest> diffAdd(List<Attachment> dbFiles) throws Exception {
protected List<Attachment> diffAdd(List<Attachment> dbFiles) throws Exception {
Set<String> dbNameSet = dbFiles.stream().map(Attachment::getName).collect(Collectors.toSet());
List<AttachmentRequest> result = new ArrayList<>();
List<Attachment> result = new ArrayList<>();
for (Map.Entry<String, String> item : versionMap.entrySet()) {
String version = "%s+%s".formatted(item.getValue(), item.getKey());
String name = "fabric-api-%s.jar".formatted(version);
@ -119,7 +118,7 @@ public class FabricAPIMirror extends AttachmentMirror {
byte[] bytes = Request.get(url).viaProxy(proxy).execute().returnContent().asBytes();
AttachmentRequest attachment = new AttachmentRequest();
Attachment attachment = new Attachment();
attachment.setAttachTypeValue(AttachType.FABRIC_API);
attachment.setName(name);
attachment.setSize((long) bytes.length);

View File

@ -3,7 +3,6 @@ package com.imyeyu.api.modules.mirror;
import com.google.gson.Gson;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.mirror.bean.AttachType;
import com.imyeyu.api.modules.mirror.data.OpenJDK;
import com.imyeyu.api.modules.mirror.entity.Mirror;
@ -70,11 +69,11 @@ public class OpenJDKMirror extends AttachmentMirror {
}
@Override
protected List<AttachmentRequest> diffAdd(List<Attachment> dbFiles) throws Exception {
protected List<Attachment> diffAdd(List<Attachment> dbFiles) throws Exception {
Map<String, OpenJDK> githubNameMap = githubMirrorResult.stream().collect(Collectors.toMap(OpenJDK::getName, item -> item));
Set<String> dbNameSet = dbFiles.stream().map(Attachment::getName).collect(Collectors.toSet());
List<AttachmentRequest> result = new ArrayList<>();
List<Attachment> result = new ArrayList<>();
for (Map.Entry<String, OpenJDK> item : githubNameMap.entrySet()) {
if (!dbNameSet.contains(item.getKey())) {
String url = item.getValue().getData();
@ -82,7 +81,7 @@ public class OpenJDKMirror extends AttachmentMirror {
byte[] bytes = Request.get(url).viaProxy(proxy).execute().returnContent().asBytes();
AttachmentRequest attachment = new AttachmentRequest();
Attachment attachment = new Attachment();
attachment.setAttachTypeValue(AttachType.OPEN_JDK);
attachment.setName(item.getKey());
attachment.setSize((long) bytes.length);

View File

@ -1,7 +1,7 @@
package com.imyeyu.api.modules.system.controller;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.system.bean.ServerStatus;
import com.imyeyu.api.modules.system.service.SystemService;
import com.imyeyu.api.modules.system.vo.TempAttachRequest;
@ -119,7 +119,7 @@ public class SystemController {
for (MultipartFile file : request.getFile()) {
byte[] bytes = IO.toBytes(file.getInputStream());
AttachmentRequest attach = new AttachmentRequest();
Attachment attach = new Attachment();
attach.setName(file.getOriginalFilename());
attach.setBizType(request.getBizType());
attach.setBizId(request.getBizId());

View File

@ -145,7 +145,7 @@ public class AsyncTaskServiceImplement implements AsyncTaskService {
for (int i = 0; i < taskList.size(); i++) {
AbstractAsyncTask task = taskList.get(i);
task.setMessage(task.logBuffer.toString());
task.setMessage(TimiJava.firstNotNull(task.getMessage(), ""));
task.setMessage(TimiJava.defaultIfNull(task.getMessage(), ""));
if (IOSize.KB * 4 < task.getMessage().length()) {
task.setMessage(task.getMessage().substring((int) (task.getMessage().length() - IOSize.KB * 4)));
}

View File

@ -12,7 +12,7 @@ import org.jcodec.api.NotSupportedException;
*/
public class RedisLanguage extends AbstractLanguageMapper {
public RedisLanguage(Language language) {
public RedisLanguage(Language.Enum language) {
super(language);
}

View File

@ -1,10 +1,10 @@
package com.imyeyu.api.util;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import com.imyeyu.api.modules.common.service.MultilingualService;
import com.imyeyu.java.bean.Language;
import com.imyeyu.lang.multi.Multilingual;
import com.imyeyu.api.modules.common.service.MultilingualService;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
@ -19,7 +19,7 @@ public class RedisMultilingual extends Multilingual {
@PostConstruct
private void postConstruct() {
Language[] languages = Language.values();
Language.Enum[] languages = Language.Enum.values();
for (int i = 0; i < languages.length; i++) {
add(languages[i], new RedisLanguage(languages[i]));
}

677
src/main/resources/db.sql Normal file
View File

@ -0,0 +1,677 @@
/*
Navicat Premium Dump SQL
Source Server : dev_mariadb
Source Server Type : MariaDB
Source Server Version : 110802 (11.8.2-MariaDB-ubu2404)
Source Host : vm.imyeyu.dev:3307
Source Schema : timi_server
Target Server Type : MariaDB
Target Server Version : 110802 (11.8.2-MariaDB-ubu2404)
File Encoding : 65001
Date: 12/12/2025 14:24:29
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for article
-- ----------------------------
DROP TABLE IF EXISTS `article`;
CREATE TABLE `article` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题',
`type` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '类型',
`digest` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '摘要',
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '数据',
`extend_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '扩展数据 JSON' CHECK (json_valid(`extend_data`)),
`reads` int(11) NULL DEFAULT 0 COMMENT '阅读',
`likes` int(11) NULL DEFAULT 0 COMMENT '喜欢',
`show_comment` tinyint(1) NULL DEFAULT 1 COMMENT '1 为显示历史评论',
`can_comment` tinyint(1) NULL DEFAULT 1 COMMENT '1 为允许评论',
`can_ranking` tinyint(1) NULL DEFAULT 1 COMMENT '1 为允许进入每周排行列表',
`can_list` tinyint(1) NULL DEFAULT NULL COMMENT '1 为允许通过列表查询',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 128 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文章' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for async_task
-- ----------------------------
DROP TABLE IF EXISTS `async_task`;
CREATE TABLE `async_task` (
`uuid` varchar(38) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`message` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`progress` double NULL DEFAULT NULL,
`start_at` bigint(20) NULL DEFAULT NULL,
`interrupt_at` bigint(20) NULL DEFAULT NULL,
`error_at` bigint(20) NULL DEFAULT NULL,
`died_at` bigint(20) NULL DEFAULT NULL,
`can_pause` tinyint(1) NULL DEFAULT NULL,
`can_interrupt` tinyint(1) NULL DEFAULT NULL,
`is_periodical` tinyint(1) NULL DEFAULT NULL,
`cron` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`uuid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for attachment
-- ----------------------------
DROP TABLE IF EXISTS `attachment`;
CREATE TABLE `attachment` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`biz_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`biz_id` bigint(20) NOT NULL,
`attach_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`mongo_id` varchar(24) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`title` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`mime_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`metadata` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`size` bigint(20) NULL DEFAULT NULL,
`md5` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`ext` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`created_at` bigint(20) NOT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
`destroy_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 530 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for bill
-- ----------------------------
DROP TABLE IF EXISTS `bill`;
CREATE TABLE `bill` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`revenue_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '收入类型',
`expenditure_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '支出类型',
`description` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '说明',
`remarks` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
`decimal` bigint(20) NOT NULL DEFAULT 0 COMMENT '金额(放大了 100 倍)',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3687 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消费账单' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for comment
-- ----------------------------
DROP TABLE IF EXISTS `comment`;
CREATE TABLE `comment` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`biz_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '关联业务',
`biz_id` bigint(20) NOT NULL COMMENT '关联业务 ID',
`user_id` bigint(20) NULL DEFAULT NULL COMMENT '评论者',
`nick` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '评论者昵称',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '内容',
`ip` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 69 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '评论' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for comment_remind_queue
-- ----------------------------
DROP TABLE IF EXISTS `comment_remind_queue`;
CREATE TABLE `comment_remind_queue` (
`uuid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'UUID',
`user_id` bigint(20) NOT NULL COMMENT '被回复用户 ID',
`reply_id` bigint(20) NOT NULL COMMENT '回复 ID',
PRIMARY KEY (`uuid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '评论回复提醒队列,被回复用户是注册用户并且开启邮件推送时,缓存在本表,由邮件推送服务操作' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for comment_reply
-- ----------------------------
DROP TABLE IF EXISTS `comment_reply`;
CREATE TABLE `comment_reply` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`comment_id` bigint(20) NOT NULL,
`reply_id` bigint(20) NULL DEFAULT NULL COMMENT '被回复的回复,回复主评论时为 NULL',
`sender_id` bigint(20) NULL DEFAULT NULL COMMENT '回复者',
`receiver_id` bigint(20) NULL DEFAULT NULL COMMENT '被回复者',
`sender_nick` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '回复者昵称',
`receiver_nick` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '被回复者昵称',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '内容',
`ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '回复时 IP',
`ignored_at` bigint(20) NULL DEFAULT NULL COMMENT '被回复者忽略该回复的时间',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '评论回复' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for comment_reply_record
-- ----------------------------
DROP TABLE IF EXISTS `comment_reply_record`;
CREATE TABLE `comment_reply_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户 ID',
`reply_id` bigint(20) NOT NULL COMMENT '回复 ID',
`created_at` bigint(20) NOT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '评论回复记录(显示在个人空间\"回复我的\"),因“仅删除回复记录不删除实体回复”功能而作此表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for email_filter
-- ----------------------------
DROP TABLE IF EXISTS `email_filter`;
CREATE TABLE `email_filter` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`email` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`from` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '通过邮件直接拒绝系统发送的列表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for email_queue
-- ----------------------------
DROP TABLE IF EXISTS `email_queue`;
CREATE TABLE `email_queue` (
`uuid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'UUID',
`biz_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT 'EmailType 推送类型',
`biz_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '推送数据 ID',
`send_at` bigint(20) NOT NULL DEFAULT 0 COMMENT '推送时间',
PRIMARY KEY (`uuid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件推送队列' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for email_queue_log
-- ----------------------------
DROP TABLE IF EXISTS `email_queue_log`;
CREATE TABLE `email_queue_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`uuid` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '队列 UUID',
`biz_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'EmailType 类型',
`biz_id` bigint(20) NULL DEFAULT NULL COMMENT '数据 ID',
`send_to` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '目标',
`send_at` bigint(20) NULL DEFAULT NULL COMMENT '时间',
`is_sent` tinyint(4) NULL DEFAULT NULL COMMENT '1 为成功',
`exception_msg` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '异常信息',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '邮件推送队列日志' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for feedback
-- ----------------------------
DROP TABLE IF EXISTS `feedback`;
CREATE TABLE `feedback` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`from` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '来自',
`email` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '联系邮箱',
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '内容',
`ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'IP',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '反馈' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for friend
-- ----------------------------
DROP TABLE IF EXISTS `friend`;
CREATE TABLE `friend` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`icon` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '图标 Base64',
`name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题',
`link` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '链接',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '友链' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for git_issue
-- ----------------------------
DROP TABLE IF EXISTS `git_issue`;
CREATE TABLE `git_issue` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`repository_id` bigint(20) NOT NULL COMMENT '所属仓库 ID',
`publisher_id` bigint(20) NULL DEFAULT NULL COMMENT '所属用户 ID',
`publisher_nick` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '游客昵称',
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类型',
`version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '版本',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题',
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '说明',
`status` varbinary(50) NULL DEFAULT NULL COMMENT '状态',
`confirmed_at` bigint(20) NULL DEFAULT NULL COMMENT '确认时间',
`develop_at` bigint(20) NULL DEFAULT NULL COMMENT '开发时间',
`closed_at` bigint(20) NULL DEFAULT NULL COMMENT '关闭时间',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Git 问题报告' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for git_merge
-- ----------------------------
DROP TABLE IF EXISTS `git_merge`;
CREATE TABLE `git_merge` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`repository_id` bigint(20) NOT NULL COMMENT '所属仓库 ID',
`requester_id` bigint(20) NOT NULL COMMENT '请求用户 ID',
`issue_id` bigint(20) NULL DEFAULT NULL COMMENT '相关反馈 ID',
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类型',
`from_branch` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '来源分支',
`to_branch` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '去向分支',
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题',
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '说明',
`checked_at` bigint(20) NULL DEFAULT NULL COMMENT '通过审查时间',
`merged_at` bigint(20) NULL DEFAULT NULL COMMENT '合并时间',
`rejected_at` bigint(20) NULL DEFAULT NULL COMMENT '拒绝时间',
`reject_reason` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '拒绝原因',
`closed_at` bigint(20) NULL DEFAULT NULL COMMENT '关闭时间',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '状态',
`created_at` bigint(20) NOT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Git 合并申请' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for git_release
-- ----------------------------
DROP TABLE IF EXISTS `git_release`;
CREATE TABLE `git_release` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`repository_id` bigint(20) NOT NULL COMMENT '所属仓库 ID',
`version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '版本',
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '说明',
`sha1` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '版本 SHA1',
`commits` int(11) NOT NULL DEFAULT 0 COMMENT '此版本后提交次数',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Git 版本发布' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for icon
-- ----------------------------
DROP TABLE IF EXISTS `icon`;
CREATE TABLE `icon` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`unicode` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`svg` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 150 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字体图标' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for journal
-- ----------------------------
DROP TABLE IF EXISTS `journal`;
CREATE TABLE `journal` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`idea` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`lat` double NULL DEFAULT NULL,
`lng` double NULL DEFAULT NULL,
`location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`weather` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`pusher` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 48 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for lyric
-- ----------------------------
DROP TABLE IF EXISTS `lyric`;
CREATE TABLE `lyric` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`song` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '歌曲名',
`singer` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '演唱',
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '歌词',
`citations` bigint(20) NOT NULL DEFAULT 0 COMMENT '被引用次数,更新者不为空时开始计数',
`updated_by` bigint(20) NULL DEFAULT NULL COMMENT '更新者 ID',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '歌词' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for lyric_corrects
-- ----------------------------
DROP TABLE IF EXISTS `lyric_corrects`;
CREATE TABLE `lyric_corrects` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`lyric_id` bigint(20) NULL DEFAULT NULL COMMENT '申请更新歌词 ID',
`song` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '歌曲名',
`singer` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '演唱',
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '歌词',
`request_by` bigint(20) NULL DEFAULT NULL COMMENT '申请人',
`request_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '申请人 IP',
`cancel_at` bigint(20) NULL DEFAULT NULL COMMENT '取消时间',
`approval_at` bigint(20) NULL DEFAULT NULL COMMENT '通过时间',
`reject_at` bigint(20) NULL DEFAULT NULL COMMENT '拒绝时间',
`reject_reason` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '拒绝原因',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '歌词更新申请' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for minecraft_pack
-- ----------------------------
DROP TABLE IF EXISTS `minecraft_pack`;
CREATE TABLE `minecraft_pack` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '游戏 ID',
`ver` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '整合包版本',
`title` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '标题',
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '简介',
`game_ver` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '游戏版本',
`def_option` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '默认配置',
`size` bigint(20) NOT NULL DEFAULT 0 COMMENT '文件大小',
`is_deprecated` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'true 为已过时',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Minecraft 客户端列表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for minecraft_pack_source
-- ----------------------------
DROP TABLE IF EXISTS `minecraft_pack_source`;
CREATE TABLE `minecraft_pack_source` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`pack_id` bigint(20) NOT NULL COMMENT '整合版 ID',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据源名称',
`type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据类型',
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据',
`order` int(11) NOT NULL DEFAULT 0 COMMENT '排序',
`is_default` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'true 为默认',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Minecraft 客户端下载源' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for minecraft_player
-- ----------------------------
DROP TABLE IF EXISTS `minecraft_player`;
CREATE TABLE `minecraft_player` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '关联 User.id',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '游戏名',
`last_login_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最近登录 IP',
`last_login_at` bigint(20) NULL DEFAULT NULL COMMENT '最近登录',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`, `user_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'Minecraft 玩家' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for mirror
-- ----------------------------
DROP TABLE IF EXISTS `mirror`;
CREATE TABLE `mirror` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`bean` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '执行类',
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '镜像数据',
`period` int(11) NOT NULL COMMENT '周期(分钟)',
`last_sync_at` bigint(20) NULL DEFAULT NULL COMMENT '最近同步时间',
`is_enable` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'true 为启用同步',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '镜像' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for multilingual
-- ----------------------------
DROP TABLE IF EXISTS `multilingual`;
CREATE TABLE `multilingual` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`key` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`zh_cn` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`zh_tw` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`en_us` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`ru_ru` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`ja_jp` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`de_de` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`ko_kr` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 795 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '多语言映射' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for setting
-- ----------------------------
DROP TABLE IF EXISTS `setting`;
CREATE TABLE `setting` (
`key` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`description` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`is_private` tinyint(1) NOT NULL DEFAULT 1,
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统配置' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for tag
-- ----------------------------
DROP TABLE IF EXISTS `tag`;
CREATE TABLE `tag` (
`id` bigint(20) NOT NULL,
`biz_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`biz_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`value` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for task
-- ----------------------------
DROP TABLE IF EXISTS `task`;
CREATE TABLE `task` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '名称',
`digest` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '摘要',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '状态',
`is_public` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'true 为公开',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '任务列表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for task_detail
-- ----------------------------
DROP TABLE IF EXISTS `task_detail`;
CREATE TABLE `task_detail` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`task_id` bigint(20) NOT NULL,
`type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '类型',
`status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '任务状态',
`digest` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '摘要',
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '附加说明',
`complete_at` bigint(20) NULL DEFAULT NULL COMMENT '完成时间',
`close_at` bigint(20) NULL DEFAULT NULL COMMENT '关闭时间',
`close_reason` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关闭原因',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '任务详细列表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for template
-- ----------------------------
DROP TABLE IF EXISTS `template`;
CREATE TABLE `template` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`biz_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`biz_code` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '模板' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for travel
-- ----------------------------
DROP TABLE IF EXISTS `travel`;
CREATE TABLE `travel` (
`id` bigint(20) NOT NULL,
`transportation_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '交通类型',
`title` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '内容',
`travel_at` bigint(20) NULL DEFAULT NULL COMMENT '出行时间',
`days` int(11) NULL DEFAULT NULL COMMENT '天数',
`status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '状态',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '旅行计划' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for travel_location
-- ----------------------------
DROP TABLE IF EXISTS `travel_location`;
CREATE TABLE `travel_location` (
`id` bigint(20) NOT NULL,
`travel_id` bigint(20) NOT NULL COMMENT '旅行计划 ID',
`type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '类型',
`title` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '标题',
`description` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '说明',
`lat` double NULL DEFAULT NULL COMMENT '经度',
`lng` double NULL DEFAULT NULL COMMENT '维度',
`location` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '位置',
`amount` decimal(10, 2) NULL DEFAULT NULL COMMENT '费用',
`require_id_card` tinyint(4) NULL DEFAULT NULL COMMENT '1 为需要身份证',
`score` int(11) NULL DEFAULT NULL COMMENT '必要评分',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '旅行地点' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名',
`password` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码摘要',
`email` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '邮箱',
`email_verify_at` bigint(20) NULL DEFAULT NULL COMMENT '邮箱验证时间',
`unmute_at` bigint(20) NULL DEFAULT NULL COMMENT '解除禁言时间',
`unban_at` bigint(20) NULL DEFAULT NULL COMMENT '解除封禁时间',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_config
-- ----------------------------
DROP TABLE IF EXISTS `user_config`;
CREATE TABLE `user_config` (
`user_id` bigint(20) NOT NULL,
`email_reply_remind` tinyint(1) NOT NULL DEFAULT 1 COMMENT '1 为邮箱接收回复提醒',
`updated_at` bigint(20) NULL DEFAULT NULL COMMENT '配置更新时间',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户配置' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_privacy
-- ----------------------------
DROP TABLE IF EXISTS `user_privacy`;
CREATE TABLE `user_privacy` (
`user_id` bigint(20) NOT NULL,
`email` tinyint(1) NOT NULL DEFAULT 1,
`sex` tinyint(1) NOT NULL DEFAULT 1,
`birthdate` tinyint(1) NOT NULL DEFAULT 1,
`qq` tinyint(1) NOT NULL DEFAULT 1,
`last_login_at` tinyint(1) NOT NULL DEFAULT 1,
`created_at` tinyint(1) NOT NULL DEFAULT 1,
`updated_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户隐私控制user_id 关联用户updated_at 为本表更新时间,其他字段 1 为公开0 为不公开' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for user_profile
-- ----------------------------
DROP TABLE IF EXISTS `user_profile`;
CREATE TABLE `user_profile` (
`user_id` bigint(20) NOT NULL COMMENT '用户 ID',
`wrapper_type` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PIXELATED' COMMENT '背景图渲染算法',
`avatar_type` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'PIXELATED' COMMENT '头像渲染算法',
`exp` int(11) NOT NULL DEFAULT 0 COMMENT '经验值',
`sex` int(11) NULL DEFAULT NULL COMMENT '性别0 女1 男',
`birthdate` bigint(20) NULL DEFAULT NULL COMMENT '出生日期',
`qq` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'QQ',
`description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '说明',
`last_login_ip` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '上次登录 IP',
`last_login_at` bigint(20) NULL DEFAULT NULL COMMENT '上次登录时间',
`updated_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户资料' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Table structure for version
-- ----------------------------
DROP TABLE IF EXISTS `version`;
CREATE TABLE `version` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(191) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '名称',
`version` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '版本',
`content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新摘要',
`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新地址',
`created_at` bigint(20) NULL DEFAULT NULL,
`updated_at` bigint(20) NULL DEFAULT NULL,
`deleted_at` bigint(20) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 15 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '软件版本管理' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@ -24,7 +24,7 @@
<select id="countByPage" resultType="long">
SELECT COUNT(1) <include refid="normalCondition" />
</select>
<select id="listByPage" resultType="com.imyeyu.api.modules.blog.entity.Article">
<select id="selectByPage" resultType="com.imyeyu.api.modules.blog.entity.Article">
SELECT
<include refid="listColumn" />
<include refid="normalCondition" />

View File

@ -67,6 +67,12 @@
#{item}
</foreach>
</if>
<if test="md5s != null and 0 &lt; md5s.size()">
AND `md5` IN
<foreach collection="md5s" item="md5" open="(" separator="," close=")">
#{md5}
</foreach>
</if>
AND deleted_at IS NULL
AND destroy_at IS NULL
</select>

View File

@ -3,7 +3,7 @@
<mapper namespace="com.imyeyu.api.modules.common.mapper.CommentMapper">
<sql id="table">comment</sql>
<!-- 主评论 -->
<select id="list" resultMap="listResult">
<select id="selectByPage" resultMap="selectResult">
SELECT
-- 主评论
comment.id,
@ -77,7 +77,7 @@
ON
reply_count.id = comment.id
</select>
<resultMap id="listResult" type="com.imyeyu.api.modules.common.vo.comment.CommentView">
<resultMap id="selectResult" type="com.imyeyu.api.modules.common.vo.comment.CommentView">
<id property="id" column="id" />
<result property="userId" column="user_id" />
<result property="nick" column="nick" />
@ -96,7 +96,7 @@
<result property="createdAt" column="reply_created_at" />
</collection>
</resultMap>
<select id="countAll" resultType="long">
<select id="countByPage" resultType="long">
SELECT
SUM(result.total_comment + result.total_reply)
FROM (

View File

@ -31,7 +31,7 @@
<resultMap id="listByRepositoryIdResult" type="com.imyeyu.api.modules.git.vo.release.ReleaseView" autoMapping="true">
<id property="id" column="id" />
<!-- 关联附件 -->
<collection property="attachmentList" ofType="com.imyeyu.api.modules.common.vo.attachment.AttachmentView">
<collection property="attachmentList" ofType="com.imyeyu.api.modules.common.entity.Attachment">
<id property="id" column="attachmentId" />
<result property="bizType" column="biz_type" />
<result property="bizId" column="biz_id" />

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.journal.mapper.JournalMapper">
<sql id="table">journal</sql>
<select id="listByIds" resultType="com.imyeyu.api.modules.journal.entity.Journal">
SELECT
*
FROM
<include refid="table" />
WHERE
1 = 1
<if test="ids != null and 0 &lt; ids.length">
AND `id` IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
AND deleted_at IS NULL
</select>
</mapper>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.imyeyu.api.modules.journal.mapper.TravelLocationMapper">
<sql id="table">travel_location</sql>
<select id="listByIds" resultType="com.imyeyu.api.modules.journal.entity.TravelLocation">
SELECT
*
FROM
<include refid="table" />
WHERE
1 = 1
<if test="ids != null and 0 &lt; ids.length">
AND `id` IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
AND deleted_at IS NULL
</select>
</mapper>

View File

@ -1,371 +0,0 @@
package test;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imyeyu.io.IO;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.CallbackArgReturn;
import com.imyeyu.java.bean.Language;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.java.ref.Ref;
import com.imyeyu.network.FormMap;
import com.imyeyu.api.TimiServerAPI;
import com.imyeyu.api.modules.common.entity.Multilingual;
import com.imyeyu.api.modules.common.mapper.MultilingualMapper;
import com.imyeyu.utils.Digest;
import com.imyeyu.utils.Encoder;
import com.imyeyu.utils.Time;
import lombok.AllArgsConstructor;
import org.apache.hc.client5.http.fluent.Request;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@SpringBootTest(classes = TimiServerAPI.class)
@RunWith(SpringRunner.class)
public class SpringLang {
@Autowired
private MultilingualMapper mapper;
private static final String API = "http://api.fanyi.baidu.com/api/trans/vip/translate?";
@Test
public void temp() throws Exception {
List<Multilingual> multilinguals = mapper.selectByKeyLike(".exception.");
for (int i = 0; i < multilinguals.size(); i++) {
Multilingual item = multilinguals.get(i);
item.setKey(item.getKey().replaceAll("exception", "tips"));
mapper.update(item);
}
}
@Test
public void addLang() throws Exception {
Map<String, String> map = new HashMap<>();
for (Map.Entry<String, String> item : map.entrySet()) {
Multilingual exist = mapper.selectByKey(item.getKey());
if (exist != null) {
System.err.println("exist key: " + item.getKey());
continue;
}
Multilingual multilingual = new Multilingual();
multilingual.setKey(item.getKey());
multilingual.setZhCN(item.getValue());
multilingual.setCreatedAt(Time.now());
mapper.insert(multilingual);
}
}
@Test
public void addLang4File() throws Exception {
Properties properties = new Properties();
properties.load(IO.getInputStream(new File("newLang.properties")));
for (Map.Entry<Object, Object> item : properties.entrySet()) {
Multilingual exist = mapper.selectByKey(item.getKey().toString());
if (exist != null) {
System.err.println("exist key: " + item.getKey());
continue;
}
Multilingual multilingual = new Multilingual();
multilingual.setKey(item.getKey().toString());
multilingual.setZhCN(item.getValue().toString());
multilingual.setCreatedAt(Time.now());
mapper.insert(multilingual);
}
}
@Test
public void appendTranslateResult4Path() throws Exception {
String path = "";
// DB_KEY, FILE_KEY
Map<String, String> keyMap = new HashMap<>();
keyMap.put("", "");
for (Map.Entry<String, String> item : keyMap.entrySet()) {
Language[] languages = Language.values();
for (int i = 0; i < languages.length; i++) {
if (languages[i] == Language.zh_CN) {
continue;
}
Properties properties = new Properties();
File file = new File(path + "%s.lang".formatted(languages[i].toString()));
if (!file.exists()) {
System.err.println(languages[i] + " not exist");
continue;
}
properties.load(IO.getInputStream(file));
Multilingual multilingual = mapper.selectByKey(item.getKey());
String fieldName = languages[i].toString().replace("_", "");
Ref.setFieldValue(multilingual, fieldName, properties.getProperty(item.getValue()));
mapper.update(multilingual);
}
}
}
@Test
public void upper() {
List<Multilingual> list = mapper.list(0, 9999);
for (int i = 0; i < list.size(); i++) {
Multilingual multilingual = list.get(i);
multilingual.setEnUS(String.valueOf(multilingual.getEnUS().charAt(0)).toUpperCase() + multilingual.getEnUS().substring(1));
mapper.update(multilingual);
}
}
@Test
public void translate() throws Exception {
List<Multilingual> data = mapper.selectByNotTranslate();
Map<String, Multilingual> cnMap = new HashMap<>();
for (int i = 0; i < data.size(); i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < Math.min(data.size() - i, 20); j++, i++) {
Multilingual multilingual = data.get(i);
sb.append(multilingual.getZhCN()).append("\r\n");
cnMap.put(multilingual.getZhCN(), multilingual);
}
i--;
System.out.println("translate " + sb);
List<BaiduLanguage> languageList = BaiduLanguage.valuesWithout(BaiduLanguage.ZH);
for (int j = 0; j < languageList.size(); j++) {
Map<String, String> result = doTranslate(languageList.get(j), sb.toString());
for (Map.Entry<String, String> item : result.entrySet()) {
Multilingual multilingual = cnMap.get(item.getKey());
Language lang = languageList.get(j).language;
String value = multilingual.getValue(lang);
if (TimiJava.isEmpty(value)) {
Ref.setFieldValue(multilingual, lang.toString().replace("_", ""), item.getValue());
}
mapper.update(multilingual);
}
}
Thread.sleep(1000);
}
}
// 数据库查重
// SELECT * FROM multilingual WHERE zh_cn IN (SELECT zh_cn FROM multilingual GROUP BY zh_cn HAVING COUNT(zh_cn) > 1);
// SELECT * FROM multilingual WHERE key IN (SELECT key FROM multilingual GROUP BY key HAVING COUNT(key) > 1);
@Test
public void export() throws Exception {
Pattern compile = Pattern.compile("\\.(text|textArgs)\\(\"(.*?)\"");
File scanDir = new File("E:\\IDEAProject\\ForeverMC\\src");
File outDir = new File("E:\\IDEAProject\\ForeverMC\\src\\main\\resources\\lang");
// 排除键
// String[] excludePath = {};
String[] excludePath = {"E:\\IDEAProject\\timi-fx-ui\\src\\main\\resources\\lang\\timi-fx-ui"};
Set<String> excludeKeys = new HashSet<>();
if (excludePath.length != 0) {
for (int i = 0; i < excludePath.length; i++) {
InputStream is = IO.getInputStream(new File(excludePath[i] + "\\zh_CN.lang"));
Properties properties = new Properties();
properties.load(is);
Map<Object, Object> map = new HashMap<>(properties);
excludeKeys.addAll(map.keySet().stream().map(Object::toString).collect(Collectors.toSet()));
}
}
// 扫描文件获取键
List<File> files = IO.listFile(scanDir);
Set<String> keySet = new HashSet<>();
for (int i = 0; i < files.size(); i++) {
File file = files.get(i);
if (file.isDirectory()) {
continue;
}
if (!file.getName().endsWith(".java")) {
continue;
}
String fileData = IO.toString(file);
Matcher matcher = compile.matcher(fileData);
while (matcher.find()) {
String key = matcher.group(2);
if (!excludeKeys.contains(key)) {
keySet.add(key);
}
}
}
StringBuilder notFoundKey = new StringBuilder();
List<String> keyList = new ArrayList<>(keySet);
keyList.sort(Comparator.naturalOrder());
List<Multilingual> result = mapper.selectByKeyList(keyList);
{
List<String> deepQueryKeyList;
CallbackArgReturn<List<Multilingual>, List<String>> newKeyList = mList -> {
List<String> arr = new ArrayList<>();
for (int i = 0; i < mList.size(); i++) {
if (mList.get(i).getZhCN().startsWith("@")) {
String newKey = mList.get(i).getZhCN().substring(1);
if (!keyList.contains(newKey)) {
arr.add(newKey);
}
}
}
return arr;
};
deepQueryKeyList = newKeyList.handler(result);
do {
List<Multilingual> deepResult = new ArrayList<>();
for (int i = 0; i < deepQueryKeyList.size(); i++) {
Multilingual r = mapper.selectByKey(deepQueryKeyList.get(i));
if (r == null) {
notFoundKey.append("[deep]").append(deepQueryKeyList.get(i)).append('\n');
} else {
deepResult.add(r);
}
}
result.addAll(deepResult);
deepQueryKeyList = newKeyList.handler(deepResult);
} while (!deepQueryKeyList.isEmpty());
}
result.sort(Comparator.comparing(Multilingual::getKey));
Set<String> resultKey = result.stream().map(Multilingual::getKey).collect(Collectors.toSet());
for (String key : keyList) {
if (!resultKey.contains(key)) {
notFoundKey.append(key).append('\n');
}
}
if (!notFoundKey.isEmpty()) {
System.err.printf("not found key in db: %n------%n%s------%n", notFoundKey);
} else {
// 输出文件
Language[] languages = Language.values();
for (int i = 0; i < languages.length; i++) {
String fieldName = languages[i].toString().replace("_", "");
File file = IO.file(IO.fitPath(outDir.getAbsolutePath()) + languages[i] + ".lang");
StringBuilder sb = new StringBuilder();
for (int j = 0; j < result.size(); j++) {
String value = Ref.getFieldValue(result.get(j), fieldName, String.class).trim();
value = value.replaceAll("\n", "\\\\n\\\\\n");
sb.append(result.get(j).getKey()).append("=").append(value);
sb.append('\n');
}
IO.toFile(file, sb.toString());
}
System.out.println("write successful");
}
}
/**
* 文本翻译
*
* @param text 原文本
* @param to 目标语言
* @return Map&lt;原数据,翻译结果&gt;
* @throws Exception 翻译异常
*/
private synchronized Map<String, String> doTranslate(BaiduLanguage to, String text) throws Exception {
String random = String.valueOf(System.currentTimeMillis());
String appId = "20180920000210118";
String key = "MfI4Iu0go3541Ryx3f6K";
FormMap<String, Object> args = new FormMap<>();
args.put("q", text);
args.put("from", "ZH".toLowerCase());
args.put("to", to.toString().toLowerCase());
args.put("appid", appId);
args.put("salt", random);
args.put("sign", Digest.md5(appId + text + random + key));
String response = Request.post(API + Encoder.urlArgs(args)).bodyForm(args.build()).execute().returnContent().asString();
JsonObject jo = JsonParser.parseString(response).getAsJsonObject();
if (jo.has("error_code")) {
System.err.println(jo);
throw new TimiException(TimiCode.ERROR, jo.get("error_msg").getAsString());
}
JsonArray ja = jo.get("trans_result").getAsJsonArray();
JsonObject resultJO;
Map<String, String> result = new HashMap<>();
for (int i = 0; i < ja.size(); i++) {
resultJO = ja.get(i).getAsJsonObject();
result.put(resultJO.get("src").getAsString(), resultJO.get("dst").getAsString());
}
wait(200);
return result;
}
@AllArgsConstructor
public enum BaiduLanguage {
ZH(Language.zh_CN),
EN(Language.en_US),
JP(Language.ja_JP),
KOR(Language.ko_KR),
RU(Language.ru_RU),
DE(Language.de_DE),
CHT(Language.zh_TW);
/** 标准映射 */
final Language language;
/**
* 根据映射查找
*
* @param language 映射
* @return 枚举对象
*/
static BaiduLanguage fromMapping(Language language) {
for (BaiduLanguage type : BaiduLanguage.values()) {
if (type.language.toString().equalsIgnoreCase(language.toString())) {
return type;
}
}
return null;
}
/**
* 获取排除语言列表
*
* @param baiduLanguage 排除语言
* @return 语言列表
*/
static List<BaiduLanguage> valuesWithout(BaiduLanguage... baiduLanguage) {
Set<BaiduLanguage> outList = Set.of(baiduLanguage);
List<BaiduLanguage> result = new ArrayList<>();
BaiduLanguage[] values = values();
for (int i = 0; i < values.length; i++) {
if (!outList.contains(values[i])) {
result.add(values[i]);
}
}
return result;
}
}
}