Compare commits
21
Commits
v1.0.19
...
cd3d522d4f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd3d522d4f | ||
|
|
8de739cbb7 | ||
|
|
181c5078e9 | ||
|
|
e2e0023d13 | ||
|
|
8c6eed2379 | ||
|
|
c32ecd1f8c | ||
|
|
3c56e21efa | ||
|
|
376f0ecd05 | ||
|
|
e97f46e6b4 | ||
|
|
a208947a1b | ||
|
|
c0f89deb83 | ||
|
|
fc62663f2c | ||
|
|
fcae2eac09 | ||
|
|
2e9225852d | ||
|
|
3a3809b661 | ||
|
|
1b8936f197 | ||
|
|
ce695463d3 | ||
|
|
aff056712e | ||
|
|
af18b0999d | ||
|
|
83c683d473 | ||
|
|
4963988ec2 |
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
<groupId>com.imyeyu.timiserverapi</groupId>
|
<groupId>com.imyeyu.timiserverapi</groupId>
|
||||||
<artifactId>TimiServerAPI</artifactId>
|
<artifactId>TimiServerAPI</artifactId>
|
||||||
<version>1.0.19</version>
|
<version>1.0.24</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.1</version>
|
<version>0.0.3</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -240,11 +240,6 @@
|
|||||||
<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,12 +1,9 @@
|
|||||||
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;
|
||||||
@@ -32,12 +29,6 @@ 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();
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.imyeyu.api.modules.common.bean.attachment;
|
||||||
|
|
||||||
|
/// 缩略图缩放模式
|
||||||
|
///
|
||||||
|
/// @author 夜雨
|
||||||
|
/// @since 2026-08-21
|
||||||
|
public enum ThumbnailFitMode {
|
||||||
|
|
||||||
|
/// 保持比例完整放入目标尺寸
|
||||||
|
FIT,
|
||||||
|
|
||||||
|
/// 保持比例裁剪填满目标尺寸
|
||||||
|
COVER,
|
||||||
|
|
||||||
|
/// 强制缩放到目标尺寸,可能导致图片变形
|
||||||
|
STRETCH
|
||||||
|
}
|
||||||
@@ -42,6 +42,19 @@ public class NotifyQueueConfig {
|
|||||||
private String secretId;
|
private String secretId;
|
||||||
|
|
||||||
private String secretKey;
|
private String secretKey;
|
||||||
|
|
||||||
|
private Captcha captcha;
|
||||||
|
|
||||||
|
/// 短信验证码
|
||||||
|
@Data
|
||||||
|
public static class Captcha {
|
||||||
|
|
||||||
|
private String templateId;
|
||||||
|
|
||||||
|
private int ttlMinute;
|
||||||
|
|
||||||
|
private int argIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 微信队列配置
|
/// 微信队列配置
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
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;
|
||||||
@@ -41,6 +43,7 @@ public class AttachmentController {
|
|||||||
|
|
||||||
private final TempFileService tempFileService;
|
private final TempFileService tempFileService;
|
||||||
private final AttachmentService service;
|
private final AttachmentService service;
|
||||||
|
private final AttachmentVariantService variantService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询附件详情
|
* 查询附件详情
|
||||||
@@ -184,14 +187,19 @@ public class AttachmentController {
|
|||||||
@AOPLog
|
@AOPLog
|
||||||
@IgnoreGlobalReturn
|
@IgnoreGlobalReturn
|
||||||
@GetMapping({"/read/{id}", "/download/{id}"})
|
@GetMapping({"/read/{id}", "/download/{id}"})
|
||||||
public void readById(@PathVariable String id, @RequestParam(required = false) Integer thumbWidth, @RequestParam(required = false) Integer thumbHeight) throws UnsupportedEncodingException {
|
public void readById(
|
||||||
|
@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);
|
responseAttachment(attach, thumbWidth, thumbHeight, fitMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AOPLog
|
@AOPLog
|
||||||
@@ -207,21 +215,31 @@ 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);
|
response(attach, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void responseAttachment(Attachment attach, Integer thumbWidth, Integer thumbHeight) throws UnsupportedEncodingException {
|
private void responseAttachment(
|
||||||
HttpServletResponse resp = TimiSpring.getResponse();
|
Attachment attach,
|
||||||
if (attach != null && TimiSpring.getRequest().getRequestURI().endsWith("/download") && attach.getName() != null) {
|
Integer thumbWidth,
|
||||||
resp.setHeader("Content-Disposition", Network.getFileDownloadHeader(attach.getName()));
|
Integer thumbHeight,
|
||||||
}
|
ThumbnailFitMode fitMode
|
||||||
responseThumb(attach, thumbWidth, thumbHeight);
|
) throws UnsupportedEncodingException {
|
||||||
|
responseThumb(attach, thumbWidth, thumbHeight, fitMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void responseThumb(Attachment attachment, Integer thumbWidth, Integer thumbHeight) {
|
protected void responseThumb(
|
||||||
|
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);
|
||||||
@@ -229,21 +247,65 @@ public class AttachmentController {
|
|||||||
}
|
}
|
||||||
if (thumbWidth == null && thumbHeight == null) {
|
if (thumbWidth == null && thumbHeight == null) {
|
||||||
// 原图
|
// 原图
|
||||||
|
setDownloadHeader(attachment);
|
||||||
response(attachment);
|
response(attachment);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 缩略图
|
// 缩略图
|
||||||
Attachment thumb = service.fetchThumb(attachment, thumbWidth, thumbHeight);
|
Attachment thumb = variantService.fetchThumb(attachment, thumbWidth, thumbHeight, fitMode);
|
||||||
response(TimiJava.defaultIfNull(thumb, attachment));
|
setDownloadHeader(thumb);
|
||||||
|
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,11 +7,16 @@ 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;
|
||||||
@@ -37,6 +42,8 @@ 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;
|
||||||
@@ -104,6 +111,14 @@ 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 分页参数
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
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,5 +1,6 @@
|
|||||||
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;
|
||||||
@@ -28,7 +29,7 @@ public class BaseQueueEntity implements IDEntity<String>, Creatable, Destroyable
|
|||||||
protected String data;
|
protected String data;
|
||||||
|
|
||||||
/** 参数 */
|
/** 参数 */
|
||||||
protected String args;
|
protected JsonNode args;
|
||||||
|
|
||||||
/** 发送去向 */
|
/** 发送去向 */
|
||||||
protected String sendTo;
|
protected String sendTo;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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;
|
||||||
@@ -103,7 +104,7 @@ public class NotifyDetail extends UUIDEntity {
|
|||||||
|
|
||||||
/** 参数 */
|
/** 参数 */
|
||||||
@JsonView(ResponseView.Public.class)
|
@JsonView(ResponseView.Public.class)
|
||||||
private String args;
|
private JsonNode args;
|
||||||
|
|
||||||
/** 发送来源 */
|
/** 发送来源 */
|
||||||
@JsonView(ResponseView.Public.class)
|
@JsonView(ResponseView.Public.class)
|
||||||
|
|||||||
@@ -36,26 +36,6 @@ 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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
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,18 +25,6 @@ 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);
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.imyeyu.api.modules.common.service;
|
||||||
|
|
||||||
|
import com.imyeyu.api.modules.common.entity.Notify;
|
||||||
|
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-05-14 12:12
|
||||||
|
*/
|
||||||
|
public interface SmsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建短信验证码通知
|
||||||
|
*
|
||||||
|
* @param detail 通知
|
||||||
|
* @return 通知详情 ID
|
||||||
|
*/
|
||||||
|
String captchaCreate(Notify detail);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验短信验证码
|
||||||
|
*
|
||||||
|
* @param detailId 通知详情 ID
|
||||||
|
* @param captcha 请求验证码
|
||||||
|
* @return 通知详情
|
||||||
|
*/
|
||||||
|
NotifyDetail captchaVerify(String detailId, String captcha);
|
||||||
|
}
|
||||||
@@ -39,4 +39,10 @@ public interface TagService extends BaseService<Tag, String> {
|
|||||||
/// @param bizId 业务 ID
|
/// @param bizId 业务 ID
|
||||||
/// @param tagIdList 标签 ID 列表
|
/// @param tagIdList 标签 ID 列表
|
||||||
void apply(TagApply.BizType bizType, String bizId, List<String> tagIdList);
|
void apply(TagApply.BizType bizType, String bizId, List<String> tagIdList);
|
||||||
|
|
||||||
|
/// 删除指定归属下的全部标签
|
||||||
|
///
|
||||||
|
/// @param ownerType 归属类型
|
||||||
|
/// @param ownerId 归属 ID
|
||||||
|
void deleteByOwner(Tag.OwnerType ownerType, String ownerId);
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-100
@@ -3,19 +3,14 @@ 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.util.JavaCV;
|
import com.imyeyu.api.modules.common.service.AttachmentVariantService;
|
||||||
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;
|
||||||
@@ -26,7 +21,6 @@ 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;
|
||||||
@@ -36,9 +30,6 @@ 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;
|
||||||
@@ -47,7 +38,6 @@ 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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +54,7 @@ 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() {
|
||||||
@@ -124,6 +115,7 @@ 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)));
|
||||||
@@ -141,6 +133,8 @@ 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) {
|
||||||
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
|
// 新文件已上传但后续操作失败,清理新文件防止孤儿数据
|
||||||
@@ -155,36 +149,18 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void delete(String id) {
|
public void delete(String id) {
|
||||||
mapper.selectThumbsBySourceId(id).forEach(thumb -> super.delete(thumb.getId()));
|
variantService.deleteBySourceId(id);
|
||||||
super.delete(id);
|
super.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void destroy(String id) {
|
public void destroy(String id) {
|
||||||
CallbackArg<Attachment> doDestroy = attach -> {
|
variantService.deleteBySourceId(id);
|
||||||
try {
|
Attachment attachment = mapper.selectRaw(id);
|
||||||
if (attach.getMongoId() != null) {
|
if (attachment != null) {
|
||||||
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attach.getMongoId())));
|
destroyAttachment(attachment);
|
||||||
}
|
}
|
||||||
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)
|
||||||
@@ -216,74 +192,20 @@ public class AttachmentServiceImplement extends AbstractEntityService<Attachment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
private void destroyAttachment(Attachment attachment) {
|
||||||
@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 {
|
||||||
ByteArrayOutputStream thumbStream = new ByteArrayOutputStream();
|
if (attachment.getMongoId() != null) {
|
||||||
CallbackArg<InputStream> applyThumbSize = inputStream -> {
|
gridFsTemplate.delete(Query.query(Criteria.where("_id").is(attachment.getMongoId())));
|
||||||
try {
|
|
||||||
Thumbnails.Builder<? extends InputStream> builder = Thumbnails.of(inputStream);
|
|
||||||
if (requestWidth != null && requestHeight != null) {
|
|
||||||
builder.size(requestWidth, requestHeight).keepAspectRatio(false).toOutputStream(thumbStream);
|
|
||||||
} else if (requestWidth != null) {
|
|
||||||
builder.width(requestWidth).keepAspectRatio(true).toOutputStream(thumbStream);
|
|
||||||
} else {
|
|
||||||
builder.height(requestHeight).keepAspectRatio(true).toOutputStream(thumbStream);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
log.error("applyThumbSize error for fetchThumb", e);
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (isImage) {
|
|
||||||
applyThumbSize.handler(source.openInputStream());
|
|
||||||
} else {
|
|
||||||
String name = source.getName();
|
|
||||||
File tempFile = IO.file("temp/%s_%s".formatted(UUID.randomUUID(), name));
|
|
||||||
try {
|
|
||||||
IO.toFile(tempFile, source.openInputStream());
|
|
||||||
ByteArrayOutputStream baos = JavaCV.captureThumbnail(IO.getInputStream(tempFile), 2);
|
|
||||||
applyThumbSize.handler(IO.toInputStream(baos));
|
|
||||||
} finally {
|
|
||||||
IO.destroy(tempFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Metadata.ThumbImage thumbMeta = new Metadata.ThumbImage();
|
if (!attachment.isDeleted()) {
|
||||||
thumbMeta.setSourceId(source.getId());
|
attachment.setDeletedAt(Time.now());
|
||||||
thumbMeta.setSourceMongoId(source.getMongoId());
|
}
|
||||||
thumbMeta.setSourceMimeType(mimeType);
|
attachment.setIsDestroyed(true);
|
||||||
thumbMeta.setRequestWidth(requestWidth);
|
attachment.setDestroyAt(Time.now());
|
||||||
thumbMeta.setRequestHeight(requestHeight);
|
mapper.update(attachment);
|
||||||
|
|
||||||
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("create thumbnail error", e);
|
log.error("delete mongo file error, id={}", attachment.getId(), e);
|
||||||
throw new TimiException(TimiCode.ERROR, "exception.attachment.create.error");
|
throw new TimiException(TimiCode.ERROR, "TODO delete mongo file error", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,6 +312,8 @@ 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
@@ -0,0 +1,339 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-8
@@ -1,6 +1,5 @@
|
|||||||
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;
|
||||||
@@ -11,9 +10,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;
|
||||||
@@ -56,12 +55,7 @@ public class SmsQueueServiceImplement extends AbstractQueueService<SmsQueue, Not
|
|||||||
@Override
|
@Override
|
||||||
protected void doSend(SmsQueue queue) {
|
protected void doSend(SmsQueue queue) {
|
||||||
try {
|
try {
|
||||||
String[] args = new String[0];
|
String[] args = jackson.treeToValue(queue.getArgs(), String[].class);
|
||||||
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();
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
package com.imyeyu.api.modules.common.service.implement;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||||
|
import com.imyeyu.api.modules.common.config.NotifyQueueConfig;
|
||||||
|
import com.imyeyu.api.modules.common.entity.Notify;
|
||||||
|
import com.imyeyu.api.modules.common.entity.NotifyDetail;
|
||||||
|
import com.imyeyu.api.modules.common.service.NotifyDetailService;
|
||||||
|
import com.imyeyu.api.modules.common.service.NotifyService;
|
||||||
|
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.utils.Regex;
|
||||||
|
import com.imyeyu.utils.Text;
|
||||||
|
import com.imyeyu.utils.Time;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @author 夜雨
|
||||||
|
* @since 2026-05-14 12:12
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SmsServiceImplement implements SmsService {
|
||||||
|
|
||||||
|
@Value("${spring.profiles.active}")
|
||||||
|
private String env;
|
||||||
|
|
||||||
|
private final NotifyQueueConfig config;
|
||||||
|
private final ObjectMapper jackson;
|
||||||
|
|
||||||
|
private final NotifyService notifyService;
|
||||||
|
private final NotifyDetailService notifyDetailService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String captchaCreate(Notify notify) {
|
||||||
|
NotifyQueueConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
|
||||||
|
|
||||||
|
NotifyDetail detail = notify.getDetailList().getFirst();
|
||||||
|
|
||||||
|
TimiException.requiredTrue(Regex.isMatch(Regex.MOBILE_PHONE, detail.getSendTo()), "invalid mobilePhone");
|
||||||
|
|
||||||
|
String code = Text.randomString("0123456789", 6);
|
||||||
|
if (!env.equalsIgnoreCase("prod")) {
|
||||||
|
code = "123456";
|
||||||
|
}
|
||||||
|
detail.setMsgType(NotifyDetail.MsgType.SMS);
|
||||||
|
detail.setTemplateId(captchaConf.getTemplateId());
|
||||||
|
detail.setArgs(jackson.valueToTree(List.of(code, captchaConf.getTtlMinute())));
|
||||||
|
|
||||||
|
notifyService.create(notify);
|
||||||
|
return notifyDetailService.listByNotifyId(notify.getId()).getFirst().getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public NotifyDetail captchaVerify(String detailId, String captcha) {
|
||||||
|
NotifyQueueConfig.Sms.Captcha captchaConf = config.getSms().getCaptcha();
|
||||||
|
|
||||||
|
NotifyDetail detail = notifyDetailService.get(detailId);
|
||||||
|
TimiException.required(detail, "短信验证码无效");
|
||||||
|
TimiException.required(detail.getSendAt(), "短信验证码无效");
|
||||||
|
TimiException.requiredTrue(NotifyDetail.MsgType.SMS == detail.getMsgType(), "短信验证码无效");
|
||||||
|
if (Time.M * captchaConf.getTtlMinute() < Time.now() - detail.getSendAt()) {
|
||||||
|
throw new TimiException(TimiCode.ARG_EXPIRED, "验证码过期");
|
||||||
|
}
|
||||||
|
if (detail.getArgs() instanceof ArrayNode array) {
|
||||||
|
String dbCaptcha = array.get(captchaConf.getArgIndex()).asText();
|
||||||
|
if (!dbCaptcha.equalsIgnoreCase(captcha)) {
|
||||||
|
throw new TimiException(TimiCode.RESULT_BAD, "验证码错误");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new TimiException(TimiCode.ERROR, "not array args");
|
||||||
|
}
|
||||||
|
detail.setNotify(notifyService.get(detail.getNotifyId()));
|
||||||
|
return detail;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -1,5 +1,6 @@
|
|||||||
package com.imyeyu.api.modules.common.service.implement;
|
package com.imyeyu.api.modules.common.service.implement;
|
||||||
|
|
||||||
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
import com.imyeyu.api.modules.common.entity.Tag;
|
import com.imyeyu.api.modules.common.entity.Tag;
|
||||||
import com.imyeyu.api.modules.common.entity.TagApply;
|
import com.imyeyu.api.modules.common.entity.TagApply;
|
||||||
import com.imyeyu.api.modules.common.mapper.TagApplyMapper;
|
import com.imyeyu.api.modules.common.mapper.TagApplyMapper;
|
||||||
@@ -14,6 +15,7 @@ import com.imyeyu.spring.service.AbstractEntityService;
|
|||||||
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;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -104,6 +106,19 @@ public class TagServiceImplement extends AbstractEntityService<Tag, String> impl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void deleteByOwner(Tag.OwnerType ownerType, String ownerId) {
|
||||||
|
TimiException.required(ownerType, "not found ownerType");
|
||||||
|
TimiException.required(ownerId, "not found ownerId");
|
||||||
|
Tag example = new Tag();
|
||||||
|
example.setOwnerType(ownerType);
|
||||||
|
example.setOwnerId(ownerId);
|
||||||
|
for (Tag tag : mapper.selectAllByExample(example)) {
|
||||||
|
super.delete(tag.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<TagApply> listApply(TagApply.BizType bizType, String bizId) {
|
private List<TagApply> listApply(TagApply.BizType bizType, String bizId) {
|
||||||
TimiException.required(bizType, "not found bizType");
|
TimiException.required(bizType, "not found bizType");
|
||||||
TimiException.required(bizId, "not found bizId");
|
TimiException.required(bizId, "not found bizId");
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ 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;
|
||||||
@@ -23,6 +24,7 @@ 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() {
|
||||||
@@ -31,19 +33,20 @@ 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");
|
||||||
return;
|
} else {
|
||||||
}
|
for (Attachment attach : needDestroyList) {
|
||||||
for (Attachment attach : needDestroyList) {
|
try {
|
||||||
try {
|
attachmentService.destroy(attach.getId());
|
||||||
attachmentService.destroy(attach.getId());
|
log.info("clear attachment success: id[{}]", attach.getId());
|
||||||
log.info("clear attachment success: id[{}]", attach.getId());
|
} catch (Exception e) {
|
||||||
} catch (Exception e) {
|
log.error("clear attachment error: id[{}]", attach.getId(), 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
@@ -7,11 +7,15 @@ import com.imyeyu.api.modules.gao.bean.GaoRoleCode;
|
|||||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoStoreService;
|
import com.imyeyu.api.modules.gao.service.GaoStoreService;
|
||||||
|
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
|
||||||
import com.imyeyu.api.modules.user.service.RoleChecker;
|
import com.imyeyu.api.modules.user.service.RoleChecker;
|
||||||
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
import com.imyeyu.spring.annotation.AOPLog;
|
import com.imyeyu.spring.annotation.AOPLog;
|
||||||
import com.imyeyu.spring.annotation.RequestRateLimit;
|
import com.imyeyu.spring.annotation.RequestRateLimit;
|
||||||
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
@@ -32,12 +36,30 @@ public class GaoPointAccountController {
|
|||||||
@AOPLog
|
@AOPLog
|
||||||
@RequestRateLimit
|
@RequestRateLimit
|
||||||
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
|
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
|
||||||
@PostMapping("/account/detail")
|
@PostMapping("/account/list")
|
||||||
|
public PageResult<GaoPointAccount> list(@RequestBody GaoPointAccountPage page) {
|
||||||
|
TimiException.required(page, "not found page");
|
||||||
|
page.setStoreId(resolveStoreId(page.getStoreId()));
|
||||||
|
return accountService.pageByQuery(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AOPLog
|
||||||
|
@RequestRateLimit
|
||||||
|
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
|
||||||
|
@PostMapping("/account/detail/customer")
|
||||||
public GaoPointAccount detail(@RequestParam String customerId, @RequestParam(required = false) String storeId) {
|
public GaoPointAccount detail(@RequestParam String customerId, @RequestParam(required = false) String storeId) {
|
||||||
storeId = resolveStoreId(storeId);
|
storeId = resolveStoreId(storeId);
|
||||||
return accountService.getByCustomerId(storeId, customerId);
|
return accountService.getByCustomerId(storeId, customerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AOPLog
|
||||||
|
@RequestRateLimit
|
||||||
|
@RequireGaoPermission(GaoPermissionCode.POINT_ACCOUNT_READ)
|
||||||
|
@PostMapping("/account/detail")
|
||||||
|
public GaoPointAccount detailById(@RequestParam String id, @RequestParam(required = false) String storeId) {
|
||||||
|
return accountService.getByIdAndStoreId(resolveStoreId(storeId), id);
|
||||||
|
}
|
||||||
|
|
||||||
private String resolveStoreId(String requestedStoreId) {
|
private String resolveStoreId(String requestedStoreId) {
|
||||||
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
if (roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
||||||
return requestedStoreId;
|
return requestedStoreId;
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ 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;
|
||||||
@@ -80,12 +79,11 @@ 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);
|
String userId = inviteService.register(data, req.getDetailId(), req.getCaptcha());
|
||||||
return userLoginService.login(UUID.randomUUID().toString(), userId);
|
return userLoginService.login(UUID.randomUUID().toString(), userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,15 +61,15 @@ public class GaoCustomer extends UUIDEntity {
|
|||||||
/// 姓名
|
/// 姓名
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
@Column("zh_cn_pinyin_full")
|
@Column("name_pinyin_full")
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private String namePinyinFull;
|
private String namePinyinFull;
|
||||||
|
|
||||||
@Column("zh_cn_pinyin_initial")
|
@Column("name_pinyin_initial")
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private String namePinyinInitial;
|
private String namePinyinInitial;
|
||||||
|
|
||||||
@Column("zh_cn_pinyin_mixed")
|
@Column("name_pinyin_mixed")
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
private String namePinyinMixed;
|
private String namePinyinMixed;
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.imyeyu.api.modules.gao.entity;
|
package com.imyeyu.api.modules.gao.entity;
|
||||||
|
|
||||||
|
import com.imyeyu.spring.annotation.table.Transient;
|
||||||
import com.imyeyu.spring.entity.UUIDEntity;
|
import com.imyeyu.spring.entity.UUIDEntity;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
@@ -24,6 +25,9 @@ public class GaoPointAccount extends UUIDEntity {
|
|||||||
/// 累计获得积分
|
/// 累计获得积分
|
||||||
private Long totalEarned;
|
private Long totalEarned;
|
||||||
|
|
||||||
|
/// 累计消耗积分
|
||||||
|
private Long totalConsumed;
|
||||||
|
|
||||||
/// 累计撤销积分
|
/// 累计撤销积分
|
||||||
private Long totalRevoked;
|
private Long totalRevoked;
|
||||||
|
|
||||||
@@ -32,4 +36,8 @@ public class GaoPointAccount extends UUIDEntity {
|
|||||||
|
|
||||||
/// 乐观锁版本
|
/// 乐观锁版本
|
||||||
private Long version;
|
private Long version;
|
||||||
|
|
||||||
|
/// 客户信息
|
||||||
|
@Transient
|
||||||
|
private GaoCustomer customer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ public class GaoPointLedger extends UUIDEntity {
|
|||||||
/// 流水变化类型
|
/// 流水变化类型
|
||||||
public enum ChangeType {
|
public enum ChangeType {
|
||||||
|
|
||||||
/// 登记事件积分变化
|
/// 登记事件发放积分
|
||||||
EARN,
|
EARN,
|
||||||
|
|
||||||
|
/// 登记事件消费积分
|
||||||
|
CONSUME,
|
||||||
|
|
||||||
/// 删除登记记录撤销积分
|
/// 删除登记记录撤销积分
|
||||||
REVOKE,
|
REVOKE,
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package com.imyeyu.api.modules.gao.mapper;
|
package com.imyeyu.api.modules.gao.mapper;
|
||||||
|
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||||
|
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
|
||||||
import com.imyeyu.spring.mapper.BaseMapper;
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/// GAO 客户积分账户 Mapper
|
/// GAO 客户积分账户 Mapper
|
||||||
///
|
///
|
||||||
/// @author Codex
|
/// @author Codex
|
||||||
@@ -17,6 +20,25 @@ public interface GaoPointAccountMapper extends BaseMapper<GaoPointAccount, Strin
|
|||||||
/// @return 积分账户
|
/// @return 积分账户
|
||||||
GaoPointAccount selectByStoreIdAndCustomerId(@Param("storeId") String storeId, @Param("customerId") String customerId);
|
GaoPointAccount selectByStoreIdAndCustomerId(@Param("storeId") String storeId, @Param("customerId") String customerId);
|
||||||
|
|
||||||
|
/// 按账户 ID 和门店查询积分账户
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID,可为空
|
||||||
|
/// @param id 账户 ID
|
||||||
|
/// @return 积分账户
|
||||||
|
GaoPointAccount selectByIdAndStoreId(@Param("storeId") String storeId, @Param("id") String id);
|
||||||
|
|
||||||
|
/// 统计积分账户数量
|
||||||
|
///
|
||||||
|
/// @param page 查询条件
|
||||||
|
/// @return 账户数量
|
||||||
|
long countByQuery(GaoPointAccountPage page);
|
||||||
|
|
||||||
|
/// 分页查询积分账户
|
||||||
|
///
|
||||||
|
/// @param page 查询条件
|
||||||
|
/// @return 账户列表
|
||||||
|
List<GaoPointAccount> selectByQuery(GaoPointAccountPage page);
|
||||||
|
|
||||||
/// 创建客户积分账户,已存在时保持原账户不变
|
/// 创建客户积分账户,已存在时保持原账户不变
|
||||||
///
|
///
|
||||||
/// @param account 积分账户
|
/// @param account 积分账户
|
||||||
@@ -33,9 +55,10 @@ public interface GaoPointAccountMapper extends BaseMapper<GaoPointAccount, Strin
|
|||||||
/// @param id 账户 ID
|
/// @param id 账户 ID
|
||||||
/// @param delta 本次余额变化
|
/// @param delta 本次余额变化
|
||||||
/// @param totalEarnedDelta 累计获得积分变化
|
/// @param totalEarnedDelta 累计获得积分变化
|
||||||
|
/// @param totalConsumedDelta 累计消耗积分变化
|
||||||
/// @param totalRevokedDelta 累计撤销积分变化
|
/// @param totalRevokedDelta 累计撤销积分变化
|
||||||
/// @param totalAdjustedDelta 累计人工调整积分变化
|
/// @param totalAdjustedDelta 累计人工调整积分变化
|
||||||
/// @param updatedAt 更新时间
|
/// @param updatedAt 更新时间
|
||||||
/// @return 更新行数
|
/// @return 更新行数
|
||||||
int updateBalance(@Param("id") String id, @Param("delta") long delta, @Param("totalEarnedDelta") long totalEarnedDelta, @Param("totalRevokedDelta") long totalRevokedDelta, @Param("totalAdjustedDelta") long totalAdjustedDelta, @Param("updatedAt") long updatedAt);
|
int updateBalance(@Param("id") String id, @Param("delta") long delta, @Param("totalEarnedDelta") long totalEarnedDelta, @Param("totalConsumedDelta") long totalConsumedDelta, @Param("totalRevokedDelta") long totalRevokedDelta, @Param("totalAdjustedDelta") long totalAdjustedDelta, @Param("updatedAt") long updatedAt);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ public interface GaoPointLedgerMapper extends BaseMapper<GaoPointLedger, String>
|
|||||||
/// @return 流水
|
/// @return 流水
|
||||||
GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey);
|
GaoPointLedger selectByIdempotencyKey(@Param("idempotencyKey") String idempotencyKey);
|
||||||
|
|
||||||
/// 查询登记记录产生的原始获得积分流水
|
/// 查询登记记录产生的原始规则积分流水
|
||||||
///
|
///
|
||||||
/// @param eventRecordId 登记记录 ID
|
/// @param eventRecordId 登记记录 ID
|
||||||
/// @return 获得积分流水
|
/// @return 原始规则积分流水
|
||||||
List<GaoPointLedger> selectEarnByEventRecordId(@Param("eventRecordId") String eventRecordId);
|
List<GaoPointLedger> selectRuleChangeByEventRecordId(@Param("eventRecordId") String eventRecordId);
|
||||||
|
|
||||||
/// 批量查询幂等键对应的流水
|
/// 批量查询幂等键对应的流水
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ public interface GaoCustomerService extends BaseService<GaoCustomer, String> {
|
|||||||
|
|
||||||
List<GaoCustomer> listByIdList(Collection<String> idList);
|
List<GaoCustomer> listByIdList(Collection<String> idList);
|
||||||
|
|
||||||
|
/// 删除门店下的全部客户及其关联业务数据
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
void deleteByStoreId(String storeId);
|
||||||
|
|
||||||
default Map<String, GaoCustomer> mapByIdList(Collection<String> idList) {
|
default Map<String, GaoCustomer> mapByIdList(Collection<String> idList) {
|
||||||
return listByIdList(idList).stream().collect(Collectors.toMap(GaoCustomer::getId, item -> item));
|
return listByIdList(idList).stream().collect(Collectors.toMap(GaoCustomer::getId, item -> item));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ public interface GaoEventService extends BaseService<GaoEvent, String> {
|
|||||||
/// @return 登记事件列表
|
/// @return 登记事件列表
|
||||||
List<GaoEvent> listValid(String storeId);
|
List<GaoEvent> listValid(String storeId);
|
||||||
|
|
||||||
|
/// 删除门店下的全部登记事件及角色关联
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
void deleteByStoreId(String storeId);
|
||||||
|
|
||||||
/// 查询登记事件要求的角色
|
/// 查询登记事件要求的角色
|
||||||
///
|
///
|
||||||
/// @param eventId 登记事件 ID
|
/// @param eventId 登记事件 ID
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.imyeyu.api.modules.gao.service;
|
package com.imyeyu.api.modules.gao.service;
|
||||||
|
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||||
|
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
|
||||||
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
import com.imyeyu.spring.service.BaseService;
|
import com.imyeyu.spring.service.BaseService;
|
||||||
|
|
||||||
/// GAO 客户积分账户服务
|
/// GAO 客户积分账户服务
|
||||||
@@ -9,6 +11,12 @@ import com.imyeyu.spring.service.BaseService;
|
|||||||
/// @since 2026-08-11
|
/// @since 2026-08-11
|
||||||
public interface GaoPointAccountService extends BaseService<GaoPointAccount, String> {
|
public interface GaoPointAccountService extends BaseService<GaoPointAccount, String> {
|
||||||
|
|
||||||
|
/// 分页查询积分账户
|
||||||
|
///
|
||||||
|
/// @param page 查询条件
|
||||||
|
/// @return 账户分页
|
||||||
|
PageResult<GaoPointAccount> pageByQuery(GaoPointAccountPage page);
|
||||||
|
|
||||||
/// 查询客户当前有效积分账户
|
/// 查询客户当前有效积分账户
|
||||||
///
|
///
|
||||||
/// @param storeId 门店 ID
|
/// @param storeId 门店 ID
|
||||||
@@ -16,6 +24,18 @@ public interface GaoPointAccountService extends BaseService<GaoPointAccount, Str
|
|||||||
/// @return 积分账户,不存在时返回 null
|
/// @return 积分账户,不存在时返回 null
|
||||||
GaoPointAccount getByCustomerId(String storeId, String customerId);
|
GaoPointAccount getByCustomerId(String storeId, String customerId);
|
||||||
|
|
||||||
|
/// 按账户 ID 查询积分账户
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID,可为空
|
||||||
|
/// @param id 账户 ID
|
||||||
|
/// @return 积分账户,不存在时返回 null
|
||||||
|
GaoPointAccount getByIdAndStoreId(String storeId, String id);
|
||||||
|
|
||||||
|
/// 删除门店下的全部积分账户
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
void deleteByStoreId(String storeId);
|
||||||
|
|
||||||
/// 获取并锁定客户积分账户,不存在时创建
|
/// 获取并锁定客户积分账户,不存在时创建
|
||||||
///
|
///
|
||||||
/// @param storeId 门店 ID
|
/// @param storeId 门店 ID
|
||||||
@@ -28,8 +48,9 @@ public interface GaoPointAccountService extends BaseService<GaoPointAccount, Str
|
|||||||
/// @param accountId 账户 ID
|
/// @param accountId 账户 ID
|
||||||
/// @param delta 本次余额变化
|
/// @param delta 本次余额变化
|
||||||
/// @param totalEarnedDelta 累计获得积分变化
|
/// @param totalEarnedDelta 累计获得积分变化
|
||||||
|
/// @param totalConsumedDelta 累计消耗积分变化
|
||||||
/// @param totalRevokedDelta 累计撤销积分变化
|
/// @param totalRevokedDelta 累计撤销积分变化
|
||||||
/// @param totalAdjustedDelta 累计人工调整积分变化
|
/// @param totalAdjustedDelta 累计人工调整积分变化
|
||||||
/// @return 更新行数
|
/// @return 更新行数
|
||||||
int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta);
|
int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalConsumedDelta, long totalRevokedDelta, long totalAdjustedDelta);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,18 +15,18 @@ import com.imyeyu.spring.service.BaseService;
|
|||||||
/// @since 2026-08-11
|
/// @since 2026-08-11
|
||||||
public interface GaoPointLedgerService extends BaseService<GaoPointLedger, String> {
|
public interface GaoPointLedgerService extends BaseService<GaoPointLedger, String> {
|
||||||
|
|
||||||
/// 写入登记获得积分流水
|
/// 写入登记规则积分流水
|
||||||
///
|
///
|
||||||
/// @param record 登记记录
|
/// @param record 登记记录
|
||||||
/// @param rule 命中规则
|
/// @param rule 命中规则
|
||||||
/// @param pointDelta 积分变化
|
/// @param pointDelta 积分变化
|
||||||
/// @param sourcePayload 来源快照
|
/// @param sourcePayload 来源快照
|
||||||
/// @return 积分流水
|
/// @return 积分流水
|
||||||
GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload);
|
GaoPointLedger applyRule(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload);
|
||||||
|
|
||||||
/// 写入删除登记记录的撤销流水
|
/// 写入删除登记记录的撤销流水
|
||||||
///
|
///
|
||||||
/// @param originLedger 原始获得积分流水
|
/// @param originLedger 原始规则积分流水
|
||||||
/// @param record 登记记录
|
/// @param record 登记记录
|
||||||
/// @return 撤销流水
|
/// @return 撤销流水
|
||||||
GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record);
|
GaoPointLedger revoke(GaoPointLedger originLedger, GaoEventRecord record);
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ public interface GaoPointRuleService extends BaseService<GaoPointRule, String> {
|
|||||||
/// @return 规则列表
|
/// @return 规则列表
|
||||||
List<GaoPointRule> listByIdList(List<String> idList);
|
List<GaoPointRule> listByIdList(List<String> idList);
|
||||||
|
|
||||||
|
/// 删除门店下的全部积分规则
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
void deleteByStoreId(String storeId);
|
||||||
|
|
||||||
/// 按客户端提交的顺序更新启用规则优先级
|
/// 按客户端提交的顺序更新启用规则优先级
|
||||||
///
|
///
|
||||||
/// @param idList 规则 ID 列表
|
/// @param idList 规则 ID 列表
|
||||||
|
|||||||
@@ -36,4 +36,9 @@ public interface GaoStoreBusinessDayService extends BaseService<GaoStoreBusiness
|
|||||||
/// @param endDateValue 结束日期值,格式为 yyyyMMdd
|
/// @param endDateValue 结束日期值,格式为 yyyyMMdd
|
||||||
/// @return 营业日列表
|
/// @return 营业日列表
|
||||||
java.util.List<GaoStoreBusinessDay> listByDateRange(String storeId, Integer beginDateValue, Integer endDateValue);
|
java.util.List<GaoStoreBusinessDay> listByDateRange(String storeId, Integer beginDateValue, Integer endDateValue);
|
||||||
|
|
||||||
|
/// 删除门店下的全部营业日
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
void deleteByStoreId(String storeId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ public interface GaoUserInviteService {
|
|||||||
/// 使用邀请注册 GAO 用户
|
/// 使用邀请注册 GAO 用户
|
||||||
///
|
///
|
||||||
/// @param data 注册数据
|
/// @param data 注册数据
|
||||||
|
/// @param smsDetailId 短信通知详情 ID
|
||||||
|
/// @param smsCaptcha 短信验证码
|
||||||
/// @return 注册成功的用户 ID
|
/// @return 注册成功的用户 ID
|
||||||
String register(GaoUserInviteRegisterData data);
|
String register(GaoUserInviteRegisterData data, String smsDetailId, String smsCaptcha);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ import com.imyeyu.spring.service.BaseService;
|
|||||||
/// @since 2026-07-30
|
/// @since 2026-07-30
|
||||||
public interface GaoUserService extends BaseService<GaoUser, String> {
|
public interface GaoUserService extends BaseService<GaoUser, String> {
|
||||||
|
|
||||||
|
/// 删除门店下的全部 GAO 用户关系,不删除核心用户
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
void deleteByStoreId(String storeId);
|
||||||
|
|
||||||
/// 调整 GAO 用户所属门店
|
/// 调整 GAO 用户所属门店
|
||||||
///
|
///
|
||||||
/// @param gaoUser GAO 用户,必须包含 ID 和目标门店 ID
|
/// @param gaoUser GAO 用户,必须包含 ID 和目标门店 ID
|
||||||
|
|||||||
+11
@@ -252,6 +252,17 @@ public class GaoCustomerServiceImplement extends AbstractEntityService<GaoCustom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void deleteByStoreId(String storeId) {
|
||||||
|
TimiException.required(storeId, "not found storeId");
|
||||||
|
GaoCustomer example = new GaoCustomer();
|
||||||
|
example.setStoreId(storeId);
|
||||||
|
for (GaoCustomer customer : mapper.selectAllByExample(example)) {
|
||||||
|
delete(customer.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void updateWithAttachment(GaoCustomer customer) {
|
public void updateWithAttachment(GaoCustomer customer) {
|
||||||
|
|||||||
+14
@@ -172,6 +172,20 @@ public class GaoEventServiceImplement extends AbstractEntityService<GaoEvent, St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void deleteByStoreId(String storeId) {
|
||||||
|
TimiException.required(storeId, "not found storeId");
|
||||||
|
GaoEvent example = new GaoEvent();
|
||||||
|
example.setStoreId(storeId);
|
||||||
|
for (GaoEvent event : mapper.selectAllByExample(example)) {
|
||||||
|
GaoEventRoleRelation relationExample = new GaoEventRoleRelation();
|
||||||
|
relationExample.setEventId(event.getId());
|
||||||
|
roleRelationMapper.deleteAllByExample(relationExample);
|
||||||
|
delete(event.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Map<String, GaoEvent> mapByIdList(Collection<String> idList) {
|
public Map<String, GaoEvent> mapByIdList(Collection<String> idList) {
|
||||||
return mapper.selectByIdList(new HashSet<>(idList)).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity()));
|
return mapper.selectByIdList(new HashSet<>(idList)).stream().collect(Collectors.toMap(GaoEvent::getId, Function.identity()));
|
||||||
|
|||||||
+68
-2
@@ -1,10 +1,16 @@
|
|||||||
package com.imyeyu.api.modules.gao.service.implement;
|
package com.imyeyu.api.modules.gao.service.implement;
|
||||||
|
|
||||||
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.service.AttachmentService;
|
||||||
|
import com.imyeyu.api.modules.gao.entity.GaoCustomer;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
import com.imyeyu.api.modules.gao.entity.GaoPointAccount;
|
||||||
|
import com.imyeyu.api.modules.gao.mapper.GaoCustomerMapper;
|
||||||
import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper;
|
import com.imyeyu.api.modules.gao.mapper.GaoPointAccountMapper;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
||||||
|
import com.imyeyu.api.modules.gao.vo.GaoPointAccountPage;
|
||||||
import com.imyeyu.java.bean.timi.TimiException;
|
import com.imyeyu.java.bean.timi.TimiException;
|
||||||
|
import com.imyeyu.spring.bean.PageResult;
|
||||||
import com.imyeyu.spring.mapper.BaseMapper;
|
import com.imyeyu.spring.mapper.BaseMapper;
|
||||||
import com.imyeyu.spring.service.AbstractEntityService;
|
import com.imyeyu.spring.service.AbstractEntityService;
|
||||||
import com.imyeyu.utils.Time;
|
import com.imyeyu.utils.Time;
|
||||||
@@ -12,7 +18,10 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
/// GAO 客户积分账户服务实现
|
/// GAO 客户积分账户服务实现
|
||||||
///
|
///
|
||||||
@@ -23,12 +32,24 @@ import java.util.UUID;
|
|||||||
public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPointAccount, String> implements GaoPointAccountService {
|
public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPointAccount, String> implements GaoPointAccountService {
|
||||||
|
|
||||||
private final GaoPointAccountMapper mapper;
|
private final GaoPointAccountMapper mapper;
|
||||||
|
private final GaoCustomerMapper customerMapper;
|
||||||
|
private final AttachmentService attachmentService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected BaseMapper<GaoPointAccount, String> mapper() {
|
protected BaseMapper<GaoPointAccount, String> mapper() {
|
||||||
return mapper;
|
return mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<GaoPointAccount> pageByQuery(GaoPointAccountPage page) {
|
||||||
|
TimiException.required(page, "not found page");
|
||||||
|
PageResult<GaoPointAccount> result = new PageResult<>();
|
||||||
|
result.setTotal(mapper.countByQuery(page));
|
||||||
|
result.setList(mapper.selectByQuery(page));
|
||||||
|
fillCustomer(result.getList());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GaoPointAccount getByCustomerId(String storeId, String customerId) {
|
public GaoPointAccount getByCustomerId(String storeId, String customerId) {
|
||||||
TimiException.required(storeId, "not found storeId");
|
TimiException.required(storeId, "not found storeId");
|
||||||
@@ -36,6 +57,50 @@ public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPo
|
|||||||
return mapper.selectByStoreIdAndCustomerId(storeId, customerId);
|
return mapper.selectByStoreIdAndCustomerId(storeId, customerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public GaoPointAccount getByIdAndStoreId(String storeId, String id) {
|
||||||
|
TimiException.required(id, "not found accountId");
|
||||||
|
GaoPointAccount account = mapper.selectByIdAndStoreId(storeId, id);
|
||||||
|
if (account != null) {
|
||||||
|
fillCustomer(List.of(account));
|
||||||
|
}
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void deleteByStoreId(String storeId) {
|
||||||
|
TimiException.required(storeId, "not found storeId");
|
||||||
|
GaoPointAccount example = new GaoPointAccount();
|
||||||
|
example.setStoreId(storeId);
|
||||||
|
for (GaoPointAccount account : mapper.selectAllByExample(example)) {
|
||||||
|
super.delete(account.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillCustomer(List<GaoPointAccount> accountList) {
|
||||||
|
List<String> customerIdList = accountList.stream()
|
||||||
|
.map(GaoPointAccount::getCustomerId)
|
||||||
|
.filter(id -> id != null && !id.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.toList();
|
||||||
|
if (customerIdList.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, GaoCustomer> customerMap = customerMapper.selectByIdList(customerIdList).stream()
|
||||||
|
.collect(Collectors.toMap(GaoCustomer::getId, item -> item));
|
||||||
|
Map<String, List<Attachment>> attachmentMap = attachmentService.mapByBizIdList(Attachment.BizType.GAO_CUSTOMER, customerIdList);
|
||||||
|
for (GaoPointAccount account : accountList) {
|
||||||
|
GaoCustomer customer = customerMap.get(account.getCustomerId());
|
||||||
|
if (customer == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
customer.setAttachmentList(attachmentMap.get(customer.getId()));
|
||||||
|
account.setCustomer(customer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public GaoPointAccount getOrCreateForUpdate(String storeId, String customerId) {
|
public GaoPointAccount getOrCreateForUpdate(String storeId, String customerId) {
|
||||||
@@ -50,6 +115,7 @@ public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPo
|
|||||||
account.setCustomerId(customerId);
|
account.setCustomerId(customerId);
|
||||||
account.setBalance(0L);
|
account.setBalance(0L);
|
||||||
account.setTotalEarned(0L);
|
account.setTotalEarned(0L);
|
||||||
|
account.setTotalConsumed(0L);
|
||||||
account.setTotalRevoked(0L);
|
account.setTotalRevoked(0L);
|
||||||
account.setTotalAdjusted(0L);
|
account.setTotalAdjusted(0L);
|
||||||
account.setVersion(0L);
|
account.setVersion(0L);
|
||||||
@@ -64,8 +130,8 @@ public class GaoPointAccountServiceImplement extends AbstractEntityService<GaoPo
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalRevokedDelta, long totalAdjustedDelta) {
|
public int updateBalance(String accountId, long delta, long totalEarnedDelta, long totalConsumedDelta, long totalRevokedDelta, long totalAdjustedDelta) {
|
||||||
TimiException.required(accountId, "not found accountId");
|
TimiException.required(accountId, "not found accountId");
|
||||||
return mapper.updateBalance(accountId, delta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta, Time.now());
|
return mapper.updateBalance(accountId, delta, totalEarnedDelta, totalConsumedDelta, totalRevokedDelta, totalAdjustedDelta, Time.now());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -71,12 +71,13 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public GaoPointLedger earn(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload) {
|
public GaoPointLedger applyRule(GaoEventRecord record, GaoPointRule rule, long pointDelta, JsonNode sourcePayload) {
|
||||||
TimiException.required(record, "not found event record");
|
TimiException.required(record, "not found event record");
|
||||||
TimiException.required(rule, "not found point rule");
|
TimiException.required(rule, "not found point rule");
|
||||||
TimiException.requiredTrue(pointDelta != 0, "pointDelta invalid");
|
TimiException.requiredTrue(pointDelta != 0, "pointDelta invalid");
|
||||||
String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId());
|
String idempotencyKey = "GAO_EVENT_RECORD:%s:RULE:%s:EARN".formatted(record.getId(), rule.getId());
|
||||||
return apply(record.getStoreId(), record.getCustomerId(), GaoPointLedger.ChangeType.EARN, pointDelta, rule.getId(), record.getEventId(), record.getId(), sourcePayload, idempotencyKey, rule.getRemark(), record.getOperatorUserId(), record.getRegisteredAt());
|
GaoPointLedger.ChangeType changeType = pointDelta < 0 ? GaoPointLedger.ChangeType.CONSUME : GaoPointLedger.ChangeType.EARN;
|
||||||
|
return apply(record.getStoreId(), record.getCustomerId(), changeType, pointDelta, rule.getId(), record.getEventId(), record.getId(), sourcePayload, idempotencyKey, rule.getRemark(), record.getOperatorUserId(), record.getRegisteredAt());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@@ -154,6 +155,7 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
long balanceBefore = account.getBalance();
|
long balanceBefore = account.getBalance();
|
||||||
long balanceAfter = Math.addExact(balanceBefore, pointDelta);
|
long balanceAfter = Math.addExact(balanceBefore, pointDelta);
|
||||||
long totalEarnedDelta = changeType == GaoPointLedger.ChangeType.EARN ? pointDelta : 0L;
|
long totalEarnedDelta = changeType == GaoPointLedger.ChangeType.EARN ? pointDelta : 0L;
|
||||||
|
long totalConsumedDelta = changeType == GaoPointLedger.ChangeType.CONSUME ? -pointDelta : 0L;
|
||||||
long totalRevokedDelta = changeType == GaoPointLedger.ChangeType.REVOKE ? -pointDelta : 0L;
|
long totalRevokedDelta = changeType == GaoPointLedger.ChangeType.REVOKE ? -pointDelta : 0L;
|
||||||
long totalAdjustedDelta = changeType == GaoPointLedger.ChangeType.ADJUST ? pointDelta : 0L;
|
long totalAdjustedDelta = changeType == GaoPointLedger.ChangeType.ADJUST ? pointDelta : 0L;
|
||||||
|
|
||||||
@@ -175,7 +177,7 @@ public class GaoPointLedgerServiceImplement extends AbstractEntityService<GaoPoi
|
|||||||
ledger.setOperatorUserId(operatorUserId);
|
ledger.setOperatorUserId(operatorUserId);
|
||||||
ledger.setOccurredAt(occurredAt);
|
ledger.setOccurredAt(occurredAt);
|
||||||
create(ledger);
|
create(ledger);
|
||||||
int updated = accountService.updateBalance(account.getId(), pointDelta, totalEarnedDelta, totalRevokedDelta, totalAdjustedDelta);
|
int updated = accountService.updateBalance(account.getId(), pointDelta, totalEarnedDelta, totalConsumedDelta, totalRevokedDelta, totalAdjustedDelta);
|
||||||
TimiException.requiredTrue(0 < updated, "积分余额不足");
|
TimiException.requiredTrue(0 < updated, "积分余额不足");
|
||||||
logger.setLevel(Logger.Level.INFO);
|
logger.setLevel(Logger.Level.INFO);
|
||||||
logger.setResult(ledger.getId());
|
logger.setResult(ledger.getId());
|
||||||
|
|||||||
+11
@@ -55,6 +55,17 @@ public class GaoPointRuleServiceImplement extends AbstractEntityService<GaoPoint
|
|||||||
return mapper.selectByIdList(new LinkedHashSet<>(idList));
|
return mapper.selectByIdList(new LinkedHashSet<>(idList));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void deleteByStoreId(String storeId) {
|
||||||
|
TimiException.required(storeId, "not found storeId");
|
||||||
|
GaoPointRule example = new GaoPointRule();
|
||||||
|
example.setStoreId(storeId);
|
||||||
|
for (GaoPointRule rule : mapper.selectAllByExample(example)) {
|
||||||
|
delete(rule.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void sort(List<String> idList) {
|
public void sort(List<String> idList) {
|
||||||
|
|||||||
+2
-2
@@ -73,7 +73,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
|||||||
}
|
}
|
||||||
long pointDelta = resolution.pointDelta();
|
long pointDelta = resolution.pointDelta();
|
||||||
if (pointDelta != 0) {
|
if (pointDelta != 0) {
|
||||||
ledgerService.earn(record, rule, pointDelta, resolution.sourcePayload());
|
ledgerService.applyRule(record, rule, pointDelta, resolution.sourcePayload());
|
||||||
pointDeltaTotal = Math.addExact(pointDeltaTotal, pointDelta);
|
pointDeltaTotal = Math.addExact(pointDeltaTotal, pointDelta);
|
||||||
}
|
}
|
||||||
if (Boolean.TRUE.equals(rule.getExclusive())) {
|
if (Boolean.TRUE.equals(rule.getExclusive())) {
|
||||||
@@ -88,7 +88,7 @@ public class GaoPointTriggerServiceImplement implements GaoPointTriggerService {
|
|||||||
@Override
|
@Override
|
||||||
public void revokeByEventRecord(GaoEventRecord record) {
|
public void revokeByEventRecord(GaoEventRecord record) {
|
||||||
TimiException.required(record, "not found event record");
|
TimiException.required(record, "not found event record");
|
||||||
List<GaoPointLedger> originLedgerList = ledgerMapper.selectEarnByEventRecordId(record.getId());
|
List<GaoPointLedger> originLedgerList = ledgerMapper.selectRuleChangeByEventRecordId(record.getId());
|
||||||
if (originLedgerList.isEmpty()) {
|
if (originLedgerList.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
@@ -71,6 +71,16 @@ public class GaoStoreBusinessDayServiceImplement extends AbstractEntityService<G
|
|||||||
rebuildBusinessDayNo(businessDay.getStoreId(), businessDay.getDateValue());
|
rebuildBusinessDayNo(businessDay.getStoreId(), businessDay.getDateValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void deleteByStoreId(String storeId) {
|
||||||
|
TimiException.required(storeId, "not found storeId");
|
||||||
|
GaoStoreBusinessDay example = new GaoStoreBusinessDay();
|
||||||
|
example.setStoreId(storeId);
|
||||||
|
// 门店整体删除后不再需要重算营业日序号
|
||||||
|
mapper.deleteAllByExample(example);
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public GaoStoreBusinessDay resolveOpenBusinessDay(String storeId, Integer dateValue) {
|
public GaoStoreBusinessDay resolveOpenBusinessDay(String storeId, Integer dateValue) {
|
||||||
|
|||||||
+73
@@ -2,9 +2,17 @@ package com.imyeyu.api.modules.gao.service.implement;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
|
||||||
|
import com.imyeyu.api.modules.common.entity.Tag;
|
||||||
|
import com.imyeyu.api.modules.common.service.TagService;
|
||||||
import com.imyeyu.api.modules.gao.entity.GaoStore;
|
import com.imyeyu.api.modules.gao.entity.GaoStore;
|
||||||
import com.imyeyu.api.modules.gao.mapper.GaoStoreMapper;
|
import com.imyeyu.api.modules.gao.mapper.GaoStoreMapper;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoCustomerService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoEventService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoPointAccountService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoPointRuleService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoStoreBusinessDayService;
|
||||||
import com.imyeyu.api.modules.gao.service.GaoStoreService;
|
import com.imyeyu.api.modules.gao.service.GaoStoreService;
|
||||||
|
import com.imyeyu.api.modules.gao.service.GaoUserService;
|
||||||
import com.imyeyu.api.modules.gao.util.StoreXSSFBuilder;
|
import com.imyeyu.api.modules.gao.util.StoreXSSFBuilder;
|
||||||
import com.imyeyu.api.modules.system.entity.Logger;
|
import com.imyeyu.api.modules.system.entity.Logger;
|
||||||
import com.imyeyu.api.modules.system.service.LoggerService;
|
import com.imyeyu.api.modules.system.service.LoggerService;
|
||||||
@@ -17,6 +25,7 @@ import com.imyeyu.spring.mapper.BaseMapper;
|
|||||||
import com.imyeyu.spring.service.AbstractEntityService;
|
import com.imyeyu.spring.service.AbstractEntityService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -41,6 +50,13 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
|
|||||||
|
|
||||||
private final LoggerService loggerService;
|
private final LoggerService loggerService;
|
||||||
private final UserLoginService userLoginService;
|
private final UserLoginService userLoginService;
|
||||||
|
private final GaoCustomerService customerService;
|
||||||
|
private final GaoEventService eventService;
|
||||||
|
private final GaoPointAccountService pointAccountService;
|
||||||
|
private final GaoPointRuleService pointRuleService;
|
||||||
|
private final GaoStoreBusinessDayService businessDayService;
|
||||||
|
private final ObjectProvider<GaoUserService> gaoUserServiceProvider;
|
||||||
|
private final TagService tagService;
|
||||||
|
|
||||||
private final GaoStoreMapper mapper;
|
private final GaoStoreMapper mapper;
|
||||||
private final StoreXSSFBuilder storeXSSFBuilder;
|
private final StoreXSSFBuilder storeXSSFBuilder;
|
||||||
@@ -116,6 +132,14 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
|
|||||||
try {
|
try {
|
||||||
logger.setContent(id);
|
logger.setContent(id);
|
||||||
GaoStore store = get(id);
|
GaoStore store = get(id);
|
||||||
|
TimiException.required(store, "not found store");
|
||||||
|
deleteCustomers(store.getId());
|
||||||
|
deletePointRules(store.getId());
|
||||||
|
deleteEvents(store.getId());
|
||||||
|
deleteBusinessDays(store.getId());
|
||||||
|
deleteGaoUsers(store.getId());
|
||||||
|
deletePointAccounts(store.getId());
|
||||||
|
deleteStoreTags(store.getId());
|
||||||
store.setStatus(GaoStore.Status.DELETED);
|
store.setStatus(GaoStore.Status.DELETED);
|
||||||
super.update(store);
|
super.update(store);
|
||||||
super.delete(id);
|
super.delete(id);
|
||||||
@@ -135,6 +159,55 @@ public class GaoStoreServiceImplement extends AbstractEntityService<GaoStore, St
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 删除门店客户及其登记、积分账户、客户标签和附件
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
private void deleteCustomers(String storeId) {
|
||||||
|
customerService.deleteByStoreId(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除门店积分规则
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
private void deletePointRules(String storeId) {
|
||||||
|
pointRuleService.deleteByStoreId(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除门店登记事件及角色关联
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
private void deleteEvents(String storeId) {
|
||||||
|
eventService.deleteByStoreId(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除门店营业日
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
private void deleteBusinessDays(String storeId) {
|
||||||
|
businessDayService.deleteByStoreId(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除门店用户关系,不删除核心用户
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
private void deleteGaoUsers(String storeId) {
|
||||||
|
gaoUserServiceProvider.getObject().deleteByStoreId(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 清理门店下未被客户删除逻辑覆盖的积分账户
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
private void deletePointAccounts(String storeId) {
|
||||||
|
pointAccountService.deleteByStoreId(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 删除门店自有标签,客户标签应用由客户删除逻辑处理
|
||||||
|
///
|
||||||
|
/// @param storeId 门店 ID
|
||||||
|
private void deleteStoreTags(String storeId) {
|
||||||
|
tagService.deleteByOwner(Tag.OwnerType.GAO_STORE, storeId);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public GaoStore getByUserId(String userId) {
|
public GaoStore getByUserId(String userId) {
|
||||||
TimiException.required(userId, "not found userId");
|
TimiException.required(userId, "not found userId");
|
||||||
|
|||||||
+31
-14
@@ -3,6 +3,8 @@ 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;
|
||||||
@@ -64,11 +66,13 @@ 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;
|
||||||
@@ -98,9 +102,11 @@ 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 adminInviteGlobalManager = roleChecker.hasAny(ModuleCode.CORE, CoreRoleCode.ADMIN.name())
|
boolean coreAdmin = isCoreAdmin();
|
||||||
|
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;
|
||||||
@@ -147,14 +153,21 @@ 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);
|
||||||
GaoUserInviteCache cache = requireValidInvite(code);
|
try {
|
||||||
validateInviteTarget(cache);
|
GaoUserInviteCache cache = requireValidInvite(code);
|
||||||
return new GaoUserInviteValidateResponse(true, cache.getExpireAt());
|
validateInviteTarget(cache);
|
||||||
|
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) {
|
public String register(GaoUserInviteRegisterData data, String smsDetailId, String smsCaptcha) {
|
||||||
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");
|
||||||
@@ -170,6 +183,9 @@ 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))) {
|
||||||
@@ -260,14 +276,10 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
|
|
||||||
private Role requireInvitableRole(String inviterUserId, String roleId, String storeId) {
|
private Role requireInvitableRole(String inviterUserId, String roleId, String storeId) {
|
||||||
Role role = requireValidRole(roleId, storeId);
|
Role role = requireValidRole(roleId, storeId);
|
||||||
List<String> directRoleIdList = userRoleService.listRoleByUserId(ModuleCode.GAO, inviterUserId)
|
boolean adminInviteGlobalManager = isCoreAdmin() && isGlobalManagerRole(role);
|
||||||
|
boolean descendant = adminInviteGlobalManager || authorizationScopeService.listManageableRole(inviterUserId, ModuleCode.GAO)
|
||||||
.stream()
|
.stream()
|
||||||
.map(Role::getId)
|
.anyMatch(item -> role.getId().equals(item.getId()));
|
||||||
.toList();
|
|
||||||
boolean descendant = authorizationScopeService.listManageableRole(inviterUserId, ModuleCode.GAO)
|
|
||||||
.stream()
|
|
||||||
.anyMatch(item -> role.getId().equals(item.getId()))
|
|
||||||
&& !directRoleIdList.contains(role.getId());
|
|
||||||
if (!descendant) {
|
if (!descendant) {
|
||||||
throw new TimiException(TimiCode.PERMISSION_ERROR, "角色不在可邀请范围内");
|
throw new TimiException(TimiCode.PERMISSION_ERROR, "角色不在可邀请范围内");
|
||||||
}
|
}
|
||||||
@@ -302,7 +314,8 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void checkStoreScope(String userId, String storeId) {
|
private void checkStoreScope(String userId, String storeId) {
|
||||||
if (authorizationScopeService.isSystemAdmin(userId) || roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
if (authorizationScopeService.isSystemAdmin(userId) || isCoreAdmin()
|
||||||
|
|| roleChecker.hasAny(ModuleCode.GAO, GaoRoleCode.GLOBAL_MANAGER.name())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
GaoStore userStore = gaoStoreService.getByUserId(userId);
|
GaoStore userStore = gaoStoreService.getByUserId(userId);
|
||||||
@@ -344,6 +357,10 @@ public class GaoUserInviteServiceImplement implements GaoUserInviteService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private TimiException invalidInvite() {
|
private TimiException invalidInvite() {
|
||||||
return new TimiException(TimiCode.ARG_BAD, "邀请码无效或已过期");
|
return new TimiException(TimiCode.ARG_BAD, INVALID_INVITE_MESSAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCoreAdmin() {
|
||||||
|
return roleChecker.hasAny(ModuleCode.CORE, CoreRoleCode.ADMIN.name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+11
@@ -40,6 +40,17 @@ public class GaoUserServiceImplement extends AbstractEntityService<GaoUser, Stri
|
|||||||
return mapper;
|
return mapper;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
|
@Override
|
||||||
|
public void deleteByStoreId(String storeId) {
|
||||||
|
TimiException.required(storeId, "not found storeId");
|
||||||
|
GaoUser example = new GaoUser();
|
||||||
|
example.setStoreId(storeId);
|
||||||
|
for (GaoUser gaoUser : mapper.selectAllByExample(example)) {
|
||||||
|
super.delete(gaoUser.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
@Transactional(TimiServerDBConfig.ROLLBACKER)
|
||||||
@Override
|
@Override
|
||||||
public void create(GaoUser gaoUser) {
|
public void create(GaoUser gaoUser) {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.imyeyu.api.modules.gao.vo;
|
||||||
|
|
||||||
|
import com.imyeyu.java.bean.BasePage;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
|
||||||
|
/// GAO 客户积分账户分页查询请求
|
||||||
|
///
|
||||||
|
/// @author Codex
|
||||||
|
/// @since 2026-08-22
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
public class GaoPointAccountPage extends BasePage {
|
||||||
|
|
||||||
|
/// 门店 ID
|
||||||
|
private String storeId;
|
||||||
|
|
||||||
|
/// 客户编码、姓名、电话或拼音关键词
|
||||||
|
private String keyword;
|
||||||
|
}
|
||||||
@@ -1,20 +1,33 @@
|
|||||||
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
|
||||||
public class GaoUserInviteRegisterRequest extends CaptchaData<GaoUserInviteRegisterData> {
|
@Data
|
||||||
|
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 super.getData();
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,6 +5,8 @@ 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;
|
||||||
@@ -50,6 +52,7 @@ 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;
|
||||||
@@ -133,6 +136,15 @@ 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.e2ins.ms.notify.vo.sms.CaptchaVerifyReq;
|
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.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(CaptchaVerifyReq req);
|
LoginResponse loginBySms(SmsCaptchaVerifyRequest request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据令牌获取用户
|
* 根据令牌获取用户
|
||||||
|
|||||||
+12
-19
@@ -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,7 +16,6 @@ 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;
|
||||||
@@ -30,7 +29,6 @@ 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;
|
||||||
|
|
||||||
@@ -47,10 +45,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;
|
||||||
@@ -104,20 +102,15 @@ public class UserLoginServiceImplement implements UserLoginService, TimiJava {
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoginResponse loginBySms(CaptchaVerifyReq req) {
|
public LoginResponse loginBySms(SmsCaptchaVerifyRequest request) {
|
||||||
try {
|
NotifyDetail detail = smsService.captchaVerify(request.getDetailId(), request.getCaptcha());
|
||||||
NotifyDetail detail = smsNotifyService.captchaVerify(req);
|
// 无报错即成功
|
||||||
// 无报错即成功
|
User user = userService.getByPhoneNo(detail.getSendTo());
|
||||||
User user = userService.getByPhoneNo(detail.getSendTo());
|
if (!user.isValidPhone()) {
|
||||||
if (!user.isValidPhone()) {
|
user.setPhoneNoVerifyAt(Time.now());
|
||||||
user.setPhoneNoVerifyAt(Time.now());
|
userService.update(user);
|
||||||
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,36 +7,58 @@ 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 夜雨
|
||||||
* @author 夜雨
|
/// @since 2025-10-23 17:05
|
||||||
* @since 2025-10-23 17:05
|
|
||||||
*/
|
|
||||||
public class JavaCV {
|
public class JavaCV {
|
||||||
|
|
||||||
public static ByteArrayOutputStream captureThumbnail(InputStream stream, double targetSeconds) throws Exception {
|
/// 从视频流顺序解码并提取指定时间附近的一帧 PNG 图片
|
||||||
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
|
///
|
||||||
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(stream)) {
|
/// 不执行时间定位,避免为了取实时缩略图而把整个视频复制到临时文件
|
||||||
grabber.start();
|
///
|
||||||
long targetMillis = (long) (targetSeconds * 1000);
|
/// @param stream 视频输入流
|
||||||
grabber.setTimestamp(targetMillis);
|
/// @param targetSeconds 目标时间,单位为秒
|
||||||
Frame frame;
|
/// @return PNG 图片字节流
|
||||||
while ((frame = grabber.grabImage()) != null) {
|
/// @throws Exception 视频解析或取帧失败
|
||||||
if (grabber.getTimestamp() >= targetMillis) {
|
public static ByteArrayOutputStream captureThumbnail(InputStream stream, double targetSeconds) throws Exception {
|
||||||
Java2DFrameConverter converter = new Java2DFrameConverter();
|
long targetMicros = toMicros(targetSeconds);
|
||||||
try (converter) {
|
ByteArrayOutputStream firstFrame = new ByteArrayOutputStream();
|
||||||
BufferedImage bi = converter.getBufferedImage(frame);
|
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(stream)) {
|
||||||
if (bi != null) {
|
grabber.start();
|
||||||
ImageIO.write(bi, "png", outStream);
|
try (Java2DFrameConverter converter = new Java2DFrameConverter()) {
|
||||||
break;
|
Frame frame;
|
||||||
}
|
while ((frame = grabber.grabImage()) != null) {
|
||||||
}
|
BufferedImage image = converter.getBufferedImage(frame);
|
||||||
}
|
if (image == null) {
|
||||||
}
|
continue;
|
||||||
}
|
}
|
||||||
return outStream;
|
if (!(0 < firstFrame.size())) {
|
||||||
}
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
UPDATE `gao_point_ledger`
|
||||||
|
SET `change_type` = 'CONSUME'
|
||||||
|
WHERE `change_type` = 'EARN'
|
||||||
|
AND `point_delta` < 0;
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
ALTER TABLE `gao_point_account`
|
||||||
|
ADD COLUMN `total_consumed` BIGINT NOT NULL DEFAULT 0 COMMENT '累计消耗积分' AFTER `total_earned`;
|
||||||
|
|
||||||
|
UPDATE `gao_point_account` account
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT `account_id`, SUM(-`point_delta`) AS `total_consumed`
|
||||||
|
FROM `gao_point_ledger`
|
||||||
|
WHERE `change_type` = 'CONSUME'
|
||||||
|
AND `point_delta` < 0
|
||||||
|
AND `deleted_at` IS NULL
|
||||||
|
GROUP BY `account_id`
|
||||||
|
) ledger ON ledger.`account_id` = account.`id`
|
||||||
|
SET account.`total_consumed` = COALESCE(ledger.`total_consumed`, 0);
|
||||||
@@ -26,33 +26,4 @@
|
|||||||
</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>
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<?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>
|
||||||
@@ -10,11 +10,58 @@
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByIdAndStoreId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointAccount">
|
||||||
|
SELECT *
|
||||||
|
FROM `gao_point_account`
|
||||||
|
WHERE `id` = #{id}
|
||||||
|
<if test="storeId != null and storeId != ''">
|
||||||
|
AND `store_id` = #{storeId}
|
||||||
|
</if>
|
||||||
|
AND `deleted_at` IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<sql id="queryWhere">
|
||||||
|
`a`.`deleted_at` IS NULL
|
||||||
|
AND `c`.`deleted_at` IS NULL
|
||||||
|
<if test="storeId != null and storeId != ''">
|
||||||
|
AND `a`.`store_id` = #{storeId}
|
||||||
|
</if>
|
||||||
|
<if test="keyword != null and keyword != ''">
|
||||||
|
AND (
|
||||||
|
`c`.`code` LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR `c`.`name` LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR `c`.`telephone` LIKE CONCAT('%', #{keyword}, '%')
|
||||||
|
OR `c`.`name_pinyin_full` LIKE CONCAT(#{keyword}, '%')
|
||||||
|
OR `c`.`name_pinyin_initial` LIKE CONCAT(#{keyword}, '%')
|
||||||
|
OR `c`.`name_pinyin_mixed` LIKE CONCAT(#{keyword}, '%')
|
||||||
|
)
|
||||||
|
</if>
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
<select id="countByQuery" resultType="long">
|
||||||
|
SELECT COUNT(1)
|
||||||
|
FROM `gao_point_account` `a`
|
||||||
|
INNER JOIN `gao_customer` `c` ON `c`.`id` = `a`.`customer_id` AND `c`.`store_id` = `a`.`store_id`
|
||||||
|
WHERE
|
||||||
|
<include refid="queryWhere"/>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<select id="selectByQuery" resultType="com.imyeyu.api.modules.gao.entity.GaoPointAccount">
|
||||||
|
SELECT `a`.*
|
||||||
|
FROM `gao_point_account` `a`
|
||||||
|
INNER JOIN `gao_customer` `c` ON `c`.`id` = `a`.`customer_id` AND `c`.`store_id` = `a`.`store_id`
|
||||||
|
WHERE
|
||||||
|
<include refid="queryWhere"/>
|
||||||
|
ORDER BY `a`.`created_at` DESC, `a`.`id` DESC
|
||||||
|
LIMIT #{offset}, #{limit}
|
||||||
|
</select>
|
||||||
|
|
||||||
<insert id="insertIfAbsent">
|
<insert id="insertIfAbsent">
|
||||||
INSERT INTO `gao_point_account` (
|
INSERT INTO `gao_point_account` (
|
||||||
`id`, `store_id`, `customer_id`, `balance`, `total_earned`, `total_revoked`, `total_adjusted`, `version`, `created_at`, `updated_at`, `deleted_at`
|
`id`, `store_id`, `customer_id`, `balance`, `total_earned`, `total_consumed`, `total_revoked`, `total_adjusted`, `version`, `created_at`, `updated_at`, `deleted_at`
|
||||||
) VALUES (
|
) VALUES (
|
||||||
#{account.id}, #{account.storeId}, #{account.customerId}, #{account.balance}, #{account.totalEarned}, #{account.totalRevoked}, #{account.totalAdjusted}, #{account.version}, #{account.createdAt}, #{account.updatedAt}, #{account.deletedAt}
|
#{account.id}, #{account.storeId}, #{account.customerId}, #{account.balance}, #{account.totalEarned}, #{account.totalConsumed}, #{account.totalRevoked}, #{account.totalAdjusted}, #{account.version}, #{account.createdAt}, #{account.updatedAt}, #{account.deletedAt}
|
||||||
)
|
)
|
||||||
ON DUPLICATE KEY UPDATE `id` = `id`
|
ON DUPLICATE KEY UPDATE `id` = `id`
|
||||||
</insert>
|
</insert>
|
||||||
@@ -33,6 +80,7 @@
|
|||||||
SET
|
SET
|
||||||
`balance` = `balance` + #{delta},
|
`balance` = `balance` + #{delta},
|
||||||
`total_earned` = `total_earned` + #{totalEarnedDelta},
|
`total_earned` = `total_earned` + #{totalEarnedDelta},
|
||||||
|
`total_consumed` = `total_consumed` + #{totalConsumedDelta},
|
||||||
`total_revoked` = `total_revoked` + #{totalRevokedDelta},
|
`total_revoked` = `total_revoked` + #{totalRevokedDelta},
|
||||||
`total_adjusted` = `total_adjusted` + #{totalAdjustedDelta},
|
`total_adjusted` = `total_adjusted` + #{totalAdjustedDelta},
|
||||||
`version` = `version` + 1,
|
`version` = `version` + 1,
|
||||||
|
|||||||
@@ -9,11 +9,11 @@
|
|||||||
LIMIT 1
|
LIMIT 1
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectEarnByEventRecordId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
<select id="selectRuleChangeByEventRecordId" resultType="com.imyeyu.api.modules.gao.entity.GaoPointLedger">
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM `gao_point_ledger`
|
FROM `gao_point_ledger`
|
||||||
WHERE `event_record_id` = #{eventRecordId}
|
WHERE `event_record_id` = #{eventRecordId}
|
||||||
AND `change_type` = 'EARN'
|
AND `change_type` IN ('EARN', 'CONSUME')
|
||||||
AND `deleted_at` IS NULL
|
AND `deleted_at` IS NULL
|
||||||
ORDER BY `created_at` ASC, `id` ASC
|
ORDER BY `created_at` ASC, `id` ASC
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
Reference in New Issue
Block a user