Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce695463d3 | ||
|
|
83c683d473 | ||
|
|
9555474b0b | ||
|
|
a150f9e017 | ||
|
|
ecd2fa1c33 | ||
|
|
e3236d262d | ||
|
|
c8e2e16ba5 | ||
|
|
99b44faf09 | ||
|
|
b3e3cbf84b | ||
|
|
0aee11d824 | ||
|
|
fcf358a768 | ||
|
|
619369104c | ||
|
|
4f1408b9d0 | ||
|
|
029eea70cc | ||
|
|
22b1e65385 | ||
|
|
2170c48763 | ||
|
|
c2d1c48aae | ||
|
|
e14958c643 | ||
|
|
da288338cd | ||
|
|
ea430f2468 | ||
|
|
f76f79744a | ||
|
|
19daaff907 | ||
|
|
db94d8f932 | ||
|
|
53618fe362 | ||
|
|
523edffc4e | ||
|
|
c7ddb1a8b0 | ||
|
|
45c9fc814a | ||
|
|
407dc13ac4 | ||
|
|
0c06bf16c2 | ||
|
|
1db39e77d3 |
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<groupId>com.imyeyu.timiserverapi</groupId>
|
<groupId>com.imyeyu.timiserverapi</groupId>
|
||||||
<artifactId>TimiServerAPI</artifactId>
|
<artifactId>TimiServerAPI</artifactId>
|
||||||
<version>1.0.22</version>
|
<version>1.0.21</version>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
<name>TimiServerAPI</name>
|
<name>TimiServerAPI</name>
|
||||||
<description>imyeyu.com API</description>
|
<description>imyeyu.com API</description>
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.imyeyu.font.icon</groupId>
|
<groupId>com.imyeyu.font.icon</groupId>
|
||||||
<artifactId>timi-icon-font</artifactId>
|
<artifactId>timi-icon-font</artifactId>
|
||||||
<version>0.0.3</version>
|
<version>0.0.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -240,6 +240,11 @@
|
|||||||
<groupId>org.flywaydb</groupId>
|
<groupId>org.flywaydb</groupId>
|
||||||
<artifactId>flyway-mysql</artifactId>
|
<artifactId>flyway-mysql</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.e2ins.ms.notify</groupId>
|
||||||
|
<artifactId>e2ins-msg-notify-service-sdk</artifactId>
|
||||||
|
<version>0.0.9</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.tencentcloudapi</groupId>
|
<groupId>com.tencentcloudapi</groupId>
|
||||||
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
<artifactId>tencentcloud-sdk-java-sms</artifactId>
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.imyeyu.api.config;
|
package com.imyeyu.api.config;
|
||||||
|
|
||||||
|
import com.e2ins.ms.notify.SmsNotifyService;
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||||
import com.fasterxml.jackson.databind.MapperFeature;
|
import com.fasterxml.jackson.databind.MapperFeature;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||||
|
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import com.imyeyu.utils.Time;
|
import com.imyeyu.utils.Time;
|
||||||
@@ -29,6 +32,12 @@ public class BeanConfig {
|
|||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SmsNotifyService smsNotifyService() {
|
||||||
|
// TODO
|
||||||
|
return new SmsNotifyService(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public Yaml yaml() {
|
public Yaml yaml() {
|
||||||
return new Yaml();
|
return new Yaml();
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.imyeyu.api.modules.common.bean.attachment;
|
||||||
|
|
||||||
|
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 String sourceId;
|
||||||
|
|
||||||
|
private String sourceMongoId;
|
||||||
|
|
||||||
|
private String sourceMimeType;
|
||||||
|
|
||||||
|
/** 请求宽度(缓存键之一,null 表示未指定) */
|
||||||
|
private Integer requestWidth;
|
||||||
|
|
||||||
|
/** 请求高度(缓存键之一,null 表示未指定) */
|
||||||
|
private Integer requestHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package com.imyeyu.api.modules.common.bean.attachment;
|
|
||||||
|
|
||||||
/// 缩略图缩放模式
|
|
||||||
///
|
|
||||||
/// @author 夜雨
|
|
||||||
/// @since 2026-08-21
|
|
||||||
public enum ThumbnailFitMode {
|
|
||||||
|
|
||||||
/// 保持比例完整放入目标尺寸
|
|
||||||
FIT,
|
|
||||||
|
|
||||||
/// 保持比例裁剪填满目标尺寸
|
|
||||||
COVER,
|
|
||||||
|
|
||||||
/// 强制缩放到目标尺寸,可能导致图片变形
|
|
||||||
STRETCH
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
package com.imyeyu.api.modules.common.controller;
|
package com.imyeyu.api.modules.common.controller;
|
||||||
|
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
|
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
|
|
||||||
import com.imyeyu.api.modules.common.service.TempFileService;
|
import com.imyeyu.api.modules.common.service.TempFileService;
|
||||||
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
|
import com.imyeyu.api.modules.common.vo.attach.TempFileResp;
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
@@ -43,7 +41,6 @@ public class AttachmentController {
|
|||||||
|
|
||||||
private final TempFileService tempFileService;
|
private final TempFileService tempFileService;
|
||||||
private final AttachmentService service;
|
private final AttachmentService service;
|
||||||
private final AttachmentVariantService variantService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询附件详情
|
* 查询附件详情
|
||||||
@@ -187,19 +184,14 @@ public class AttachmentController {
|
|||||||
@AOPLog
|
@AOPLog
|
||||||
@IgnoreGlobalReturn
|
@IgnoreGlobalReturn
|
||||||
@GetMapping({"/read/{id}", "/download/{id}"})
|
@GetMapping({"/read/{id}", "/download/{id}"})
|
||||||
public void readById(
|
public void readById(@PathVariable String id, @RequestParam(required = false) Integer thumbWidth, @RequestParam(required = false) Integer thumbHeight) throws UnsupportedEncodingException {
|
||||||
@PathVariable String id,
|
|
||||||
@RequestParam(required = false) Integer thumbWidth,
|
|
||||||
@RequestParam(required = false) Integer thumbHeight,
|
|
||||||
@RequestParam(defaultValue = "FIT") ThumbnailFitMode fitMode
|
|
||||||
) throws UnsupportedEncodingException {
|
|
||||||
HttpServletResponse resp = TimiSpring.getResponse();
|
HttpServletResponse resp = TimiSpring.getResponse();
|
||||||
Attachment attach = service.get(id);
|
Attachment attach = service.get(id);
|
||||||
if (attach == null) {
|
if (attach == null) {
|
||||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
responseAttachment(attach, thumbWidth, thumbHeight, fitMode);
|
responseAttachment(attach, thumbWidth, thumbHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AOPLog
|
@AOPLog
|
||||||
@@ -215,31 +207,21 @@ public class AttachmentController {
|
|||||||
public void read(@RequestParam String id) throws UnsupportedEncodingException {
|
public void read(@RequestParam String id) throws UnsupportedEncodingException {
|
||||||
HttpServletResponse resp = TimiSpring.getResponse();
|
HttpServletResponse resp = TimiSpring.getResponse();
|
||||||
Attachment attach = service.get(id);
|
Attachment attach = service.get(id);
|
||||||
if (attach == null) {
|
|
||||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (TimiSpring.getRequest().getRequestURI().endsWith("/download")) {
|
if (TimiSpring.getRequest().getRequestURI().endsWith("/download")) {
|
||||||
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
|
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
|
||||||
}
|
}
|
||||||
response(attach, false);
|
response(attach);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void responseAttachment(
|
private void responseAttachment(Attachment attach, Integer thumbWidth, Integer thumbHeight) throws UnsupportedEncodingException {
|
||||||
Attachment attach,
|
HttpServletResponse resp = TimiSpring.getResponse();
|
||||||
Integer thumbWidth,
|
if (attach != null && TimiSpring.getRequest().getRequestURI().endsWith("/download") && attach.getName() != null) {
|
||||||
Integer thumbHeight,
|
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
|
||||||
ThumbnailFitMode fitMode
|
}
|
||||||
) throws UnsupportedEncodingException {
|
responseThumb(attach, thumbWidth, thumbHeight);
|
||||||
responseThumb(attach, thumbWidth, thumbHeight, fitMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void responseThumb(
|
protected void responseThumb(Attachment attachment, Integer thumbWidth, Integer thumbHeight) {
|
||||||
Attachment attachment,
|
|
||||||
Integer thumbWidth,
|
|
||||||
Integer thumbHeight,
|
|
||||||
ThumbnailFitMode fitMode
|
|
||||||
) throws UnsupportedEncodingException {
|
|
||||||
HttpServletResponse resp = TimiSpring.getResponse();
|
HttpServletResponse resp = TimiSpring.getResponse();
|
||||||
if (TimiJava.isEmpty(attachment)) {
|
if (TimiJava.isEmpty(attachment)) {
|
||||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
@@ -247,65 +229,21 @@ public class AttachmentController {
|
|||||||
}
|
}
|
||||||
if (thumbWidth == null && thumbHeight == null) {
|
if (thumbWidth == null && thumbHeight == null) {
|
||||||
// 原图
|
// 原图
|
||||||
setDownloadHeader(attachment);
|
|
||||||
response(attachment);
|
response(attachment);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 缩略图
|
// 缩略图
|
||||||
Attachment thumb = variantService.fetchThumb(attachment, thumbWidth, thumbHeight, fitMode);
|
Attachment thumb = service.fetchThumb(attachment, thumbWidth, thumbHeight);
|
||||||
setDownloadHeader(thumb);
|
response(TimiJava.defaultIfNull(thumb, attachment));
|
||||||
response(thumb);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDownloadHeader(Attachment attachment) throws UnsupportedEncodingException {
|
|
||||||
if (attachment != null && TimiSpring.getRequest().getRequestURI().endsWith("/download") && attachment.getName() != null) {
|
|
||||||
TimiSpring.getResponse().setHeader("Content-Disposition", Network.getFileDownloadHeader(attachment.getName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 设置附件缓存头并处理条件请求
|
|
||||||
///
|
|
||||||
/// @param attachment 实际响应附件
|
|
||||||
/// @return true 表示客户端缓存仍有效,已返回 304
|
|
||||||
private boolean responseCache(Attachment attachment) {
|
|
||||||
HttpServletResponse resp = TimiSpring.getResponse();
|
|
||||||
resp.setHeader("Cache-Control", "private, no-cache");
|
|
||||||
String md5 = attachment.getMd5();
|
|
||||||
if (md5 == null || md5.isBlank()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
String etag = "\"%s\"".formatted(md5);
|
|
||||||
resp.setHeader("ETag", etag);
|
|
||||||
String ifNoneMatch = TimiSpring.getHeader("If-None-Match");
|
|
||||||
if (ifNoneMatch == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
for (String value : ifNoneMatch.split(",")) {
|
|
||||||
String candidate = value.trim();
|
|
||||||
if ("*".equals(candidate) || etag.equals(candidate) || ("W/" + etag).equals(candidate)) {
|
|
||||||
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void response(Attachment attachment) {
|
protected void response(Attachment attachment) {
|
||||||
response(attachment, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void response(Attachment attachment, boolean cacheable) {
|
|
||||||
HttpServletResponse resp = TimiSpring.getResponse();
|
HttpServletResponse resp = TimiSpring.getResponse();
|
||||||
try {
|
try {
|
||||||
if (TimiJava.isEmpty(attachment)) {
|
if (TimiJava.isEmpty(attachment)) {
|
||||||
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!cacheable) {
|
|
||||||
resp.setHeader("Cache-Control", "private, no-store");
|
|
||||||
} else if (responseCache(attachment)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resp.setContentType(attachment.getMimeType());
|
resp.setContentType(attachment.getMimeType());
|
||||||
attachment.doResponse();
|
attachment.doResponse();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -7,16 +7,11 @@ import com.imyeyu.api.modules.common.entity.Notify;
|
|||||||
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
||||||
import com.imyeyu.api.modules.common.service.NotifyDetailService;
|
import com.imyeyu.api.modules.common.service.NotifyDetailService;
|
||||||
import com.imyeyu.api.modules.common.service.NotifyService;
|
import com.imyeyu.api.modules.common.service.NotifyService;
|
||||||
import com.imyeyu.api.modules.common.service.SmsService;
|
|
||||||
import com.imyeyu.api.modules.user.entity.User;
|
import com.imyeyu.api.modules.user.entity.User;
|
||||||
import com.imyeyu.api.modules.user.service.UserLoginService;
|
import com.imyeyu.api.modules.user.service.UserLoginService;
|
||||||
import com.imyeyu.api.modules.user.service.UserService;
|
import com.imyeyu.api.modules.user.service.UserService;
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
import com.imyeyu.spring.annotation.AOPLog;
|
|
||||||
import com.imyeyu.spring.annotation.CaptchaValid;
|
|
||||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
|
||||||
import com.imyeyu.spring.annotation.RequiredToken;
|
import com.imyeyu.spring.annotation.RequiredToken;
|
||||||
import com.imyeyu.spring.bean.CaptchaData;
|
|
||||||
import com.imyeyu.spring.bean.Page;
|
import com.imyeyu.spring.bean.Page;
|
||||||
import com.imyeyu.spring.bean.PageResult;
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
import com.imyeyu.spring.util.ResponseView;
|
import com.imyeyu.spring.util.ResponseView;
|
||||||
@@ -42,8 +37,6 @@ import java.util.stream.Stream;
|
|||||||
@RequestMapping("/notify")
|
@RequestMapping("/notify")
|
||||||
public class NotifyController {
|
public class NotifyController {
|
||||||
|
|
||||||
|
|
||||||
private final SmsService smsService;
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final NotifyService service;
|
private final NotifyService service;
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
@@ -111,14 +104,6 @@ public class NotifyController {
|
|||||||
service.cancel(id);
|
service.cancel(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AOPLog
|
|
||||||
@CaptchaValid
|
|
||||||
@RequestRateLimit(value = 1, inSeconds = 59)
|
|
||||||
@PostMapping("/sms/captcha/create")
|
|
||||||
public String smsCaptchaCreate(@RequestBody CaptchaData<Notify> req) {
|
|
||||||
return smsService.captchaCreate(req.getData());
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 查询当前用户的站内通知
|
/// 查询当前用户的站内通知
|
||||||
///
|
///
|
||||||
/// @param page 分页参数
|
/// @param page 分页参数
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
package com.imyeyu.api.modules.common.entity;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
|
|
||||||
import com.imyeyu.spring.annotation.table.AutoUUID;
|
|
||||||
import com.imyeyu.spring.annotation.table.Id;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/// 附件缩略图变体记录
|
|
||||||
///
|
|
||||||
/// @author 夜雨
|
|
||||||
/// @since 2026-08-21
|
|
||||||
@Data
|
|
||||||
public class AttachmentVariant {
|
|
||||||
|
|
||||||
/// 变体 ID
|
|
||||||
@Id
|
|
||||||
@AutoUUID
|
|
||||||
private String id;
|
|
||||||
|
|
||||||
/// 源附件 ID
|
|
||||||
private String sourceAttachmentId;
|
|
||||||
|
|
||||||
/// 源文件版本,使用源 GridFS ID
|
|
||||||
private String sourceMongoId;
|
|
||||||
|
|
||||||
/// 源文件摘要
|
|
||||||
private String sourceMd5;
|
|
||||||
|
|
||||||
/// 请求宽度,0 表示未指定
|
|
||||||
private int requestWidth;
|
|
||||||
|
|
||||||
/// 请求高度,0 表示未指定
|
|
||||||
private int requestHeight;
|
|
||||||
|
|
||||||
/// 缩放模式
|
|
||||||
private ThumbnailFitMode fitMode;
|
|
||||||
|
|
||||||
/// 输出格式
|
|
||||||
private String outputFormat;
|
|
||||||
|
|
||||||
/// 缩略图算法版本
|
|
||||||
private String algorithmVersion;
|
|
||||||
|
|
||||||
/// 缩略图 GridFS ID
|
|
||||||
private String mongoId;
|
|
||||||
|
|
||||||
/// 缩略图名称
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/// 缩略图 MIME 类型
|
|
||||||
private String mimeType;
|
|
||||||
|
|
||||||
/// 缩略图元数据
|
|
||||||
private ObjectNode metadata;
|
|
||||||
|
|
||||||
/// 缩略图大小
|
|
||||||
private long size;
|
|
||||||
|
|
||||||
/// 缩略图摘要
|
|
||||||
private String md5;
|
|
||||||
|
|
||||||
/// 创建时间
|
|
||||||
private long createdAt;
|
|
||||||
|
|
||||||
/// 过期时间
|
|
||||||
private long expireAt;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.imyeyu.api.modules.common.entity;
|
package com.imyeyu.api.modules.common.entity;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.imyeyu.spring.annotation.table.AutoUUID;
|
import com.imyeyu.spring.annotation.table.AutoUUID;
|
||||||
import com.imyeyu.spring.annotation.table.Id;
|
import com.imyeyu.spring.annotation.table.Id;
|
||||||
import com.imyeyu.spring.entity.Creatable;
|
import com.imyeyu.spring.entity.Creatable;
|
||||||
@@ -29,7 +28,7 @@ public class BaseQueueEntity implements IDEntity<String>, Creatable, Destroyable
|
|||||||
protected String data;
|
protected String data;
|
||||||
|
|
||||||
/** 参数 */
|
/** 参数 */
|
||||||
protected JsonNode args;
|
protected String args;
|
||||||
|
|
||||||
/** 发送去向 */
|
/** 发送去向 */
|
||||||
protected String sendTo;
|
protected String sendTo;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.imyeyu.api.modules.common.entity;
|
package com.imyeyu.api.modules.common.entity;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonView;
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
|
||||||
import com.imyeyu.api.TimiServerAPI;
|
import com.imyeyu.api.TimiServerAPI;
|
||||||
import com.imyeyu.api.modules.common.service.BaseQueueService;
|
import com.imyeyu.api.modules.common.service.BaseQueueService;
|
||||||
import com.imyeyu.api.modules.common.service.HTTPQueueService;
|
import com.imyeyu.api.modules.common.service.HTTPQueueService;
|
||||||
@@ -104,7 +103,7 @@ public class NotifyDetail extends UUIDEntity {
|
|||||||
|
|
||||||
/** 参数 */
|
/** 参数 */
|
||||||
@JsonView(ResponseView.Public.class)
|
@JsonView(ResponseView.Public.class)
|
||||||
private JsonNode args;
|
private String args;
|
||||||
|
|
||||||
/** 发送来源 */
|
/** 发送来源 */
|
||||||
@JsonView(ResponseView.Public.class)
|
@JsonView(ResponseView.Public.class)
|
||||||
|
|||||||
@@ -36,6 +36,26 @@ public interface AttachmentMapper extends BaseMapper<Attachment, String>, RawMap
|
|||||||
|
|
||||||
long countByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes);
|
long countByBizId(Attachment.BizType bizType, String bizId, List<String> attachTypes);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按源附件 ID 与请求尺寸查询已缓存缩略图
|
||||||
|
*
|
||||||
|
* @param bizType 源附件业务类型
|
||||||
|
* @param sourceId 源附件 ID
|
||||||
|
* @param requestWidth 请求宽度,{@code null} 表示未指定
|
||||||
|
* @param requestHeight 请求高度,{@code null} 表示未指定
|
||||||
|
* @return 缩略图附件,不存在返回 {@code null}
|
||||||
|
*/
|
||||||
|
Attachment selectThumb(Attachment.BizType bizType, String sourceId, Integer requestWidth, Integer requestHeight);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询源附件的所有未销毁缩略图(含已软删除)
|
||||||
|
*
|
||||||
|
* @param sourceId 源附件 ID
|
||||||
|
* @return 缩略图列表
|
||||||
|
*/
|
||||||
|
@Select("SELECT * FROM attachment WHERE biz_id = #{sourceId} AND attach_type = 'THUMB' AND is_destroyed = FALSE")
|
||||||
|
List<Attachment> selectThumbsBySourceId(String sourceId);
|
||||||
|
|
||||||
@Select("SELECT * FROM attachment WHERE `is_destroyed` = FALSE AND `destroy_at` < " + UNIX_TIME)
|
@Select("SELECT * FROM attachment WHERE `is_destroyed` = FALSE AND `destroy_at` < " + UNIX_TIME)
|
||||||
List<Attachment> selectNeedDestroy();
|
List<Attachment> selectNeedDestroy();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
package com.imyeyu.api.modules.common.mapper;
|
|
||||||
|
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
|
||||||
import com.imyeyu.api.modules.common.entity.AttachmentVariant;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/// 附件缩略图变体 Mapper
|
|
||||||
///
|
|
||||||
/// @author 夜雨
|
|
||||||
/// @since 2026-08-21
|
|
||||||
public interface AttachmentVariantMapper {
|
|
||||||
|
|
||||||
/// 查询有效缩略图附件
|
|
||||||
///
|
|
||||||
/// @param sourceAttachmentId 源附件 ID
|
|
||||||
/// @param sourceMongoId 源 GridFS ID
|
|
||||||
/// @param requestWidth 请求宽度,0 表示未指定
|
|
||||||
/// @param requestHeight 请求高度,0 表示未指定
|
|
||||||
/// @param fitMode 缩放模式
|
|
||||||
/// @param outputFormat 输出格式
|
|
||||||
/// @param algorithmVersion 算法版本
|
|
||||||
/// @param now 当前时间
|
|
||||||
/// @return 缩略图附件
|
|
||||||
Attachment selectValidAttachment(
|
|
||||||
@Param("sourceAttachmentId") String sourceAttachmentId,
|
|
||||||
@Param("sourceMongoId") String sourceMongoId,
|
|
||||||
@Param("requestWidth") int requestWidth,
|
|
||||||
@Param("requestHeight") int requestHeight,
|
|
||||||
@Param("fitMode") String fitMode,
|
|
||||||
@Param("outputFormat") String outputFormat,
|
|
||||||
@Param("algorithmVersion") String algorithmVersion,
|
|
||||||
@Param("now") long now
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 查询指定变体记录,不判断是否过期
|
|
||||||
///
|
|
||||||
/// @param sourceAttachmentId 源附件 ID
|
|
||||||
/// @param sourceMongoId 源 GridFS ID
|
|
||||||
/// @param requestWidth 请求宽度,0 表示未指定
|
|
||||||
/// @param requestHeight 请求高度,0 表示未指定
|
|
||||||
/// @param fitMode 缩放模式
|
|
||||||
/// @param outputFormat 输出格式
|
|
||||||
/// @param algorithmVersion 算法版本
|
|
||||||
/// @return 变体记录
|
|
||||||
AttachmentVariant selectByKey(
|
|
||||||
@Param("sourceAttachmentId") String sourceAttachmentId,
|
|
||||||
@Param("sourceMongoId") String sourceMongoId,
|
|
||||||
@Param("requestWidth") int requestWidth,
|
|
||||||
@Param("requestHeight") int requestHeight,
|
|
||||||
@Param("fitMode") String fitMode,
|
|
||||||
@Param("outputFormat") String outputFormat,
|
|
||||||
@Param("algorithmVersion") String algorithmVersion
|
|
||||||
);
|
|
||||||
|
|
||||||
/// 查询源附件的所有变体
|
|
||||||
///
|
|
||||||
/// @param sourceAttachmentId 源附件 ID
|
|
||||||
/// @return 变体列表
|
|
||||||
List<AttachmentVariant> selectBySourceAttachmentId(@Param("sourceAttachmentId") String sourceAttachmentId);
|
|
||||||
|
|
||||||
/// 查询已过期变体
|
|
||||||
///
|
|
||||||
/// @param now 当前时间
|
|
||||||
/// @return 已过期变体列表
|
|
||||||
List<AttachmentVariant> selectExpired(@Param("now") long now);
|
|
||||||
|
|
||||||
/// 新增变体
|
|
||||||
///
|
|
||||||
/// @param variant 变体
|
|
||||||
void insert(AttachmentVariant variant);
|
|
||||||
|
|
||||||
/// 删除变体
|
|
||||||
///
|
|
||||||
/// @param id 变体 ID
|
|
||||||
void delete(@Param("id") String id);
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,18 @@ public interface AttachmentService extends BaseService<Attachment, String> {
|
|||||||
/// @return 克隆后的附件
|
/// @return 克隆后的附件
|
||||||
Attachment clone(String id);
|
Attachment clone(String id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按需获取或创建缩略图附件。
|
||||||
|
* <p>当附件为图片或视频时,若指定尺寸的缩略图已存在则直接返回缓存,
|
||||||
|
* 否则生成并持久化后返回。不支持缩略图的附件类型返回 {@code null}。</p>
|
||||||
|
*
|
||||||
|
* @param source 源附件(必须已持久化,mimeType 已填充)
|
||||||
|
* @param requestWidth 请求宽度,{@code null} 表示未指定
|
||||||
|
* @param requestHeight 请求高度,{@code null} 表示未指定
|
||||||
|
* @return 缩略图附件,不支持时返回 {@code null}
|
||||||
|
*/
|
||||||
|
Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight);
|
||||||
|
|
||||||
Attachment getByBizId(Attachment.BizType bizType, String bizId);
|
Attachment getByBizId(Attachment.BizType bizType, String bizId);
|
||||||
|
|
||||||
Attachment getByAttachType(Attachment.BizType bizType, String bizId, String attachType);
|
Attachment getByAttachType(Attachment.BizType bizType, String bizId, String attachType);
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package com.imyeyu.api.modules.common.service;
|
|
||||||
|
|
||||||
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
|
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
|
||||||
|
|
||||||
/// 附件缩略图变体服务
|
|
||||||
///
|
|
||||||
/// @author 夜雨
|
|
||||||
/// @since 2026-08-21
|
|
||||||
public interface AttachmentVariantService {
|
|
||||||
|
|
||||||
/// 按需获取或创建缩略图附件
|
|
||||||
///
|
|
||||||
/// @param source 源附件
|
|
||||||
/// @param requestWidth 请求宽度
|
|
||||||
/// @param requestHeight 请求高度
|
|
||||||
/// @param fitMode 缩放模式
|
|
||||||
/// @return 缩略图附件
|
|
||||||
Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight, ThumbnailFitMode fitMode);
|
|
||||||
|
|
||||||
/// 删除源附件对应的全部缩略图变体
|
|
||||||
///
|
|
||||||
/// @param sourceAttachmentId 源附件 ID
|
|
||||||
void deleteBySourceId(String sourceAttachmentId);
|
|
||||||
|
|
||||||
/// 清理已过期的缩略图变体
|
|
||||||
void clearExpired();
|
|
||||||
}
|
|
||||||
@@ -1,30 +1,30 @@
|
|||||||
package com.imyeyu.api.modules.common.service;
|
//package com.imyeyu.api.modules.common.service;
|
||||||
|
//
|
||||||
import com.imyeyu.api.modules.common.entity.Notify;
|
//import com.e2ins.ms.notify.entity.Notify;
|
||||||
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
//import com.e2ins.ms.notify.entity.NotifyDetail;
|
||||||
|
//
|
||||||
/**
|
///**
|
||||||
*
|
// *
|
||||||
*
|
// *
|
||||||
* @author 夜雨
|
// * @author 夜雨
|
||||||
* @since 2026-05-14 12:12
|
// * @since 2026-05-14 12:12
|
||||||
*/
|
// */
|
||||||
public interface SmsService {
|
//public interface SmsService {
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 创建短信验证码通知
|
// * 创建短信验证码通知
|
||||||
*
|
// *
|
||||||
* @param detail 通知
|
// * @param detail 通知
|
||||||
* @return 通知详情 ID
|
// * @return 通知详情 ID
|
||||||
*/
|
// */
|
||||||
String captchaCreate(Notify detail);
|
// String captchaCreate(Notify detail);
|
||||||
|
//
|
||||||
/**
|
// /**
|
||||||
* 校验短信验证码
|
// * 校验短信验证码
|
||||||
*
|
// *
|
||||||
* @param detailId 通知详情 ID
|
// * @param detailId 通知详情 ID
|
||||||
* @param captcha 请求验证码
|
// * @param captcha 请求验证码
|
||||||
* @return 通知详情
|
// * @return 通知详情
|
||||||
*/
|
// */
|
||||||
NotifyDetail captchaVerify(String detailId, String captcha);
|
// NotifyDetail captchaVerify(String detailId, String captcha);
|
||||||
}
|
//}
|
||||||
|
|||||||
+100
-24
@@ -3,14 +3,19 @@ package com.imyeyu.api.modules.common.service.implement;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
|
import com.imyeyu.api.modules.common.bean.attachment.MediaAttach;
|
||||||
|
import com.imyeyu.api.modules.common.bean.attachment.Metadata;
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
import com.imyeyu.api.modules.common.mapper.AttachmentMapper;
|
import com.imyeyu.api.modules.common.mapper.AttachmentMapper;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
|
import com.imyeyu.api.util.JavaCV;
|
||||||
import com.imyeyu.io.IO;
|
import com.imyeyu.io.IO;
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.bean.CallbackArg;
|
||||||
import com.imyeyu.java.bean.timi.TimiCode;
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
import com.imyeyu.java.bean.timi.TimiException;
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
import com.imyeyu.java.ref.Ref;
|
||||||
|
import com.imyeyu.network.Network;
|
||||||
import com.imyeyu.spring.TimiSpring;
|
import com.imyeyu.spring.TimiSpring;
|
||||||
import com.imyeyu.spring.bean.Page;
|
import com.imyeyu.spring.bean.Page;
|
||||||
import com.imyeyu.spring.bean.PageResult;
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
@@ -21,6 +26,7 @@ import com.mongodb.client.gridfs.GridFSBucket;
|
|||||||
import com.mongodb.client.gridfs.model.GridFSFile;
|
import com.mongodb.client.gridfs.model.GridFSFile;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.coobird.thumbnailator.Thumbnails;
|
||||||
import org.apache.tika.Tika;
|
import org.apache.tika.Tika;
|
||||||
import org.springframework.data.mongodb.core.query.Criteria;
|
import org.springframework.data.mongodb.core.query.Criteria;
|
||||||
import org.springframework.data.mongodb.core.query.Query;
|
import org.springframework.data.mongodb.core.query.Query;
|
||||||
@@ -30,6 +36,9 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -38,6 +47,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +64,6 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
private final ObjectMapper jackson;
|
private final ObjectMapper jackson;
|
||||||
private final GridFSBucket gridFSBucket;
|
private final GridFSBucket gridFSBucket;
|
||||||
private final GridFsTemplate gridFsTemplate;
|
private final GridFsTemplate gridFsTemplate;
|
||||||
private final AttachmentVariantService variantService;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected BaseMapper<Attachment, String> mapper() {
|
protected BaseMapper<Attachment, String> mapper() {
|
||||||
@@ -115,7 +124,6 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
}
|
}
|
||||||
// 有新文件时,先查旧记录获取旧 mongoId 和兜底字段
|
// 有新文件时,先查旧记录获取旧 mongoId 和兜底字段
|
||||||
Attachment old = mapper.selectRaw(attachment.getId());
|
Attachment old = mapper.selectRaw(attachment.getId());
|
||||||
TimiException.required(old, "not found attachment");
|
|
||||||
// 存储新文件
|
// 存储新文件
|
||||||
newMongoId = gridFsTemplate.store(stream, attachment.buildMongoName()).toString();
|
newMongoId = gridFsTemplate.store(stream, attachment.buildMongoName()).toString();
|
||||||
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(newMongoId)));
|
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(newMongoId)));
|
||||||
@@ -133,8 +141,6 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
|
|
||||||
// 更新数据库后删除旧 GridFS 文件
|
// 更新数据库后删除旧 GridFS 文件
|
||||||
super.update(attachment);
|
super.update(attachment);
|
||||||
// 源文件版本已变化,旧缩略图全部失效
|
|
||||||
variantService.deleteBySourceId(attachment.getId());
|
|
||||||
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(old.getMongoId())));
|
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(old.getMongoId())));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
|
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
|
||||||
@@ -149,18 +155,36 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void delete(String id) {
|
public void delete(String id) {
|
||||||
variantService.deleteBySourceId(id);
|
mapper.selectThumbsBySourceId(id).forEach(thumb -> super.delete(thumb.getId()));
|
||||||
super.delete(id);
|
super.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void destroy(String id) {
|
public void destroy(String id) {
|
||||||
variantService.deleteBySourceId(id);
|
CallbackArg<Attachment> doDestroy = attach -> {
|
||||||
Attachment attachment = mapper.selectRaw(id);
|
try {
|
||||||
if (attachment != null) {
|
if (attach.getMongoId() != null) {
|
||||||
destroyAttachment(attachment);
|
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attach.getMongoId())));
|
||||||
}
|
}
|
||||||
|
if (!attach.isDeleted()) {
|
||||||
|
attach.setDeletedAt(Time.now());
|
||||||
|
}
|
||||||
|
attach.setIsDestroyed(true);
|
||||||
|
attach.setDestroyAt(Time.now());
|
||||||
|
mapper.update(attach);
|
||||||
|
|
||||||
|
if (Ref.toType(MediaAttach.Type.class, attach.getAttachType()) == MediaAttach.Type.THUMB) {
|
||||||
|
// 缩略图记录物理删除
|
||||||
|
mapper.destroy(attach.getId());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("delete mongo file error, id={}", attach.getId(), e);
|
||||||
|
throw new TimiException(TimiCode.ERROR, "TODO delete mongo file error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
mapper.selectThumbsBySourceId(id).forEach(doDestroy::handler);
|
||||||
|
doDestroy.handler(mapper.selectRaw(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@@ -192,20 +216,74 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void destroyAttachment(Attachment attachment) {
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight) {
|
||||||
|
TimiException.requiredTrue(requestWidth != null || requestHeight != null, "required width or height");
|
||||||
|
String mimeType = source.getMimeType();
|
||||||
|
boolean isImage = mimeType != null && Set.of("image/png", "image/bmp", "image/jpeg").contains(mimeType);
|
||||||
|
boolean isVideo = mimeType != null && Set.of("video/mp4", "video/quicktime").contains(mimeType);
|
||||||
|
if (!isImage && !isVideo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Attachment cached = mapper.selectThumb(source.getBizType(), source.getId(), requestWidth, requestHeight);
|
||||||
|
if (cached != null) {
|
||||||
|
// 命中缓存直接返回
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (attachment.getMongoId() != null) {
|
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
|
||||||
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attachment.getMongoId())));
|
CallbackArg<InputStream> applyThumbSize = inputStream -> {
|
||||||
|
try {
|
||||||
|
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
|
||||||
|
if (requestWidth != null && requestHeight != null) {
|
||||||
|
builder.size(requestWidth, requestHeight).keepAspectRatio(false).toOutputStream(thumbStream);
|
||||||
|
} else if (requestWidth != null) {
|
||||||
|
builder.width(requestWidth).keepAspectRatio(true).toOutputStream(thumbStream);
|
||||||
|
} else {
|
||||||
|
builder.height(requestHeight).keepAspectRatio(true).toOutputStream(thumbStream);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("applyThumbSize error for fetchThumb", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isImage) {
|
||||||
|
applyThumbSize.handler(source.openInputStream());
|
||||||
|
} else {
|
||||||
|
String name = source.getName();
|
||||||
|
File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID(), name));
|
||||||
|
try {
|
||||||
|
IO.toFile(tempFile, source.openInputStream());
|
||||||
|
ByteArrayOutputStream baos = JavaCV.captureThumbnail(IO.getInputStream(tempFile), 2);
|
||||||
|
applyThumbSize.handler(IO.toInputStream(baos));
|
||||||
|
} finally {
|
||||||
|
IO.destroy(tempFile);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!attachment.isDeleted()) {
|
Metadata.ThumbImage thumbMeta = new Metadata.ThumbImage();
|
||||||
attachment.setDeletedAt(Time.now());
|
thumbMeta.setSourceId(source.getId());
|
||||||
}
|
thumbMeta.setSourceMongoId(source.getMongoId());
|
||||||
attachment.setIsDestroyed(true);
|
thumbMeta.setSourceMimeType(mimeType);
|
||||||
attachment.setDestroyAt(Time.now());
|
thumbMeta.setRequestWidth(requestWidth);
|
||||||
mapper.update(attachment);
|
thumbMeta.setRequestHeight(requestHeight);
|
||||||
|
|
||||||
|
Attachment thumb = new Attachment();
|
||||||
|
thumb.setName(Network.simpleURIFileName(source.getName()) + ".png");
|
||||||
|
thumb.setBizType(source.getBizType());
|
||||||
|
thumb.setBizId(source.getId());
|
||||||
|
thumb.setAttachType(MediaAttach.Type.THUMB.toString());
|
||||||
|
thumb.setMetadata(jackson.valueToTree(thumbMeta));
|
||||||
|
thumb.setInputStream(new ByteArrayInputStream(thumbStream.toByteArray()));
|
||||||
|
// 缩略图 7 天后过期,额外 1 天宽限供后台任务销毁
|
||||||
|
long expireAt = Time.now() + 7 * Time.D;
|
||||||
|
thumb.setDeletedAt(expireAt);
|
||||||
|
thumb.setDestroyAt(expireAt + Time.D);
|
||||||
|
create(thumb);
|
||||||
|
return thumb;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("delete mongo file error, id={}", attachment.getId(), e);
|
log.error("create thumbnail error", e);
|
||||||
throw new TimiException(TimiCode.ERROR, "TODO delete mongo file error", e);
|
throw new TimiException(TimiCode.ERROR, "exception.attachment.create.error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,8 +390,6 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
// 将临时文件的 GridFS 存储转移至当前永久记录,废弃临时记录
|
// 将临时文件的 GridFS 存储转移至当前永久记录,废弃临时记录
|
||||||
Attachment current = mapper.selectRaw(item.getId());
|
Attachment current = mapper.selectRaw(item.getId());
|
||||||
Attachment tempFile = mapper.selectRaw(tempFileId);
|
Attachment tempFile = mapper.selectRaw(tempFileId);
|
||||||
// 源文件版本已变化,旧缩略图全部失效
|
|
||||||
variantService.deleteBySourceId(current.getId());
|
|
||||||
// 删除旧 GridFS 文件
|
// 删除旧 GridFS 文件
|
||||||
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(current.getMongoId())));
|
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(current.getMongoId())));
|
||||||
// 将存储信息从临时记录迁移到永久记录
|
// 将存储信息从临时记录迁移到永久记录
|
||||||
|
|||||||
-339
@@ -1,339 +0,0 @@
|
|||||||
package com.imyeyu.api.modules.common.service.implement;
|
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
|
||||||
import com.imyeyu.api.modules.common.bean.attachment.MediaAttach;
|
|
||||||
import com.imyeyu.api.modules.common.bean.attachment.ThumbnailFitMode;
|
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
|
||||||
import com.imyeyu.api.modules.common.entity.AttachmentVariant;
|
|
||||||
import com.imyeyu.api.modules.common.mapper.AttachmentVariantMapper;
|
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
|
|
||||||
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.util.Redis;
|
|
||||||
import com.imyeyu.utils.Time;
|
|
||||||
import com.mongodb.client.gridfs.GridFSBucket;
|
|
||||||
import com.mongodb.client.gridfs.model.GridFSFile;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import net.coobird.thumbnailator.Thumbnails;
|
|
||||||
import net.coobird.thumbnailator.geometry.Positions;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
|
||||||
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;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
/// 附件缩略图变体服务实现
|
|
||||||
///
|
|
||||||
/// 负责缩略图生成、变体缓存、并发控制和变体文件清理,源附件生命周期由 `AttachmentService` 负责
|
|
||||||
///
|
|
||||||
/// @author 夜雨
|
|
||||||
/// @since 2026-08-21
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class AttachmentVariantServiceImplement implements AttachmentVariantService {
|
|
||||||
|
|
||||||
private static final int THUMB_MAX_WIDTH = 4096;
|
|
||||||
private static final int THUMB_MAX_HEIGHT = 4096;
|
|
||||||
private static final long THUMB_MAX_PIXELS = 16_000_000L;
|
|
||||||
private static final long THUMB_CACHE_TTL = Time.D * 7;
|
|
||||||
private static final long THUMB_LOCK_TTL = Time.M * 10;
|
|
||||||
private static final double THUMB_TARGET_SECONDS = 2;
|
|
||||||
private static final String THUMB_OUTPUT_FORMAT = "png";
|
|
||||||
private static final String THUMB_ALGORITHM_VERSION = "v3";
|
|
||||||
private static final String THUMB_LOCK_KEY_PREFIX = "ATTACHMENT:THUMB:LOCK:";
|
|
||||||
|
|
||||||
private final AttachmentVariantMapper variantMapper;
|
|
||||||
private final ObjectMapper jackson;
|
|
||||||
private final GridFSBucket gridFSBucket;
|
|
||||||
private final GridFsTemplate gridFsTemplate;
|
|
||||||
|
|
||||||
@Qualifier("redisLocker")
|
|
||||||
private final Redis<String, Integer> redisLocker;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Attachment fetchThumb(Attachment source, Integer requestWidth, Integer requestHeight, ThumbnailFitMode fitMode) {
|
|
||||||
TimiException.required(source, "not found source attachment");
|
|
||||||
TimiException.required(source.getId(), "not found source attachment.id");
|
|
||||||
TimiException.required(source.getMongoId(), "not found source attachment.mongoId");
|
|
||||||
TimiException.requiredTrue(requestWidth != null || requestHeight != null, "required width or height");
|
|
||||||
if (requestWidth != null && !(0 < requestWidth && requestWidth < THUMB_MAX_WIDTH + 1)) {
|
|
||||||
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.width.invalid");
|
|
||||||
}
|
|
||||||
if (requestHeight != null && !(0 < requestHeight && requestHeight < THUMB_MAX_HEIGHT + 1)) {
|
|
||||||
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.height.invalid");
|
|
||||||
}
|
|
||||||
if (requestWidth != null && requestHeight != null) {
|
|
||||||
long pixels = (long) requestWidth * requestHeight;
|
|
||||||
if (THUMB_MAX_PIXELS < pixels) {
|
|
||||||
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.area.invalid");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fitMode = TimiJava.defaultIfNull(fitMode, ThumbnailFitMode.FIT);
|
|
||||||
|
|
||||||
String mimeType = source.getMimeType();
|
|
||||||
boolean isImage = mimeType != null && Set.of("image/png", "image/bmp", "image/jpeg").contains(mimeType);
|
|
||||||
boolean isVideo = mimeType != null && Set.of("video/mp4", "video/quicktime").contains(mimeType);
|
|
||||||
if (!isImage && !isVideo) {
|
|
||||||
throw new TimiException(TimiCode.ARG_BAD, "attachment.thumbnail.not.support");
|
|
||||||
}
|
|
||||||
|
|
||||||
int width = TimiJava.defaultIfNull(requestWidth, 0);
|
|
||||||
int height = TimiJava.defaultIfNull(requestHeight, 0);
|
|
||||||
long now = Time.now();
|
|
||||||
Attachment cached = variantMapper.selectValidAttachment(
|
|
||||||
source.getId(),
|
|
||||||
source.getMongoId(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fitMode.toString(),
|
|
||||||
THUMB_OUTPUT_FORMAT,
|
|
||||||
THUMB_ALGORITHM_VERSION,
|
|
||||||
now
|
|
||||||
);
|
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
|
|
||||||
String lockKey = buildThumbLockKey(source, width, height, fitMode);
|
|
||||||
if (!redisLocker.lock(lockKey, 1, THUMB_LOCK_TTL)) {
|
|
||||||
throw new TimiException(TimiCode.ERROR_SERVICE_BUSY, "attachment.thumbnail.generating");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
cached = variantMapper.selectValidAttachment(
|
|
||||||
source.getId(),
|
|
||||||
source.getMongoId(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fitMode.toString(),
|
|
||||||
THUMB_OUTPUT_FORMAT,
|
|
||||||
THUMB_ALGORITHM_VERSION,
|
|
||||||
Time.now()
|
|
||||||
);
|
|
||||||
if (cached != null) {
|
|
||||||
return cached;
|
|
||||||
}
|
|
||||||
AttachmentVariant oldVariant = variantMapper.selectByKey(
|
|
||||||
source.getId(),
|
|
||||||
source.getMongoId(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fitMode.toString(),
|
|
||||||
THUMB_OUTPUT_FORMAT,
|
|
||||||
THUMB_ALGORITHM_VERSION
|
|
||||||
);
|
|
||||||
if (oldVariant != null) {
|
|
||||||
deleteVariant(oldVariant);
|
|
||||||
}
|
|
||||||
File thumbFile = null;
|
|
||||||
AttachmentVariant variant = null;
|
|
||||||
boolean variantInserted = false;
|
|
||||||
try {
|
|
||||||
thumbFile = createThumbFile(source, requestWidth, requestHeight, fitMode, isImage);
|
|
||||||
variant = new AttachmentVariant();
|
|
||||||
variant.setId(UUID.randomUUID().toString());
|
|
||||||
variant.setName(Network.simpleURIFileName(source.getName()) + "." + THUMB_OUTPUT_FORMAT);
|
|
||||||
variant.setMimeType("image/" + THUMB_OUTPUT_FORMAT);
|
|
||||||
long expireAt = Time.now() + THUMB_CACHE_TTL;
|
|
||||||
variant.setSourceAttachmentId(source.getId());
|
|
||||||
variant.setSourceMongoId(source.getMongoId());
|
|
||||||
variant.setSourceMd5(source.getMd5());
|
|
||||||
variant.setRequestWidth(width);
|
|
||||||
variant.setRequestHeight(height);
|
|
||||||
variant.setFitMode(fitMode);
|
|
||||||
variant.setOutputFormat(THUMB_OUTPUT_FORMAT);
|
|
||||||
variant.setAlgorithmVersion(THUMB_ALGORITHM_VERSION);
|
|
||||||
variant.setCreatedAt(Time.now());
|
|
||||||
variant.setExpireAt(expireAt);
|
|
||||||
BufferedImage image = ImageIO.read(thumbFile);
|
|
||||||
if (image != null) {
|
|
||||||
ObjectNode metadata = jackson.createObjectNode();
|
|
||||||
metadata.put("width", image.getWidth());
|
|
||||||
metadata.put("height", image.getHeight());
|
|
||||||
variant.setMetadata(metadata);
|
|
||||||
}
|
|
||||||
try (InputStream inputStream = Files.newInputStream(thumbFile.toPath())) {
|
|
||||||
String mongoId = gridFsTemplate.store(inputStream, variant.getName()).toString();
|
|
||||||
GridFSFile gridFSFile = gridFsTemplate.findOne(new Query(Criteria.where("_id").is(mongoId)));
|
|
||||||
variant.setMongoId(mongoId);
|
|
||||||
variant.setSize(gridFSFile.getLength());
|
|
||||||
try (InputStream md5InputStream = gridFSBucket.openDownloadStream(gridFSFile.getObjectId())) {
|
|
||||||
variant.setMd5(IO.md5(md5InputStream));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
variantMapper.insert(variant);
|
|
||||||
variantInserted = true;
|
|
||||||
} catch (DuplicateKeyException e) {
|
|
||||||
destroyVariantFile(variant);
|
|
||||||
Attachment concurrent = variantMapper.selectValidAttachment(
|
|
||||||
source.getId(),
|
|
||||||
source.getMongoId(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fitMode.toString(),
|
|
||||||
THUMB_OUTPUT_FORMAT,
|
|
||||||
THUMB_ALGORITHM_VERSION,
|
|
||||||
Time.now()
|
|
||||||
);
|
|
||||||
if (concurrent != null) {
|
|
||||||
return concurrent;
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
return toThumbAttachment(source, variant);
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (variant != null) {
|
|
||||||
destroyVariantFile(variant);
|
|
||||||
if (variantInserted) {
|
|
||||||
variantMapper.delete(variant.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
IO.destroy(thumbFile);
|
|
||||||
}
|
|
||||||
} catch (TimiException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("create thumbnail error", e);
|
|
||||||
throw new TimiException(TimiCode.ERROR, "exception.attachment.create.error", e);
|
|
||||||
} finally {
|
|
||||||
redisLocker.releaseLock(lockKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildThumbLockKey(Attachment source, int width, int height, ThumbnailFitMode fitMode) {
|
|
||||||
return "%s%s:%s:%s:%s:%s".formatted(
|
|
||||||
THUMB_LOCK_KEY_PREFIX,
|
|
||||||
source.getId(),
|
|
||||||
source.getMongoId(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
fitMode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private File createThumbFile(
|
|
||||||
Attachment source,
|
|
||||||
Integer requestWidth,
|
|
||||||
Integer requestHeight,
|
|
||||||
ThumbnailFitMode fitMode,
|
|
||||||
boolean isImage
|
|
||||||
) throws Exception {
|
|
||||||
File thumbFile = Files.createTempFile("timi-thumb-", ".png").toFile();
|
|
||||||
try {
|
|
||||||
if (isImage) {
|
|
||||||
try (InputStream inputStream = source.openInputStream()) {
|
|
||||||
resize(inputStream, thumbFile, requestWidth, requestHeight, fitMode);
|
|
||||||
}
|
|
||||||
return thumbFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
try (InputStream inputStream = source.openInputStream()) {
|
|
||||||
ByteArrayOutputStream frame = JavaCV.captureThumbnail(inputStream, THUMB_TARGET_SECONDS);
|
|
||||||
if (!(0 < frame.size())) {
|
|
||||||
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT, "attachment.thumbnail.video.frame.empty");
|
|
||||||
}
|
|
||||||
try (InputStream frameInputStream = new ByteArrayInputStream(frame.toByteArray())) {
|
|
||||||
resize(frameInputStream, thumbFile, requestWidth, requestHeight, fitMode);
|
|
||||||
}
|
|
||||||
return thumbFile;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
IO.destroy(thumbFile);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void resize(
|
|
||||||
InputStream inputStream,
|
|
||||||
File outputFile,
|
|
||||||
Integer requestWidth,
|
|
||||||
Integer requestHeight,
|
|
||||||
ThumbnailFitMode fitMode
|
|
||||||
) throws IOException {
|
|
||||||
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(inputStream)
|
|
||||||
.useExifOrientation(true)
|
|
||||||
.outputFormat(THUMB_OUTPUT_FORMAT);
|
|
||||||
if (requestWidth != null && requestHeight != null) {
|
|
||||||
switch (fitMode) {
|
|
||||||
case FIT -> builder.size(requestWidth, requestHeight).keepAspectRatio(true);
|
|
||||||
case COVER -> builder.size(requestWidth, requestHeight).keepAspectRatio(true).crop(Positions.CENTER);
|
|
||||||
case STRETCH -> builder.size(requestWidth, requestHeight).keepAspectRatio(false);
|
|
||||||
}
|
|
||||||
} else if (requestWidth != null) {
|
|
||||||
builder.width(requestWidth).keepAspectRatio(true);
|
|
||||||
} else {
|
|
||||||
builder.height(requestHeight).keepAspectRatio(true);
|
|
||||||
}
|
|
||||||
builder.toFile(outputFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
|
||||||
@Override
|
|
||||||
public void deleteBySourceId(String sourceAttachmentId) {
|
|
||||||
for (AttachmentVariant variant : variantMapper.selectBySourceAttachmentId(sourceAttachmentId)) {
|
|
||||||
deleteVariant(variant);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteVariant(AttachmentVariant variant) {
|
|
||||||
destroyVariantFile(variant);
|
|
||||||
variantMapper.delete(variant.getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void destroyVariantFile(AttachmentVariant variant) {
|
|
||||||
if (variant.getMongoId() != null) {
|
|
||||||
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(variant.getMongoId())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Attachment toThumbAttachment(Attachment source, AttachmentVariant variant) {
|
|
||||||
Attachment thumb = new Attachment();
|
|
||||||
thumb.setId(variant.getId());
|
|
||||||
thumb.setBizType(source.getBizType());
|
|
||||||
thumb.setBizId(source.getId());
|
|
||||||
thumb.setAttachType(MediaAttach.Type.THUMB.toString());
|
|
||||||
thumb.setMongoId(variant.getMongoId());
|
|
||||||
thumb.setName(variant.getName());
|
|
||||||
thumb.setMimeType(variant.getMimeType());
|
|
||||||
thumb.setMetadata(variant.getMetadata());
|
|
||||||
thumb.setSize(variant.getSize());
|
|
||||||
thumb.setMd5(variant.getMd5());
|
|
||||||
thumb.setIsDestroyed(false);
|
|
||||||
thumb.setDestroyAt(variant.getExpireAt());
|
|
||||||
return thumb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void clearExpired() {
|
|
||||||
for (AttachmentVariant variant : variantMapper.selectExpired(Time.now())) {
|
|
||||||
try {
|
|
||||||
deleteVariant(variant);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("clear expired thumbnail variant error, id={}", variant.getId(), e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-2
@@ -1,5 +1,6 @@
|
|||||||
package com.imyeyu.api.modules.common.service.implement;
|
package com.imyeyu.api.modules.common.service.implement;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imyeyu.api.modules.common.config.NotifyQueueConfig;
|
import com.imyeyu.api.modules.common.config.NotifyQueueConfig;
|
||||||
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
||||||
@@ -10,9 +11,9 @@ import com.imyeyu.api.modules.common.service.SmsQueueService;
|
|||||||
import com.imyeyu.api.modules.common.util.QCloudSmsProxy;
|
import com.imyeyu.api.modules.common.util.QCloudSmsProxy;
|
||||||
import com.imyeyu.java.bean.timi.TimiCode;
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
import com.imyeyu.java.bean.timi.TimiException;
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
import com.imyeyu.utils.Regex;
|
|
||||||
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse;
|
||||||
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
|
import com.tencentcloudapi.sms.v20210111.models.SendStatus;
|
||||||
|
import com.imyeyu.utils.Regex;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
@@ -55,7 +56,12 @@ public class SmsQueueServiceImplement extends AbstractQueueService<SmsQueue, Not
|
|||||||
@Override
|
@Override
|
||||||
protected void doSend(SmsQueue queue) {
|
protected void doSend(SmsQueue queue) {
|
||||||
try {
|
try {
|
||||||
String[] args = jackson.treeToValue(queue.getArgs(), String[].class);
|
String[] args = new String[0];
|
||||||
|
if (queue.getArgs() != null && !queue.getArgs().isBlank()) {
|
||||||
|
JsonNode argsNode = jackson.readTree(queue.getArgs());
|
||||||
|
TimiException.requiredTrue(argsNode != null && argsNode.isArray(), "短信参数必须是数组");
|
||||||
|
args = jackson.treeToValue(argsNode, String[].class);
|
||||||
|
}
|
||||||
SendSmsResponse response = smsProxy.send(queue.getSendTo(), queue.getTemplateId(), args);
|
SendSmsResponse response = smsProxy.send(queue.getSendTo(), queue.getTemplateId(), args);
|
||||||
TimiException.required(response, "未找到短信响应");
|
TimiException.required(response, "未找到短信响应");
|
||||||
SendStatus[] statusSet = response.getSendStatusSet();
|
SendStatus[] statusSet = response.getSendStatusSet();
|
||||||
|
|||||||
+82
-85
@@ -1,85 +1,82 @@
|
|||||||
package com.imyeyu.api.modules.common.service.implement;
|
//package com.imyeyu.api.modules.common.service.implement;
|
||||||
|
//
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
//import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
//import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
import com.imyeyu.api.modules.common.config.NotifyQueueConfig;
|
//import com.imyeyu.api.modules.common.config.NotifyQueueConfig;
|
||||||
import com.imyeyu.api.modules.common.entity.Notify;
|
//import com.imyeyu.api.modules.common.entity.Notify;
|
||||||
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
//import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
||||||
import com.imyeyu.api.modules.common.service.NotifyDetailService;
|
//import com.imyeyu.api.modules.common.service.NotifyDetailService;
|
||||||
import com.imyeyu.api.modules.common.service.NotifyService;
|
//import com.imyeyu.api.modules.common.service.NotifyService;
|
||||||
import com.imyeyu.api.modules.common.service.SmsService;
|
//import com.imyeyu.api.modules.common.service.SmsService;
|
||||||
import com.imyeyu.java.bean.timi.TimiCode;
|
//import com.imyeyu.java.bean.timi.TimiException;
|
||||||
import com.imyeyu.java.bean.timi.TimiException;
|
//import com.imyeyu.utils.Regex;
|
||||||
import com.imyeyu.utils.Regex;
|
//import com.imyeyu.utils.Text;
|
||||||
import com.imyeyu.utils.Text;
|
//import com.imyeyu.utils.Time;
|
||||||
import com.imyeyu.utils.Time;
|
//import lombok.RequiredArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
//import lombok.extern.slf4j.Slf4j;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
//import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
//import org.springframework.stereotype.Service;
|
||||||
import org.springframework.stereotype.Service;
|
//
|
||||||
|
//import java.util.List;
|
||||||
import java.util.List;
|
//
|
||||||
|
///**
|
||||||
/**
|
// *
|
||||||
*
|
// *
|
||||||
*
|
// * @author 夜雨
|
||||||
* @author 夜雨
|
// * @since 2026-05-14 12:12
|
||||||
* @since 2026-05-14 12:12
|
// */
|
||||||
*/
|
//@Slf4j
|
||||||
@Slf4j
|
//@Service
|
||||||
@Service
|
//@RequiredArgsConstructor
|
||||||
@RequiredArgsConstructor
|
//public class SmsServiceImplement implements SmsService {
|
||||||
public class SmsServiceImplement implements SmsService {
|
//
|
||||||
|
// @Value("${spring.profiles.active}")
|
||||||
@Value("${spring.profiles.active}")
|
// private String env;
|
||||||
private String env;
|
//
|
||||||
|
// private final NotifyQueueConfig config;
|
||||||
private final NotifyQueueConfig config;
|
// private final ObjectMapper jackson;
|
||||||
private final ObjectMapper jackson;
|
//
|
||||||
|
// private final NotifyService notifyService;
|
||||||
private final NotifyService notifyService;
|
// private final NotifyDetailService notifyDetailService;
|
||||||
private final NotifyDetailService notifyDetailService;
|
//
|
||||||
|
// @Override
|
||||||
@Override
|
// public String captchaCreate(Notify notify) {
|
||||||
public String captchaCreate(Notify notify) {
|
// NotifyQueueConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
|
||||||
NotifyQueueConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
|
// TimiException.required(notify.getAppId(), "not found appId");
|
||||||
|
//
|
||||||
NotifyDetail detail = notify.getDetailList().getFirst();
|
// NotifyDetail detail = notify.getDetailList().getFirst();
|
||||||
|
//
|
||||||
TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, detail.getSendTo()), "invalid mobilePhone");
|
// TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, detail.getSendTo()), "invalid mobilePhone", "captcha.sms.invalid_mobile_phone");
|
||||||
|
//
|
||||||
String code = Text.randomString("0123456789", 6);
|
// String code = Text.randomString("0123456789", 6);
|
||||||
if (!env.equalsIgnoreCase("prod")) {
|
// if (!env.equalsIgnoreCase("prod")) {
|
||||||
code = "123456";
|
// code = "123456";
|
||||||
}
|
// }
|
||||||
detail.setMsgType(NotifyDetail.MsgType.SMS);
|
// detail.setMsgType(NotifyDetail.MsgType.SMS);
|
||||||
detail.setTemplateId(captchaConf.getTemplateId());
|
// detail.setTemplateId(captchaConf.getTemplateId());
|
||||||
detail.setArgs(jackson.valueToTree(List.of(code, captchaConf.getTtlMinute())));
|
// detail.setArgs(jackson.valueToTree(List.of(code, captchaConf.getTtlMinute())));
|
||||||
|
//
|
||||||
notifyService.create(notify);
|
// notifyService.create(notify);
|
||||||
return notifyDetailService.listByNotifyId(notify.getId()).getFirst().getId();
|
// return notifyDetailService.listByNotifyId(notify.getId()).getFirst().getId();
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public NotifyDetail captchaVerify(String detailId, String captcha) {
|
// public NotifyDetail captchaVerify(String detailId, String captcha) {
|
||||||
NotifyQueueConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
|
// AppConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
|
||||||
|
//
|
||||||
NotifyDetail detail = notifyDetailService.get(detailId);
|
// NotifyDetail detail = notifyDetailService.get(detailId);
|
||||||
TimiException.required(detail, "短信验证码无效");
|
// if (Time.M * captchaConf.getTtlMinute() < Time.now() - detail.getSendAt()) {
|
||||||
TimiException.required(detail.getSendAt(), "短信验证码无效");
|
// throw new AppException(ResultCode.ARG_EXPIRED).msgKey("captcha.expired");
|
||||||
TimiException.requiredTrue(NotifyDetail.MsgType.SMS == detail.getMsgType(), "短信验证码无效");
|
// }
|
||||||
if (Time.M * captchaConf.getTtlMinute() < Time.now() - detail.getSendAt()) {
|
// if (detail.getArgs() instanceof ArrayNode array) {
|
||||||
throw new TimiException(TimiCode.ARG_EXPIRED, "验证码过期");
|
// String dbCaptcha = array.get(captchaConf.getArgIndex()).asText();
|
||||||
}
|
// if (!dbCaptcha.equalsIgnoreCase(captcha)) {
|
||||||
if (detail.getArgs() instanceof ArrayNode array) {
|
// throw new AppException(ResultCode.RESULT_BAD).msgKey("captcha.invalid");
|
||||||
String dbCaptcha = array.get(captchaConf.getArgIndex()).asText();
|
// }
|
||||||
if (!dbCaptcha.equalsIgnoreCase(captcha)) {
|
// } else {
|
||||||
throw new TimiException(TimiCode.RESULT_BAD, "验证码错误");
|
// throw new AppException(ResultCode.ERROR, "not array args");
|
||||||
}
|
// }
|
||||||
} else {
|
// detail.setNotify(notifyService.get(detail.getNotifyId()));
|
||||||
throw new TimiException(TimiCode.ERROR, "not array args");
|
// return detail;
|
||||||
}
|
// }
|
||||||
detail.setNotify(notifyService.get(detail.getNotifyId()));
|
//}
|
||||||
return detail;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.imyeyu.api.modules.common.task;
|
|||||||
|
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -24,7 +23,6 @@ import java.util.List;
|
|||||||
public class AttachmentClearTask {
|
public class AttachmentClearTask {
|
||||||
|
|
||||||
private final AttachmentService attachmentService;
|
private final AttachmentService attachmentService;
|
||||||
private final AttachmentVariantService attachmentVariantService;
|
|
||||||
|
|
||||||
@Scheduled(cron = "0 0 1 * * ?")
|
@Scheduled(cron = "0 0 1 * * ?")
|
||||||
public void run() {
|
public void run() {
|
||||||
@@ -33,20 +31,19 @@ public class AttachmentClearTask {
|
|||||||
List<Attachment> needDestroyList = attachmentService.listNeedDestroy();
|
List<Attachment> needDestroyList = attachmentService.listNeedDestroy();
|
||||||
if (needDestroyList.isEmpty()) {
|
if (needDestroyList.isEmpty()) {
|
||||||
log.info("nothing attachment need clear");
|
log.info("nothing attachment need clear");
|
||||||
} else {
|
return;
|
||||||
for (Attachment attach : needDestroyList) {
|
}
|
||||||
try {
|
for (Attachment attach : needDestroyList) {
|
||||||
attachmentService.destroy(attach.getId());
|
try {
|
||||||
log.info("clear attachment success: id[{}]", attach.getId());
|
attachmentService.destroy(attach.getId());
|
||||||
} catch (Exception e) {
|
log.info("clear attachment success: id[{}]", attach.getId());
|
||||||
log.error("clear attachment error: id[{}]", attach.getId(), e);
|
} catch (Exception e) {
|
||||||
}
|
log.error("clear attachment error: id[{}]", attach.getId(), e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
attachmentVariantService.clearExpired();
|
|
||||||
log.info("end clear expired attachments");
|
log.info("end clear expired attachments");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("attachment clear task error", e);
|
log.error("attachment clear task error", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
package com.imyeyu.api.modules.common.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// @author 夜雨
|
|
||||||
/// @since 2026-08-21 17:59
|
|
||||||
@Data
|
|
||||||
public class SmsCaptchaVerifyRequest {
|
|
||||||
|
|
||||||
private String detailId;
|
|
||||||
|
|
||||||
private String captcha;
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,7 @@ import com.imyeyu.api.modules.user.service.UserService;
|
|||||||
import com.imyeyu.api.modules.user.vo.LoginResponse;
|
import com.imyeyu.api.modules.user.vo.LoginResponse;
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
import com.imyeyu.spring.annotation.AOPLog;
|
import com.imyeyu.spring.annotation.AOPLog;
|
||||||
|
import com.imyeyu.spring.annotation.CaptchaValid;
|
||||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
import com.imyeyu.spring.annotation.RequestRateLimit;
|
||||||
import com.imyeyu.spring.bean.Page;
|
import com.imyeyu.spring.bean.Page;
|
||||||
import com.imyeyu.spring.bean.PageResult;
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
@@ -79,11 +80,12 @@ public class GaoUserController {
|
|||||||
|
|
||||||
@AOPLog
|
@AOPLog
|
||||||
@JsonView(ResponseView.Public.class)
|
@JsonView(ResponseView.Public.class)
|
||||||
|
@CaptchaValid
|
||||||
@RequestRateLimit(value = 5, inSeconds = 60)
|
@RequestRateLimit(value = 5, inSeconds = 60)
|
||||||
@PostMapping("/invite/register")
|
@PostMapping("/invite/register")
|
||||||
public LoginResponse registerByInvite(@RequestBody @Valid GaoUserInviteRegisterRequest req) {
|
public LoginResponse registerByInvite(@RequestBody @Valid GaoUserInviteRegisterRequest req) {
|
||||||
GaoUserInviteRegisterData data = req.getData();
|
GaoUserInviteRegisterData data = req.getData();
|
||||||
String userId = inviteService.register(data, req.getDetailId(), req.getCaptcha());
|
String userId = inviteService.register(data);
|
||||||
return userLoginService.login(UUID.randomUUID().toString(), userId);
|
return userLoginService.login(UUID.randomUUID().toString(), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,8 +26,6 @@ public interface GaoUserInviteService {
|
|||||||
/// 使用邀请注册 GAO 用户
|
/// 使用邀请注册 GAO 用户
|
||||||
///
|
///
|
||||||
/// @param data 注册数据
|
/// @param data 注册数据
|
||||||
/// @param smsDetailId 短信通知详情 ID
|
|
||||||
/// @param smsCaptcha 短信验证码
|
|
||||||
/// @return 注册成功的用户 ID
|
/// @return 注册成功的用户 ID
|
||||||
String register(GaoUserInviteRegisterData data, String smsDetailId, String smsCaptcha);
|
String register(GaoUserInviteRegisterData data);
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-31
@@ -3,8 +3,6 @@ package com.imyeyu.api.modules.gao.service.implement;
|
|||||||
import com.imyeyu.api.bean.CoreRoleCode;
|
import com.imyeyu.api.bean.CoreRoleCode;
|
||||||
import com.imyeyu.api.bean.ModuleCode;
|
import com.imyeyu.api.bean.ModuleCode;
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
|
||||||
import com.imyeyu.api.modules.common.service.SmsService;
|
|
||||||
import com.imyeyu.api.modules.gao.bean.GaoUserInviteCache;
|
import com.imyeyu.api.modules.gao.bean.GaoUserInviteCache;
|
||||||
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
import com.imyeyu.api.modules.gao.bean.GaoPermissionCode;
|
||||||
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
||||||
@@ -66,13 +64,11 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
private static final long REGISTER_RATE_LIMIT_TTL = Time.M * 10;
|
private static final long REGISTER_RATE_LIMIT_TTL = Time.M * 10;
|
||||||
private static final int CODE_RATE_LIMIT = 30;
|
private static final int CODE_RATE_LIMIT = 30;
|
||||||
private static final int REGISTER_VALUE_RATE_LIMIT = 10;
|
private static final int REGISTER_VALUE_RATE_LIMIT = 10;
|
||||||
private static final String INVALID_INVITE_MESSAGE = "邀请码无效或已过期";
|
|
||||||
|
|
||||||
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
|
||||||
|
|
||||||
private final GaoStoreService gaoStoreService;
|
private final GaoStoreService gaoStoreService;
|
||||||
private final GaoUserService gaoUserService;
|
private final GaoUserService gaoUserService;
|
||||||
private final SmsService smsService;
|
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
private final UserRoleService userRoleService;
|
private final UserRoleService userRoleService;
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
@@ -102,11 +98,9 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
logger.setUserId(inviterUserId);
|
logger.setUserId(inviterUserId);
|
||||||
permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.USER_CREATE.getValue());
|
permissionChecker.checkAny(ModuleCode.GAO, GaoPermissionCode.USER_CREATE.getValue());
|
||||||
Role requestedRole = roleService.get(req.getRoleId());
|
Role requestedRole = roleService.get(req.getRoleId());
|
||||||
boolean coreAdmin = isCoreAdmin();
|
boolean adminInviteGlobalManager = roleChecker.hasAny(ModuleCode.CORE, CoreRoleCode.ADMIN.name())
|
||||||
boolean adminInviteGlobalManager = coreAdmin
|
|
||||||
&& isGlobalManagerRole(requestedRole);
|
&& isGlobalManagerRole(requestedRole);
|
||||||
boolean globalManager = authorizationScopeService.isSystemAdmin(inviterUserId)
|
boolean globalManager = authorizationScopeService.isSystemAdmin(inviterUserId)
|
||||||
|| coreAdmin
|
|
||||||
|| roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name());
|
|| roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name());
|
||||||
boolean storeManager = !globalManager;
|
boolean storeManager = !globalManager;
|
||||||
GaoStore loginStore = storeManager ? gaoStoreService.getByUserId(inviterUserId) : null;
|
GaoStore loginStore = storeManager ? gaoStoreService.getByUserId(inviterUserId) : null;
|
||||||
@@ -153,21 +147,14 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
public GaoUserInviteValidateResponse validate(String code) {
|
public GaoUserInviteValidateResponse validate(String code) {
|
||||||
TimiException.required(code, "not found code");
|
TimiException.required(code, "not found code");
|
||||||
checkCodeRateLimit(code);
|
checkCodeRateLimit(code);
|
||||||
try {
|
GaoUserInviteCache cache = requireValidInvite(code);
|
||||||
GaoUserInviteCache cache = requireValidInvite(code);
|
validateInviteTarget(cache);
|
||||||
validateInviteTarget(cache);
|
return new GaoUserInviteValidateResponse(true, cache.getExpireAt());
|
||||||
return new GaoUserInviteValidateResponse(true, cache.getExpireAt());
|
|
||||||
} catch (TimiException e) {
|
|
||||||
if (TimiCode.ARG_BAD == e.getCode() && INVALID_INVITE_MESSAGE.equals(e.getMessage())) {
|
|
||||||
return new GaoUserInviteValidateResponse(false, -1L);
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public String register(GaoUserInviteRegisterData data, String smsDetailId, String smsCaptcha) {
|
public String register(GaoUserInviteRegisterData data) {
|
||||||
TimiException.required(data, "not found register data");
|
TimiException.required(data, "not found register data");
|
||||||
String code = data.getCode();
|
String code = data.getCode();
|
||||||
TimiException.required(code, "not found code");
|
TimiException.required(code, "not found code");
|
||||||
@@ -183,9 +170,6 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
cache = requireValidInvite(code);
|
cache = requireValidInvite(code);
|
||||||
logger.setContent(buildLogContent(cache));
|
logger.setContent(buildLogContent(cache));
|
||||||
validateInviteTarget(cache);
|
validateInviteTarget(cache);
|
||||||
NotifyDetail smsDetail = smsService.captchaVerify(smsDetailId, smsCaptcha);
|
|
||||||
TimiException.requiredTrue(NotifyDetail.MsgType.SMS == smsDetail.getMsgType()
|
|
||||||
&& data.getPhoneNo().equals(smsDetail.getSendTo()), "短信验证码与手机号不匹配");
|
|
||||||
TimiException.requiredNull(userService.getByName(data.getName()), "existed user name");
|
TimiException.requiredNull(userService.getByName(data.getName()), "existed user name");
|
||||||
TimiException.requiredNull(userService.getByPhoneNo(data.getPhoneNo()), "existed phone number");
|
TimiException.requiredNull(userService.getByPhoneNo(data.getPhoneNo()), "existed phone number");
|
||||||
if (!redisInvite.destroy(getInviteKey(code))) {
|
if (!redisInvite.destroy(getInviteKey(code))) {
|
||||||
@@ -280,11 +264,10 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
.stream()
|
.stream()
|
||||||
.map(Role::getId)
|
.map(Role::getId)
|
||||||
.toList();
|
.toList();
|
||||||
boolean adminInviteGlobalManager = isCoreAdmin() && isGlobalManagerRole(role);
|
boolean descendant = authorizationScopeService.listManageableRole(inviterUserId, ModuleCode.GAO)
|
||||||
boolean descendant = adminInviteGlobalManager || (authorizationScopeService.listManageableRole(inviterUserId, ModuleCode.GAO)
|
|
||||||
.stream()
|
.stream()
|
||||||
.anyMatch(item -> role.getId().equals(item.getId()))
|
.anyMatch(item -> role.getId().equals(item.getId()))
|
||||||
&& !directRoleIdList.contains(role.getId()));
|
&& !directRoleIdList.contains(role.getId());
|
||||||
if (!descendant) {
|
if (!descendant) {
|
||||||
throw new TimiException(TimiCode.PERMISSION_ERROR, "角色不在可邀请范围内");
|
throw new TimiException(TimiCode.PERMISSION_ERROR, "角色不在可邀请范围内");
|
||||||
}
|
}
|
||||||
@@ -319,8 +302,7 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkStoreScope(String userId, String storeId) {
|
private void checkStoreScope(String userId, String storeId) {
|
||||||
if (authorizationScopeService.isSystemAdmin(userId) || isCoreAdmin()
|
if (authorizationScopeService.isSystemAdmin(userId) || roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
||||||
|| roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GaoStore userStore = gaoStoreService.getByUserId(userId);
|
GaoStore userStore = gaoStoreService.getByUserId(userId);
|
||||||
@@ -362,10 +344,6 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private TimiException invalidInvite() {
|
private TimiException invalidInvite() {
|
||||||
return new TimiException(TimiCode.ARG_BAD, INVALID_INVITE_MESSAGE);
|
return new TimiException(TimiCode.ARG_BAD, "邀请码无效或已过期");
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCoreAdmin() {
|
|
||||||
return roleChecker.hasAny(ModuleCode.CORE, CoreRoleCode.ADMIN.name());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,20 @@
|
|||||||
package com.imyeyu.api.modules.gao.vo;
|
package com.imyeyu.api.modules.gao.vo;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.bean.CaptchaData;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import jakarta.validation.constraints.NotBlank;
|
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.NotNull;
|
||||||
import jakarta.validation.constraints.Pattern;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/// GAO 用户邀请注册请求
|
/// GAO 用户邀请注册请求
|
||||||
///
|
///
|
||||||
/// @author Codex
|
/// @author Codex
|
||||||
/// @since 2026-08-13
|
/// @since 2026-08-13
|
||||||
@Data
|
public class GaoUserInviteRegisterRequest extends CaptchaData<GaoUserInviteRegisterData> {
|
||||||
public class GaoUserInviteRegisterRequest {
|
|
||||||
|
|
||||||
/// 短信通知详情 ID
|
|
||||||
@NotBlank
|
|
||||||
private String detailId;
|
|
||||||
|
|
||||||
/// 短信验证码
|
|
||||||
@NotBlank
|
|
||||||
@Pattern(regexp = "\\d{6}")
|
|
||||||
private String captcha;
|
|
||||||
|
|
||||||
private GaoUserInviteRegisterData data;
|
|
||||||
|
|
||||||
/// 级联校验注册数据
|
/// 级联校验注册数据
|
||||||
|
@Override
|
||||||
@NotNull
|
@NotNull
|
||||||
@Valid
|
@Valid
|
||||||
public GaoUserInviteRegisterData getData() {
|
public GaoUserInviteRegisterData getData() {
|
||||||
return data;
|
return super.getData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ public class GaoUserInviteValidateResponse {
|
|||||||
/// true 为邀请码可用
|
/// true 为邀请码可用
|
||||||
private boolean valid;
|
private boolean valid;
|
||||||
|
|
||||||
/// 邀请过期时间,毫秒时间戳;valid 为 false 时为 0
|
/// 邀请过期时间,毫秒时间戳
|
||||||
private Long expireAt;
|
private Long expireAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import com.imyeyu.api.annotation.RequireCorePermission;
|
|||||||
import com.imyeyu.api.bean.CorePermissionCode;
|
import com.imyeyu.api.bean.CorePermissionCode;
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.common.service.SmsService;
|
|
||||||
import com.imyeyu.api.modules.common.vo.SmsCaptchaVerifyRequest;
|
|
||||||
import com.imyeyu.api.modules.user.entity.User;
|
import com.imyeyu.api.modules.user.entity.User;
|
||||||
import com.imyeyu.api.modules.user.service.UserLoginService;
|
import com.imyeyu.api.modules.user.service.UserLoginService;
|
||||||
import com.imyeyu.api.modules.user.service.UserService;
|
import com.imyeyu.api.modules.user.service.UserService;
|
||||||
@@ -52,7 +50,6 @@ import java.util.List;
|
|||||||
@RequestMapping("/user")
|
@RequestMapping("/user")
|
||||||
public class UserController {
|
public class UserController {
|
||||||
|
|
||||||
private final SmsService smsService;
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
private final AttachmentService attachmentService;
|
private final AttachmentService attachmentService;
|
||||||
@@ -136,15 +133,6 @@ public class UserController {
|
|||||||
return userLoginService.login(TimiSpring.getToken(), userLoginService.getRequireLoginUserId());
|
return userLoginService.login(TimiSpring.getToken(), userLoginService.getRequireLoginUserId());
|
||||||
}
|
}
|
||||||
|
|
||||||
@AOPLog
|
|
||||||
@JsonView(ResponseView.Public.class)
|
|
||||||
@RequestRateLimit
|
|
||||||
@CaptchaValid
|
|
||||||
@PostMapping("/login/sms")
|
|
||||||
public LoginResponse loginBySms(@RequestBody SmsCaptchaVerifyRequest request) {
|
|
||||||
return userLoginService.loginBySms(request);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新当前用户资料
|
* 更新当前用户资料
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.imyeyu.api.modules.user.service;
|
package com.imyeyu.api.modules.user.service;
|
||||||
|
|
||||||
import com.imyeyu.api.modules.common.vo.SmsCaptchaVerifyRequest;
|
import com.e2ins.ms.notify.vo.sms.CaptchaVerifyReq;
|
||||||
import com.imyeyu.api.modules.user.entity.User;
|
import com.imyeyu.api.modules.user.entity.User;
|
||||||
import com.imyeyu.api.modules.user.vo.LoginRequest;
|
import com.imyeyu.api.modules.user.vo.LoginRequest;
|
||||||
import com.imyeyu.api.modules.user.vo.LoginResponse;
|
import com.imyeyu.api.modules.user.vo.LoginResponse;
|
||||||
@@ -24,7 +24,7 @@ public interface UserLoginService {
|
|||||||
|
|
||||||
LoginResponse login(String token, String userId);
|
LoginResponse login(String token, String userId);
|
||||||
|
|
||||||
LoginResponse loginBySms(SmsCaptchaVerifyRequest request);
|
LoginResponse loginBySms(CaptchaVerifyReq req);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据令牌获取用户
|
* 根据令牌获取用户
|
||||||
|
|||||||
+19
-12
@@ -1,13 +1,13 @@
|
|||||||
package com.imyeyu.api.modules.user.service.implement;
|
package com.imyeyu.api.modules.user.service.implement;
|
||||||
|
|
||||||
|
import com.e2ins.ms.notify.SmsNotifyService;
|
||||||
|
import com.e2ins.ms.notify.bean.NotifyDetail;
|
||||||
|
import com.e2ins.ms.notify.vo.sms.CaptchaVerifyReq;
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
import com.imyeyu.api.modules.common.entity.Attachment;
|
import com.imyeyu.api.modules.common.entity.Attachment;
|
||||||
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
|
||||||
import com.imyeyu.api.modules.common.entity.Setting;
|
import com.imyeyu.api.modules.common.entity.Setting;
|
||||||
import com.imyeyu.api.modules.common.service.AttachmentService;
|
import com.imyeyu.api.modules.common.service.AttachmentService;
|
||||||
import com.imyeyu.api.modules.common.service.SettingService;
|
import com.imyeyu.api.modules.common.service.SettingService;
|
||||||
import com.imyeyu.api.modules.common.service.SmsService;
|
|
||||||
import com.imyeyu.api.modules.common.vo.SmsCaptchaVerifyRequest;
|
|
||||||
import com.imyeyu.api.modules.user.bean.BootstrapData;
|
import com.imyeyu.api.modules.user.bean.BootstrapData;
|
||||||
import com.imyeyu.api.modules.user.entity.User;
|
import com.imyeyu.api.modules.user.entity.User;
|
||||||
import com.imyeyu.api.modules.user.service.UserLoginService;
|
import com.imyeyu.api.modules.user.service.UserLoginService;
|
||||||
@@ -16,6 +16,7 @@ import com.imyeyu.api.modules.user.service.UserService;
|
|||||||
import com.imyeyu.api.modules.user.vo.LoginRequest;
|
import com.imyeyu.api.modules.user.vo.LoginRequest;
|
||||||
import com.imyeyu.api.modules.user.vo.LoginResponse;
|
import com.imyeyu.api.modules.user.vo.LoginResponse;
|
||||||
import com.imyeyu.java.TimiJava;
|
import com.imyeyu.java.TimiJava;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiCode;
|
||||||
import com.imyeyu.java.bean.timi.TimiException;
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
import com.imyeyu.spring.TimiSpring;
|
import com.imyeyu.spring.TimiSpring;
|
||||||
import com.imyeyu.spring.util.Redis;
|
import com.imyeyu.spring.util.Redis;
|
||||||
@@ -29,6 +30,7 @@ import org.springframework.context.annotation.Lazy;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
|
||||||
@@ -45,10 +47,10 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
|
|||||||
private static final String PHONE_VERIFY_KEY_PREFIX = "USER:PHONE:VERIFY:";
|
private static final String PHONE_VERIFY_KEY_PREFIX = "USER:PHONE:VERIFY:";
|
||||||
private static final long PHONE_VERIFY_CODE_TTL = Time.M * 5;
|
private static final long PHONE_VERIFY_CODE_TTL = Time.M * 5;
|
||||||
|
|
||||||
private final SmsService smsService;
|
|
||||||
private final UserService userService;
|
private final UserService userService;
|
||||||
private final SettingService settingService;
|
private final SettingService settingService;
|
||||||
private final UserRoleService userRoleService;
|
private final UserRoleService userRoleService;
|
||||||
|
private final SmsNotifyService smsNotifyService;
|
||||||
private final AttachmentService attachmentService;
|
private final AttachmentService attachmentService;
|
||||||
|
|
||||||
private final Redis<String, String> redisUserToken;
|
private final Redis<String, String> redisUserToken;
|
||||||
@@ -102,15 +104,20 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginResponse loginBySms(SmsCaptchaVerifyRequest request) {
|
public LoginResponse loginBySms(CaptchaVerifyReq req) {
|
||||||
NotifyDetail detail = smsService.captchaVerify(request.getDetailId(), request.getCaptcha());
|
try {
|
||||||
// 无报错即成功
|
NotifyDetail detail = smsNotifyService.captchaVerify(req);
|
||||||
User user = userService.getByPhoneNo(detail.getSendTo());
|
// 无报错即成功
|
||||||
if (!user.isValidPhone()) {
|
User user = userService.getByPhoneNo(detail.getSendTo());
|
||||||
user.setPhoneNoVerifyAt(Time.now());
|
if (!user.isValidPhone()) {
|
||||||
userService.update(user);
|
user.setPhoneNoVerifyAt(Time.now());
|
||||||
|
userService.update(user);
|
||||||
|
}
|
||||||
|
return login(UUID.randomUUID().toString(), user.getId());
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("login by sms error", e);
|
||||||
|
throw new TimiException(TimiCode.ERROR, "验证短信失败");
|
||||||
}
|
}
|
||||||
return login(UUID.randomUUID().toString(), user.getId());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -7,58 +7,36 @@ import org.bytedeco.javacv.Java2DFrameConverter;
|
|||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
/// JavaCV 工具类
|
/**
|
||||||
///
|
*
|
||||||
/// @author 夜雨
|
*
|
||||||
/// @since 2025-10-23 17:05
|
* @author 夜雨
|
||||||
|
* @since 2025-10-23 17:05
|
||||||
|
*/
|
||||||
public class JavaCV {
|
public class JavaCV {
|
||||||
|
|
||||||
/// 从视频流顺序解码并提取指定时间附近的一帧 PNG 图片
|
public static ByteArrayOutputStream captureThumbnail(InputStream stream, double targetSeconds) throws Exception {
|
||||||
///
|
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
||||||
/// 不执行时间定位,避免为了取实时缩略图而把整个视频复制到临时文件
|
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(stream)) {
|
||||||
///
|
grabber.start();
|
||||||
/// @param stream 视频输入流
|
long targetMillis = (long) (targetSeconds * 1000);
|
||||||
/// @param targetSeconds 目标时间,单位为秒
|
grabber.setTimestamp(targetMillis);
|
||||||
/// @return PNG 图片字节流
|
Frame frame;
|
||||||
/// @throws Exception 视频解析或取帧失败
|
while ((frame = grabber.grabImage()) != null) {
|
||||||
public static ByteArrayOutputStream captureThumbnail(InputStream stream, double targetSeconds) throws Exception {
|
if (grabber.getTimestamp() >= targetMillis) {
|
||||||
long targetMicros = toMicros(targetSeconds);
|
Java2DFrameConverter converter = new Java2DFrameConverter();
|
||||||
ByteArrayOutputStream firstFrame = new ByteArrayOutputStream();
|
try (converter) {
|
||||||
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(stream)) {
|
BufferedImage bi = converter.getBufferedImage(frame);
|
||||||
grabber.start();
|
if (bi != null) {
|
||||||
try (Java2DFrameConverter converter = new Java2DFrameConverter()) {
|
ImageIO.write(bi, "png", outStream);
|
||||||
Frame frame;
|
break;
|
||||||
while ((frame = grabber.grabImage()) != null) {
|
}
|
||||||
BufferedImage image = converter.getBufferedImage(frame);
|
}
|
||||||
if (image == null) {
|
}
|
||||||
continue;
|
}
|
||||||
}
|
}
|
||||||
if (!(0 < firstFrame.size())) {
|
return outStream;
|
||||||
firstFrame = toPng(image);
|
}
|
||||||
}
|
|
||||||
if (grabber.getTimestamp() >= targetMicros) {
|
|
||||||
return targetMicros == 0 ? firstFrame : toPng(image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 短视频提前结束时退回首个有效帧
|
|
||||||
return firstFrame;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static long toMicros(double targetSeconds) {
|
|
||||||
double seconds = Double.isFinite(targetSeconds) ? Math.max(0, targetSeconds) : 0;
|
|
||||||
return Math.round(seconds * 1_000_000D);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static ByteArrayOutputStream toPng(BufferedImage image) throws IOException {
|
|
||||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
|
||||||
if (ImageIO.write(image, "png", output)) {
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
return new ByteArrayOutputStream();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
CREATE TABLE `attachment_variant` (
|
|
||||||
`id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`source_attachment_id` VARCHAR(36) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`source_mongo_id` VARCHAR(24) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`source_md5` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
|
||||||
`request_width` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
||||||
`request_height` INT UNSIGNED NOT NULL DEFAULT 0,
|
|
||||||
`fit_mode` VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`output_format` VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`algorithm_version` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`mongo_id` VARCHAR(24) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`name` VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
|
||||||
`mime_type` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
|
|
||||||
`metadata` LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
|
||||||
`size` BIGINT(20) NOT NULL,
|
|
||||||
`md5` VARCHAR(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,
|
|
||||||
`created_at` BIGINT(20) NOT NULL,
|
|
||||||
`expire_at` BIGINT(20) NOT NULL,
|
|
||||||
PRIMARY KEY (`id`) USING BTREE,
|
|
||||||
UNIQUE INDEX `uk_source_variant`(
|
|
||||||
`source_attachment_id`,
|
|
||||||
`source_mongo_id`,
|
|
||||||
`request_width`,
|
|
||||||
`request_height`,
|
|
||||||
`fit_mode`,
|
|
||||||
`output_format`,
|
|
||||||
`algorithm_version`
|
|
||||||
) USING BTREE,
|
|
||||||
UNIQUE INDEX `uk_mongo_id`(`mongo_id`) USING BTREE,
|
|
||||||
INDEX `idx_source_attachment_id`(`source_attachment_id`) USING BTREE,
|
|
||||||
INDEX `idx_expire_at`(`expire_at`) USING BTREE
|
|
||||||
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '附件缩略图变体' ROW_FORMAT = Dynamic;
|
|
||||||
@@ -26,4 +26,33 @@
|
|||||||
</if>
|
</if>
|
||||||
ORDER BY `created_at`
|
ORDER BY `created_at`
|
||||||
</select>
|
</select>
|
||||||
|
<select id="selectThumb" resultType="com.imyeyu.api.modules.common.entity.Attachment">
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
`attachment`
|
||||||
|
WHERE
|
||||||
|
biz_type = #{bizType}
|
||||||
|
AND biz_id = #{sourceId}
|
||||||
|
AND attach_type = 'THUMB'
|
||||||
|
<choose>
|
||||||
|
<when test="requestWidth != null">
|
||||||
|
AND JSON_EXTRACT(metadata, '$.requestWidth') = #{requestWidth}
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
AND JSON_EXTRACT(metadata, '$.requestWidth') IS NULL
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
<choose>
|
||||||
|
<when test="requestHeight != null">
|
||||||
|
AND JSON_EXTRACT(metadata, '$.requestHeight') = #{requestHeight}
|
||||||
|
</when>
|
||||||
|
<otherwise>
|
||||||
|
AND JSON_EXTRACT(metadata, '$.requestHeight') IS NULL
|
||||||
|
</otherwise>
|
||||||
|
</choose>
|
||||||
|
AND is_destroyed = FALSE
|
||||||
|
AND UNIX_TIMESTAMP() * 1000 < destroy_at
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
<?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.common.mapper.AttachmentVariantMapper">
|
|
||||||
<sql id="variantKey">
|
|
||||||
`source_attachment_id` = #{sourceAttachmentId}
|
|
||||||
AND `source_mongo_id` = #{sourceMongoId}
|
|
||||||
AND `request_width` = #{requestWidth}
|
|
||||||
AND `request_height` = #{requestHeight}
|
|
||||||
AND `fit_mode` = #{fitMode}
|
|
||||||
AND `output_format` = #{outputFormat}
|
|
||||||
AND `algorithm_version` = #{algorithmVersion}
|
|
||||||
</sql>
|
|
||||||
|
|
||||||
<select id="selectValidAttachment" resultType="com.imyeyu.api.modules.common.entity.Attachment">
|
|
||||||
SELECT
|
|
||||||
v.`id`,
|
|
||||||
a.`biz_type`,
|
|
||||||
v.`source_attachment_id` AS `biz_id`,
|
|
||||||
'THUMB' AS `attach_type`,
|
|
||||||
v.`mongo_id`,
|
|
||||||
v.`name`,
|
|
||||||
v.`mime_type`,
|
|
||||||
v.`metadata`,
|
|
||||||
v.`size`,
|
|
||||||
v.`md5`,
|
|
||||||
FALSE AS `is_destroyed`,
|
|
||||||
v.`created_at`,
|
|
||||||
v.`expire_at` AS `destroy_at`
|
|
||||||
FROM `attachment_variant` v
|
|
||||||
INNER JOIN `attachment` a ON a.`id` = v.`source_attachment_id`
|
|
||||||
WHERE
|
|
||||||
<include refid="variantKey" />
|
|
||||||
AND v.`expire_at` > #{now}
|
|
||||||
AND a.`mongo_id` = v.`source_mongo_id`
|
|
||||||
AND a.`is_destroyed` = FALSE
|
|
||||||
AND (a.`deleted_at` IS NULL OR #{now} < a.`deleted_at`)
|
|
||||||
AND (a.`destroy_at` IS NULL OR #{now} < a.`destroy_at`)
|
|
||||||
LIMIT 1
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectByKey" resultType="com.imyeyu.api.modules.common.entity.AttachmentVariant">
|
|
||||||
SELECT *
|
|
||||||
FROM `attachment_variant`
|
|
||||||
WHERE
|
|
||||||
<include refid="variantKey" />
|
|
||||||
LIMIT 1
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectBySourceAttachmentId" resultType="com.imyeyu.api.modules.common.entity.AttachmentVariant">
|
|
||||||
SELECT *
|
|
||||||
FROM `attachment_variant`
|
|
||||||
WHERE `source_attachment_id` = #{sourceAttachmentId}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<select id="selectExpired" resultType="com.imyeyu.api.modules.common.entity.AttachmentVariant">
|
|
||||||
SELECT *
|
|
||||||
FROM `attachment_variant`
|
|
||||||
WHERE `expire_at` <= #{now}
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<insert id="insert">
|
|
||||||
INSERT INTO `attachment_variant` (
|
|
||||||
`id`,
|
|
||||||
`source_attachment_id`,
|
|
||||||
`source_mongo_id`,
|
|
||||||
`source_md5`,
|
|
||||||
`request_width`,
|
|
||||||
`request_height`,
|
|
||||||
`fit_mode`,
|
|
||||||
`output_format`,
|
|
||||||
`algorithm_version`,
|
|
||||||
`mongo_id`,
|
|
||||||
`name`,
|
|
||||||
`mime_type`,
|
|
||||||
`metadata`,
|
|
||||||
`size`,
|
|
||||||
`md5`,
|
|
||||||
`created_at`,
|
|
||||||
`expire_at`
|
|
||||||
) VALUES (
|
|
||||||
#{id},
|
|
||||||
#{sourceAttachmentId},
|
|
||||||
#{sourceMongoId},
|
|
||||||
#{sourceMd5},
|
|
||||||
#{requestWidth},
|
|
||||||
#{requestHeight},
|
|
||||||
#{fitMode},
|
|
||||||
#{outputFormat},
|
|
||||||
#{algorithmVersion},
|
|
||||||
#{mongoId},
|
|
||||||
#{name},
|
|
||||||
#{mimeType},
|
|
||||||
#{metadata},
|
|
||||||
#{size},
|
|
||||||
#{md5},
|
|
||||||
#{createdAt},
|
|
||||||
#{expireAt}
|
|
||||||
)
|
|
||||||
</insert>
|
|
||||||
|
|
||||||
<delete id="delete">
|
|
||||||
DELETE FROM `attachment_variant`
|
|
||||||
WHERE `id` = #{id}
|
|
||||||
</delete>
|
|
||||||
</mapper>
|
|
||||||
Reference in New Issue
Block a user