Compare commits
262 Commits
da84e1e4b4
...
dev/3.1_re
| Author | SHA1 | Date | |
|---|---|---|---|
| a8c1261c89 | |||
| c35e60dcde | |||
| ad3bc69e5c | |||
| bb682e56fa | |||
| 9a4a5d5504 | |||
| b4354d5975 | |||
| 635d913084 | |||
| 61e8901bb1 | |||
| 1680debd4b | |||
| bd6ba95a25 | |||
| 75efc341be | |||
| 921de43b08 | |||
| c558ebb3d0 | |||
| d20bb27244 | |||
| 6e98f295c5 | |||
| cf02b59722 | |||
| 838a8a13b3 | |||
| c95f3accb9 | |||
| 65cde0b8f5 | |||
| b66877425e | |||
| f6d28fec07 | |||
|
|
f53fca9a09 | ||
|
|
c8dc38575a | ||
|
|
c00d906083 | ||
| 4df3f9cc53 | |||
|
|
b0343be544 | ||
|
|
d33cb9f0bf | ||
|
|
40f2735831 | ||
|
|
d73442d1dd | ||
|
|
c8164cb997 | ||
| 981fc35be4 | |||
| 01d3806d5f | |||
| 107e4e9771 | |||
|
|
716d720782 | ||
|
|
6b5bacc49b | ||
|
|
409bc7b1fd | ||
|
|
ec6a5df8af | ||
|
|
029b96ae99 | ||
|
|
14002e7331 | ||
| 14dfe2806c | |||
| 798c7b0592 | |||
| 9bd10581f4 | |||
| 1f288fe5e3 | |||
| 72602eb245 | |||
| 983d53268d | |||
| f3aeeb3584 | |||
| 5d3692a204 | |||
| f2a074b2f6 | |||
| 6a7a37dcec | |||
|
|
c4d2780f0e | ||
|
|
1da6b7728c | ||
|
|
0faf77899b | ||
|
|
e4940019bf | ||
|
|
0da66ff210 | ||
|
|
5dd862ff79 | ||
|
|
edaec9884d | ||
|
|
76eeb2be53 | ||
|
|
cb6f94d2d4 | ||
|
|
28656c44c8 | ||
|
|
6757a89d04 | ||
|
|
9be1a1e307 | ||
|
|
2168978f61 | ||
|
|
54466b935d | ||
|
|
c970ebe691 | ||
|
|
1c5a3a12b9 | ||
|
|
6e06000083 | ||
|
|
dea2b3be42 | ||
|
|
bcf51aea23 | ||
|
|
0c9d5404c6 | ||
|
|
93429839c0 | ||
|
|
27859c3e28 | ||
|
|
f02c0930a6 | ||
|
|
d57bb83b25 | ||
| 731e34f133 | |||
| 75eca8d6ba | |||
| 3e53401f76 | |||
|
|
b6a068ebcd | ||
|
|
dc291ea086 | ||
|
|
2e846e671a | ||
|
|
a5093311f9 | ||
|
|
aed338a6d7 | ||
|
|
8bdb49d25c | ||
|
|
5d53a8cd42 | ||
|
|
61b7f3072f | ||
|
|
a1f489f3a1 | ||
|
|
fc3fd877a8 | ||
|
|
fc72d2c430 | ||
|
|
1ac01dd090 | ||
|
|
3bbdf7c672 | ||
|
|
0646484fba | ||
|
|
96b8613741 | ||
|
|
cf30226a51 | ||
|
|
3c15a3ff68 | ||
|
|
0c904be227 | ||
|
|
7759b56123 | ||
|
|
d5bfaa8822 | ||
| 967c0cbc01 | |||
| 417e34b41a | |||
|
|
d51aa84647 | ||
|
|
5895bc6ab6 | ||
|
|
3301869f20 | ||
|
|
1ec42f4ad5 | ||
| cc506ff7e9 | |||
|
|
f2d43f06f4 | ||
|
|
9251df49f8 | ||
| 430156f4e8 | |||
|
|
d1123aedcc | ||
| 8c007077a3 | |||
|
|
d63b4b4e63 | ||
|
|
b826f0bf39 | ||
|
|
1decd8e258 | ||
|
|
1286e84488 | ||
| a252fdf7f9 | |||
| 807d802178 | |||
| 53f1b548be | |||
| 45dd78032a | |||
| c160da5132 | |||
| b23faeeee2 | |||
| 67789abca4 | |||
| 1c78d66aab | |||
| 528bc69923 | |||
|
|
22880d128d | ||
| 9c56a102cc | |||
| 2f59fe074f | |||
| 9c61b1c8fe | |||
| e30fdf7401 | |||
| ba2d10afbc | |||
| 6146112d04 | |||
| 412550df27 | |||
| 497421e7fe | |||
| 891527426c | |||
|
|
3e334d7956 | ||
|
|
8f0d0953b2 | ||
| f5c3621a5d | |||
|
|
9a1a0045e0 | ||
| 6223c8e994 | |||
| 67bbee49fd | |||
| ad62ceb32a | |||
| 082afe9e94 | |||
| 49288c3a31 | |||
| 81624e36db | |||
| a526b122d1 | |||
|
|
d882b2e817 | ||
|
|
ebf6427d42 | ||
| 77fe03d361 | |||
| 7a44d67dbf | |||
| 55ce2c6c7e | |||
| a426caaca3 | |||
| 7cb7ce2836 | |||
| 8e075f1da4 | |||
|
|
0f0fde2a3e | ||
|
|
8c6389a1f6 | ||
| 652f82b6a4 | |||
| 7ca2528dcf | |||
|
|
a7800913d2 | ||
|
|
1eaec64ff4 | ||
| e603952332 | |||
| 2bc8b8ef96 | |||
| 0ce968b919 | |||
|
|
dfc9ae4db2 | ||
|
|
a3505c6d95 | ||
|
|
6db0afd515 | ||
|
|
b1e6183dd1 | ||
| 30d08356c0 | |||
| 64cc29f456 | |||
|
|
2b3e12a11c | ||
|
|
d4a4724f61 | ||
|
|
ba6e2bd24c | ||
|
|
a38895b028 | ||
|
|
69a95e66ca | ||
|
|
40518cab37 | ||
|
|
46d61cb73f | ||
| 08f20fd1fe | |||
| d7edc166b3 | |||
|
|
79ad02f66b | ||
|
|
5e261b55c7 | ||
|
|
bc92fcbaf4 | ||
|
|
c6aec917c2 | ||
| 6bc500e78f | |||
| 4c43b98c02 | |||
|
|
5bae785a9f | ||
|
|
7b619aa4cb | ||
| c93ad6daa9 | |||
| 0047be7a03 | |||
| 4ef209cfd4 | |||
| a19751b4b7 | |||
|
|
bb0e5a4263 | ||
|
|
9e9df5367d | ||
| ba8a2c52de | |||
| 39d8c7efcf | |||
|
|
401910901a | ||
|
|
3f5ce6e0e7 | ||
| 0787025151 | |||
| 08b26872ff | |||
| 5bbf1326bb | |||
|
|
c5e27cd220 | ||
|
|
112e9c3bc9 | ||
|
|
ce95cb5080 | ||
|
|
71211bfbc3 | ||
| 72ad977dcb | |||
|
|
6400e79929 | ||
|
|
dd8c72f7d7 | ||
| 13151b65f5 | |||
| 9f523d5953 | |||
| 4879cfeb60 | |||
| 9e252b16ef | |||
| e64add14af | |||
| 3beb27e491 | |||
| 501032ef17 | |||
|
|
cb25bdd2e0 | ||
|
|
7a9fb0213b | ||
| cd767dce6f | |||
| bf95b85841 | |||
| 9e58bd9e7d | |||
| d0ec5c5c26 | |||
| ab8aa5ea5c | |||
| aefcd2fdb0 | |||
| e74eab1070 | |||
|
|
34da437a26 | ||
|
|
f84935d0bd | ||
| 35edaa0f27 | |||
| f43099e19e | |||
| 8079877734 | |||
| ef686e38ac | |||
| 100019d2a4 | |||
|
|
12af237d76 | ||
|
|
44dbbb2a4b | ||
| 9f42e153a4 | |||
| 4fa70a1c90 | |||
| dfb34916e7 | |||
| 9f7987306c | |||
|
|
d3fc70fbf2 | ||
|
|
17645d17e5 | ||
|
|
258eea5277 | ||
|
|
bb1d3bd359 | ||
|
|
6a8c87ed95 | ||
|
|
eb3826927d | ||
|
|
7720c8c771 | ||
|
|
b459148d58 | ||
|
|
eadda18d1e | ||
|
|
d403df51ec | ||
| 6903b98b60 | |||
| 81bf65515c | |||
| 8e984eb283 | |||
|
|
afc6041570 | ||
| bce368248c | |||
| ca7121dcda | |||
|
|
9a206f9979 | ||
|
|
eb7a46c7e8 | ||
|
|
95ef68a784 | ||
|
|
6429288fd9 | ||
| c5af194876 | |||
| 6cd42b799a | |||
| 6e1ed7f9b8 | |||
| b7be16738b | |||
| 6da5e91ec1 | |||
| a710fdd432 | |||
| d598f53d3c | |||
| 96170a9956 | |||
| 8205fb5290 | |||
| fcbe4762b3 | |||
| e750adcc94 |
@@ -99,6 +99,8 @@ jobs:
|
||||
volumes:
|
||||
# 数据挂载
|
||||
- ./log:/log
|
||||
- ./temp:/temp
|
||||
- ./uploads:/temp/uploads
|
||||
ports:
|
||||
- '10090:5567'
|
||||
restart: always
|
||||
@@ -133,8 +135,6 @@ jobs:
|
||||
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
echo "停止旧容器..."
|
||||
docker compose down || true
|
||||
echo "清理Docker资源..."
|
||||
docker system prune -f
|
||||
echo "构建镜像..."
|
||||
docker compose build --no-cache
|
||||
echo "启动服务..."
|
||||
|
||||
7
pom.xml
7
pom.xml
@@ -240,7 +240,7 @@
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
<version>26.2.0</version>
|
||||
<version>32.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- aws s3 -->
|
||||
@@ -427,6 +427,11 @@
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
</dependency>
|
||||
<!-- AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -636,26 +636,26 @@ public class GenerateConsumer {
|
||||
public void getPoseTransformationResult(Message msg, Channel channel) {
|
||||
processPoseTransformResult(msg, channel);
|
||||
}
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.designBatch}")
|
||||
@RabbitHandler
|
||||
public void getDesignBatchResult(Message msg, Channel channel) {
|
||||
processDesignBatchResult(msg, channel);
|
||||
}
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.toProductImageBatch}")
|
||||
@RabbitHandler
|
||||
public void getToProductImageBatchResult(Message msg, Channel channel) {
|
||||
processToProductImageBatchResult(msg, channel);
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.relightBatch}")
|
||||
@RabbitHandler
|
||||
public void getRelightBatchResult(Message msg, Channel channel) {
|
||||
processRelightBatchResult(msg, channel);
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.poseTransformBatch}")
|
||||
@RabbitHandler
|
||||
public void getPoseTransformBatchResult(Message msg, Channel channel) {
|
||||
processPoseTransformBatchResult(msg, channel);
|
||||
}
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.designBatch}")
|
||||
// @RabbitHandler
|
||||
// public void getDesignBatchResult(Message msg, Channel channel) {
|
||||
// processDesignBatchResult(msg, channel);
|
||||
// }
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.toProductImageBatch}")
|
||||
// @RabbitHandler
|
||||
// public void getToProductImageBatchResult(Message msg, Channel channel) {
|
||||
// processToProductImageBatchResult(msg, channel);
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.relightBatch}")
|
||||
// @RabbitHandler
|
||||
// public void getRelightBatchResult(Message msg, Channel channel) {
|
||||
// processRelightBatchResult(msg, channel);
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.poseTransformBatch}")
|
||||
// @RabbitHandler
|
||||
// public void getPoseTransformBatchResult(Message msg, Channel channel) {
|
||||
// processPoseTransformBatchResult(msg, channel);
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -28,6 +28,11 @@ public class MQPublisher {
|
||||
amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getSr(), mm);
|
||||
}
|
||||
|
||||
public void sendGenerateResultMessage(String mm) {
|
||||
log.info("send generate result message: {}", mm);
|
||||
amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getGenerateResult(), mm);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mailParams 含有的字段
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.ai.da.common.aspect;
|
||||
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Controller日志切面
|
||||
* 记录所有Controller接口的请求参数和用户信息
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class ControllerLoggingAspect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ControllerLoggingAspect.class);
|
||||
|
||||
/**
|
||||
* 定义切点:所有Controller方法
|
||||
*/
|
||||
@Pointcut("execution(* com.ai.da.controller..*(..))")
|
||||
public void controllerMethods() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller方法执行前记录日志
|
||||
*/
|
||||
// @Before("controllerMethods()")
|
||||
public void logControllerBefore(JoinPoint joinPoint) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
|
||||
// 获取当前用户ID
|
||||
Long userId = null;
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
if (authPrincipalVo != null) {
|
||||
userId = authPrincipalVo.getId();
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
Map<String, Object> params = getRequestParams(joinPoint, request);
|
||||
|
||||
logger.info("=== 请求开始 ===");
|
||||
logger.info("用户ID: {}", userId);
|
||||
logger.info("请求URL: {}", request.getRequestURL().toString());
|
||||
logger.info("请求方法: {}", request.getMethod());
|
||||
logger.info("请求IP: {}", getClientIpAddress(request));
|
||||
logger.info("调用方法: {}.{}", joinPoint.getSignature().getDeclaringType().getSimpleName(), joinPoint.getSignature().getName());
|
||||
logger.info("请求参数: {}", params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求参数
|
||||
*/
|
||||
private Map<String, Object> getRequestParams(JoinPoint joinPoint, HttpServletRequest request) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
|
||||
// 1. 获取Query String参数
|
||||
String queryString = request.getQueryString();
|
||||
if (queryString != null && !queryString.isEmpty()) {
|
||||
params.put("queryString", queryString);
|
||||
}
|
||||
|
||||
// 2. 获取方法参数(包含 @PathVariable, @RequestParam, @RequestBody 等)
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
if (args != null && args.length > 0) {
|
||||
Map<String, Object> methodParams = new HashMap<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
Object arg = args[i];
|
||||
// 过滤掉不可序列化的参数
|
||||
if (arg != null) {
|
||||
if (isIgnorable(arg)) {
|
||||
// 对于可忽略的类型,记录类型名
|
||||
methodParams.put("arg" + i, "[" + arg.getClass().getSimpleName() + "]");
|
||||
} else {
|
||||
try {
|
||||
methodParams.put("arg" + i, arg);
|
||||
} catch (Exception e) {
|
||||
methodParams.put("arg" + i, arg.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!methodParams.isEmpty()) {
|
||||
params.put("methodParams", methodParams);
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要过滤的参数类型
|
||||
*/
|
||||
private boolean isIgnorable(Object obj) {
|
||||
return obj instanceof HttpServletRequest
|
||||
|| obj instanceof HttpServletResponse
|
||||
|| obj instanceof MultipartFile
|
||||
|| obj instanceof MultipartFile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller方法抛出异常时记录日志
|
||||
*/
|
||||
@AfterThrowing(pointcut = "controllerMethods()", throwing = "exception")
|
||||
public void logControllerAfterThrowing(JoinPoint joinPoint, Throwable exception) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
|
||||
Long userId = null;
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
if (authPrincipalVo != null) {
|
||||
userId = authPrincipalVo.getId();
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
params = getRequestParams(joinPoint, request);
|
||||
}
|
||||
|
||||
logger.error("=== 请求异常 ===");
|
||||
logger.error("用户ID: {}", userId);
|
||||
logger.error("调用方法: {}.{}", joinPoint.getSignature().getDeclaringType().getSimpleName(), joinPoint.getSignature().getName());
|
||||
logger.error("请求参数: {}", params);
|
||||
logger.error("异常信息: ", exception);
|
||||
logger.error("=== 异常结束 ===");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP地址
|
||||
*/
|
||||
private String getClientIpAddress(HttpServletRequest request) {
|
||||
String xForwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
|
||||
return xForwardedFor.split(",")[0];
|
||||
}
|
||||
|
||||
String xRealIp = request.getHeader("X-Real-IP");
|
||||
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
|
||||
return xRealIp;
|
||||
}
|
||||
|
||||
String proxyClientIp = request.getHeader("Proxy-Client-IP");
|
||||
if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) {
|
||||
return proxyClientIp;
|
||||
}
|
||||
|
||||
String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
|
||||
if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
|
||||
return wlProxyClientIp;
|
||||
}
|
||||
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ public class MyTaskScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
public void sendTrialOrderExcelToManagements() {
|
||||
// 获取前一天日期
|
||||
LocalDate yesterday = LocalDate.now().minusDays(1);
|
||||
|
||||
@@ -13,8 +13,9 @@ public class CommonConstant {
|
||||
public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60;
|
||||
// 单位 秒 一天过期 in redis
|
||||
public static final Long GENERATE_RESULT_EXPIRE_TIME = 24 * 60 * 60L;
|
||||
// 单位 秒 7天过期
|
||||
public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L;
|
||||
// 单位 秒 7天过期 (todo 测试状态下 3小时过期)
|
||||
// public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L;
|
||||
public static final Long REDIS_SET_EXPIRE_TIME = 3 * 60 * 60L;
|
||||
|
||||
public static class Numbers{
|
||||
public static final Integer NUMBER_10 = 10;
|
||||
@@ -23,6 +24,7 @@ public class CommonConstant {
|
||||
}
|
||||
|
||||
public static final String GENERATE_PATH = "/api/generate_image";
|
||||
public static final String GENERATE_PATH_FLUX2_KLEIN = "/api/generate_image_flux2_klein";
|
||||
|
||||
public static final String GENERATE_SINGLE_LOGO = "/api/generate_single_logo";
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ public class ModelConstants {
|
||||
|
||||
// 模型名称常量
|
||||
public static final String PRINTBOARD_ADVANCED_T2I = "qwen-image";
|
||||
public static final String MOODBOARD_ADVANCED = "doubao-seedream-3-0-t2i-250415";
|
||||
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-3-0-t2i-250415";
|
||||
public static final String PRINTBOARD_HIGH_I2I = "doubao-seededit-3-0-i2i-250628";
|
||||
public static final String MOODBOARD_ADVANCED = "doubao-seedream-4-5-251128";
|
||||
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-4-0-250828-high";
|
||||
public static final String PRINTBOARD_HIGH_I2I = "doubao-seedream-4-0-250828-fast";
|
||||
public static final String PRINTBOARD_ADVANCED_I2I = "doubao-seedream-4-0-250828";
|
||||
public static final String IMAGEN_MODEL = "imagen-4.0-generate-001";
|
||||
public static final String NANO_BANANA = "gemini-2.5-flash-image";
|
||||
|
||||
@@ -33,7 +33,11 @@ public enum AuthenticationOperationTypeEnum {
|
||||
*/
|
||||
UPDATE_USERINFO,
|
||||
|
||||
REGISTER;
|
||||
REGISTER,
|
||||
/**
|
||||
* Global_Award 活动验证
|
||||
*/
|
||||
GLOBAL_AWARD;
|
||||
|
||||
public static AuthenticationOperationTypeEnum of(String name) {
|
||||
return Stream.of(AuthenticationOperationTypeEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null);
|
||||
|
||||
@@ -30,7 +30,7 @@ public enum CreditsEventsEnum {
|
||||
INIT_QUARTERLY("init_quarterly", "12000"),
|
||||
INIT_MONTHLY_EDU("init_monthly_edu", "3500"),
|
||||
INIT_TRIAL("init_trial", "100"),
|
||||
INIT_WEEKLY("init_weekly","6000"),
|
||||
INIT_DAILY("init_daily","100"),
|
||||
RESET_YEAR_CREDITS("reset_year_credits","6000"),
|
||||
|
||||
// SUPER_RESOLUTION("Super Resolution","30"),
|
||||
|
||||
@@ -34,6 +34,11 @@ public enum OrderStatusEnum {
|
||||
* 已退款
|
||||
*/
|
||||
REFUND_SUCCESS("已退款"),
|
||||
|
||||
/**
|
||||
* 已部分退款
|
||||
*/
|
||||
PARTIAL_REFUND_SUCCESS("已部分退款"),
|
||||
/**
|
||||
* 退款异常
|
||||
*/
|
||||
|
||||
19
src/main/java/com/ai/da/common/enums/PaymentInfoType.java
Normal file
19
src/main/java/com/ai/da/common/enums/PaymentInfoType.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.ai.da.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PaymentInfoType {
|
||||
|
||||
NEW("new"),
|
||||
|
||||
RENEWAL("renewal"),
|
||||
|
||||
CREDIT("credit"),
|
||||
|
||||
MANUAL("manual");
|
||||
|
||||
private final String type;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.ai.da.common.enums;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProductEnum {
|
||||
@@ -23,11 +25,27 @@ public enum ProductEnum {
|
||||
;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
* 显示名称(用于与 orderInfo.title 匹配)
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
private final Long price;
|
||||
|
||||
private final Long credits;
|
||||
|
||||
/**
|
||||
* 根据显示名称获取枚举
|
||||
*
|
||||
* @param name 显示名称(与 orderInfo.title 匹配)
|
||||
* @return 对应的枚举,未找到返回 null
|
||||
*/
|
||||
public static ProductEnum getByName(String name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(values())
|
||||
.filter(pe -> pe.name.equals(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.common.security.config.SecurityProperties;
|
||||
import com.ai.da.common.security.jwt.JWTTokenHelper;
|
||||
import com.ai.da.common.utils.LocalCacheUtils;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
import com.ai.da.common.utils.MultiReadHttpServletRequest;
|
||||
import com.ai.da.common.utils.MultiReadHttpServletResponse;
|
||||
import com.ai.da.common.utils.RequestInfoUtil;
|
||||
@@ -40,6 +41,8 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
private JWTTokenHelper jwtTokenHelper;
|
||||
@Resource
|
||||
private SecurityProperties properties;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
private static final List<String> FILTER_URL =
|
||||
Arrays.asList("/favicon.ico", "/doc.html", "/swagger-ui.html",
|
||||
@@ -56,7 +59,9 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
"/api/account/designWorksRegister","/api/account/questionnaire","/api/stripe/trade/notify",
|
||||
"/notification","/api/account/activateNewEmail","/api/third/party/auth/google_callback","/api/third/party/parseGoogleCredential","/api/third/party/receiveDesignResults","/api/third/party/parseWeChatCode","/api/third/party/receiveDesignParams"
|
||||
, "/api/account/schoolLogin", "/api/account/enterpriseLogin", "/api/account/organizationNameSearch",
|
||||
"/api/llm/stream"
|
||||
"/api/llm/stream",
|
||||
//GlobalAwardController
|
||||
"/api/global-award"
|
||||
);
|
||||
|
||||
@Override
|
||||
@@ -132,12 +137,19 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
UserContext.delete();
|
||||
//存取用户信息到缓存
|
||||
UserContext.setUserHolder(principal);
|
||||
//校验token
|
||||
String cacheToken = LocalCacheUtils.getTokenCache(String.valueOf(principal.getId()));
|
||||
// 校验 token:先查本地缓存,再查 Redis,保证服务重启后仍然有效
|
||||
String userIdStr = String.valueOf(principal.getId());
|
||||
String cacheToken = LocalCacheUtils.getTokenCache(userIdStr);
|
||||
|
||||
if(StringUtils.isEmpty(cacheToken)){
|
||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
||||
throw new TokenMissingOrExpiredException("TOKEN已过期,请重新登录!(local cache empty)");
|
||||
if (StringUtils.isEmpty(cacheToken)) {
|
||||
// 本地缓存为空时,尝试从 Redis 读取
|
||||
cacheToken = redisUtil.getLoginToken(principal.getId());
|
||||
if (StringUtils.isEmpty(cacheToken)) {
|
||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
||||
throw new TokenMissingOrExpiredException("TOKEN已过期,请重新登录!(cache & redis empty)");
|
||||
}
|
||||
// 将 Redis 中的 token 回填到本地缓存,减少后续 Redis 访问
|
||||
LocalCacheUtils.setTokenCache(userIdStr, cacheToken);
|
||||
}
|
||||
if(!cacheToken.equals(jwtToken) ){
|
||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.ai.da.common.task;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
import com.ai.da.mapper.primary.entity.Account;
|
||||
import com.ai.da.service.AccountService;
|
||||
import com.ai.da.service.SubscriptionPlanService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -18,6 +19,8 @@ public class AccountTask {
|
||||
private AccountService accountService;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
@Resource
|
||||
private SubscriptionPlanService subscriptionPlanService;
|
||||
|
||||
/**
|
||||
* 每周日晚上刷新 年付用户、月付用户的积分
|
||||
@@ -31,14 +34,14 @@ public class AccountTask {
|
||||
accountService.refreshCreditsMonthly();
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
public void getPaidUser() {
|
||||
// 获取code-create 表中 指定日期之后 订单状态为wc-processing的订单
|
||||
accountService.extendValidityForCC();
|
||||
}
|
||||
|
||||
// 每天凌晨0点执行一次
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
// 每天凌晨0点执行一次 目前已没有角色类型为4的用户
|
||||
/*@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void cancelActivityBenefits() {
|
||||
// 1、查询当前所有参与了活动且过期的用户
|
||||
List<Account> accountList = accountService.getExpiredUserBySystemUser(4);
|
||||
@@ -48,7 +51,7 @@ public class AccountTask {
|
||||
log.info("参与活动的用户{} : {} 于 {} 账号有效期到期,置为游客", account.getId(), account.getUserEmail(), account.getValidEndTime());
|
||||
accountService.toVisitor(account);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// 每天检测正式用户到期情况,每天凌晨0点执行
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
@@ -86,4 +89,14 @@ public class AccountTask {
|
||||
public void checkEduAdminExpireStatus() {
|
||||
accountService.checkEduAdminExpireStatus();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 5 0 * * ?")
|
||||
public void activeSubscriptionPlan() {
|
||||
subscriptionPlanService.activeSubscriptionPlan(null);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
public void expireSubscription() {
|
||||
subscriptionPlanService.expireSubscription();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ public class PaymentTask {
|
||||
@Resource
|
||||
private PayPalCheckoutService payPalCheckoutService;
|
||||
|
||||
// @Scheduled(cron = "0/30 * * * * ?")
|
||||
// @Scheduled(cron = "0/30 * * * * ?")
|
||||
public void orderConfirmForPaypal() throws SerializeException {
|
||||
|
||||
// log.info("PayPal orderConfirm 被执行......");
|
||||
@@ -97,7 +97,7 @@ public class PaymentTask {
|
||||
//
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
public void updateAffiliateInfoWithPayment(){
|
||||
// log.info("佣金计算定时器");
|
||||
affiliateService.updateAffiliateInfoWithPayment();
|
||||
@@ -109,7 +109,7 @@ public class PaymentTask {
|
||||
affiliateService.syncLinkViewCountToDB();
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
||||
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
||||
public void commissionSummaryReminder(){
|
||||
// 每个月末的最后一天的早上八点执行
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SubscriptionReminderTask {
|
||||
REMINDER_DAYS_CONFIG.put("year", 14);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 9 * * ?")
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
public void subscriptionReminder() {
|
||||
// 获取所有需要通知的订阅
|
||||
List<SubscriptionInfo> subscriptionInfos = getDueSubscriptions();
|
||||
@@ -97,7 +97,7 @@ public class SubscriptionReminderTask {
|
||||
return subscriptionInfoMapper.selectList(qw);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 9 * * ?")
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
public void trialReminder() {
|
||||
// 今天的 00:00:00 和 23:59:59
|
||||
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||
|
||||
@@ -41,6 +41,13 @@ public class MinioUtil {
|
||||
@Autowired
|
||||
private MinioClient minioClient;
|
||||
|
||||
/**
|
||||
* 获取MinIO客户端实例
|
||||
*/
|
||||
public MinioClient getMinioClient() {
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* description: 判断bucket是否存在,不存在则创建
|
||||
*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,17 +17,38 @@ import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
|
||||
import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
|
||||
import com.tencentcloudapi.ses.v20201002.models.Template;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 邮件发送类
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SendEmailUtil {
|
||||
@Value("${merchant.email:}")
|
||||
private String merchantEmailInstance;
|
||||
@Value("${developer.email: xupei@code-create.com.hk}")
|
||||
private String developerEmailInstance;
|
||||
|
||||
private static String merchantEmail;
|
||||
private static String developerEmail;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
merchantEmail = merchantEmailInstance;
|
||||
developerEmail = developerEmailInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 秘钥id
|
||||
*/
|
||||
@@ -765,9 +786,7 @@ public class SendEmailUtil {
|
||||
|
||||
public static boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||
try {
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
Credential cred = new Credential(SECRET_ID, SECRET_KEy);
|
||||
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
@@ -966,9 +985,7 @@ public class SendEmailUtil {
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
SendEmailRequest req = new SendEmailRequest();
|
||||
req.setFromEmailAddress(SEND_ADDRESS);
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developerEmail = "xupei@code-create.com.hk";
|
||||
req.setDestination(new String[]{/*merchantEmail,*/ developerEmail});
|
||||
req.setDestination(buildMerchantReceiverEmail());
|
||||
Template template = new Template();
|
||||
req.setSubject("New Credit Purchase Order");
|
||||
template.setTemplateID(CREDITS_PURCHASE_MERCHANT);
|
||||
@@ -1024,7 +1041,7 @@ public class SendEmailUtil {
|
||||
log.info("邮件发送结果res###{}", SendEmailResponse.toJsonString(resp));
|
||||
} catch (TencentCloudSDKException e) {
|
||||
log.info("邮件发送失败###{}", e.toString());
|
||||
throw new BusinessException("failed.to.send.mail");
|
||||
// throw new BusinessException("failed.to.send.mail");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1076,4 +1093,25 @@ public class SendEmailUtil {
|
||||
|
||||
}
|
||||
|
||||
public static String[] buildMerchantReceiverEmail() {
|
||||
List<String> emails = new ArrayList<>();
|
||||
if (!StringUtils.isEmpty(merchantEmail)) {
|
||||
for (String e : merchantEmail.split(",")) {
|
||||
String trimmed = e.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
emails.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!StringUtils.isEmpty(developerEmail)) {
|
||||
for (String e : developerEmail.split(",")) {
|
||||
String trimmed = e.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
emails.add(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
return emails.toArray(new String[0]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -114,8 +114,8 @@ public class SendRequestUtil {
|
||||
|
||||
public String sendFluxPost(String url, String requestBodyStr) {
|
||||
// 尝试两个API key
|
||||
String[] apiKeys = {"d447a0ac-2291-4f1c-9a36-f7614c385989",
|
||||
"84e8f5d5-b0b3-49aa-b244-ab7ba27e7ae7"};
|
||||
String[] apiKeys = {"84e8f5d5-b0b3-49aa-b244-ab7ba27e7ae7",
|
||||
"d447a0ac-2291-4f1c-9a36-f7614c385989"};
|
||||
boolean[] notified = {false, false}; // 记录是否已发送过不足提醒
|
||||
|
||||
for (int i = 0; i < apiKeys.length; i++) {
|
||||
@@ -140,8 +140,8 @@ public class SendRequestUtil {
|
||||
if (status == 402 || status == 403) {
|
||||
if (!notified[i]) {
|
||||
SendEmailUtil.commonExceptionReminder(
|
||||
"Flux账户积分不足,flux生成任务失败",
|
||||
new String[]{"xupei3360@163.com, fangjianliao@aidlab.hk, investigation@aidlab.hk"}
|
||||
"Flux账户积分不足,flux生成任务失败。(key:)" + apiKeys[i],
|
||||
new String[]{"xupei3360@163.com", "fangjianliao@aidlab.hk", "investigation@aidlab.hk"}
|
||||
);
|
||||
notified[i] = true;
|
||||
}
|
||||
|
||||
@@ -15,16 +15,16 @@ import com.ai.da.model.vo.PersonalHomepageVO;
|
||||
import com.ai.da.service.AccountService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -139,21 +139,21 @@ public class AccountController {
|
||||
@Operation(summary = "aws状态检测")
|
||||
@GetMapping("/healthy")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public Response<Map<String,Integer>> checkStatus(){
|
||||
Map<String,Integer> returnMap = new HashMap<>();
|
||||
returnMap.put("code",200);
|
||||
public Response<Map<String, Integer>> checkStatus() {
|
||||
Map<String, Integer> returnMap = new HashMap<>();
|
||||
returnMap.put("code", 200);
|
||||
return Response.success(returnMap);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询账号到期时间")
|
||||
@PostMapping("/getExpiredTime")
|
||||
public Response<Long> getExpiredTime(){
|
||||
public Response<Long> getExpiredTime() {
|
||||
return Response.success(accountService.getExpiredTime());
|
||||
}
|
||||
|
||||
@Operation(summary = "免密登录")
|
||||
@PostMapping("/noLoginRequired")
|
||||
public Response<AccountLoginVO> noLoginRequired(@RequestBody NoLoginRequiredDTO noLoginRequiredDTO, HttpServletRequest request){
|
||||
public Response<AccountLoginVO> noLoginRequired(@RequestBody NoLoginRequiredDTO noLoginRequiredDTO, HttpServletRequest request) {
|
||||
return Response.success(accountService.noLoginRequired(noLoginRequiredDTO, request));
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ public class AccountController {
|
||||
|
||||
/**
|
||||
* 参与活动 获取福利
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
/* @Operation(summary = "参与活动 获取福利")
|
||||
@@ -201,7 +202,7 @@ public class AccountController {
|
||||
|
||||
@Operation(summary = "将用户账号过期时间设置为过期当天的23:59:59")
|
||||
@GetMapping("/setUserValidToDayEnd")
|
||||
public Response<List<Long>> setUserValidToDayEnd(){
|
||||
public Response<List<Long>> setUserValidToDayEnd() {
|
||||
return Response.success(accountService.setUserValidToDayEnd());
|
||||
}
|
||||
|
||||
@@ -223,19 +224,19 @@ public class AccountController {
|
||||
|
||||
@Operation(summary = "获取个人主页信息")
|
||||
@GetMapping("/personalHomepage")
|
||||
public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id){
|
||||
public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id) {
|
||||
return Response.success(accountService.getPersonalHomepage(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "getUsernameModifyTimes")
|
||||
@GetMapping("/getNicknameModifyTimes")
|
||||
public Response<Long> getNicknameModifyTimes(){
|
||||
public Response<Long> getNicknameModifyTimes() {
|
||||
return Response.success(accountService.getNicknameModifyTimes());
|
||||
}
|
||||
|
||||
@Operation(summary = "editUserName")
|
||||
@GetMapping("/editUserName")
|
||||
public Response<String> editUserName(@RequestParam("newUserName") String newUserName){
|
||||
public Response<String> editUserName(@RequestParam("newUserName") String newUserName) {
|
||||
accountService.editUserName(newUserName);
|
||||
return Response.success("success");
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ConvenientInquiryController {
|
||||
@GetMapping("/recentNewUserChart")
|
||||
public Response<Map<String, Object>> recentNewUserChart(@Parameter(description = "startTime") @RequestParam @Nullable String startTime,
|
||||
@Parameter(description = "endTime") @RequestParam @Nullable String endTime,
|
||||
@Parameter(description = "userType") @RequestParam Integer userType) {
|
||||
@Parameter(description = "userType") @RequestParam @Nullable Integer userType) {
|
||||
return Response.success(convenientInquiryService.recentNewUserChart(startTime, endTime, userType));
|
||||
}
|
||||
|
||||
@@ -179,8 +179,10 @@ public class ConvenientInquiryController {
|
||||
|
||||
@Operation(summary = "获取所有用户id")
|
||||
@GetMapping("/getAllUserId")
|
||||
public Response<List<Map<String, Object>>> getAllUsrIdList() {
|
||||
return Response.success(convenientInquiryService.getAllUserIdList());
|
||||
public Response<IPage<Map<String, Object>>> getAllUserIdList(@Parameter(description = "page") @RequestParam Integer page,
|
||||
@Parameter(description = "size") @RequestParam Integer size,
|
||||
@Parameter(description = "email 模糊查询") @RequestParam(required = false) String email) {
|
||||
return Response.success(convenientInquiryService.getAllUserIdList(page, size, email));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有交易信息")
|
||||
|
||||
217
src/main/java/com/ai/da/controller/GlobalAwardController.java
Normal file
217
src/main/java/com/ai/da/controller/GlobalAwardController.java
Normal file
@@ -0,0 +1,217 @@
|
||||
package com.ai.da.controller;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.model.dto.*;
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.model.vo.PageVisitCountVO;
|
||||
import com.ai.da.service.GlobalAwardService;
|
||||
import com.ai.da.service.upload.UploadService;
|
||||
import com.ai.da.service.upload.UploadTask;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/global-award")
|
||||
@Api(tags = "全球奖项API", description = "全球奖项大赛管理和文件上传")
|
||||
public class GlobalAwardController {
|
||||
|
||||
@Resource
|
||||
private GlobalAwardService globalAwardService;
|
||||
|
||||
@Resource
|
||||
private UploadService uploadService;
|
||||
|
||||
// @PostMapping("/uploads/pdf")
|
||||
// public Response<String> uploadPdf(@RequestParam("file") MultipartFile file,
|
||||
// @RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
// return Response.success(globalAwardService.uploadPdf(file, email));
|
||||
// }
|
||||
//
|
||||
// @PostMapping("/uploads/video")
|
||||
// public Response<String> uploadVideo(@RequestParam("file") MultipartFile file,
|
||||
// @RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
// return Response.success(globalAwardService.uploadVideo(file, email));
|
||||
// }
|
||||
|
||||
// ===== 新增分片上传接口 =====
|
||||
|
||||
// ===== PDF分片上传接口 =====
|
||||
|
||||
/** 初始化PDF上传任务 */
|
||||
@PostMapping("/uploads/pdf/init")
|
||||
@ApiOperation(value = "初始化PDF上传", notes = "创建新的PDF上传任务并返回上传参数")
|
||||
public Response<UploadInitResponse> initPdfUpload(@ApiParam(value = "PDF上传初始化请求", required = true) @RequestBody UploadInitRequest request) {
|
||||
UploadTask uploadTask = uploadService.initPdfUpload(request);
|
||||
return Response.success(UploadInitResponse.builder()
|
||||
.uploadId(uploadTask.getUploadId())
|
||||
.chunkSize(uploadTask.getChunkSize())
|
||||
.totalChunks(uploadTask.getTotalChunks())
|
||||
.expiresAt(uploadTask.getExpiresAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
/** 上传PDF分片 */
|
||||
@PostMapping("/uploads/pdf/chunk")
|
||||
@ApiOperation(value = "上传PDF分片", notes = "上传PDF文件的单个分片")
|
||||
public Response<UploadChunkResponse> uploadPdfChunk(
|
||||
@ApiParam(value = "PDF文件分片", required = true) @RequestParam("chunk") MultipartFile chunk,
|
||||
@ApiParam(value = "上传任务ID", required = true) @RequestParam("uploadId") String uploadId,
|
||||
@ApiParam(value = "分片索引(从0开始)", required = true) @RequestParam("chunkIndex") int chunkIndex,
|
||||
@ApiParam(value = "分片总数", required = true) @RequestParam("totalChunks") int totalChunks) {
|
||||
|
||||
UploadChunkResponse uploadChunkResponse = uploadService.uploadPdfChunk(uploadId, chunk, chunkIndex, totalChunks);
|
||||
return Response.success(uploadChunkResponse);
|
||||
}
|
||||
|
||||
/** 完成PDF上传 */
|
||||
@PostMapping("/uploads/pdf/complete")
|
||||
@ApiOperation(value = "完成PDF上传", notes = "完成PDF上传并合并所有分片")
|
||||
public Response<UploadCompleteResponse> completePdfUpload(@ApiParam(value = "PDF上传完成请求", required = true) @RequestBody UploadCompleteRequest request) {
|
||||
UploadCompleteResponse uploadCompleteResponse = uploadService.completePdfUpload(
|
||||
request.getUploadId(),
|
||||
request.getFileName(),
|
||||
request.getTotalSize(),
|
||||
request.getEmail(),
|
||||
request.getSecureToken());
|
||||
return Response.success(uploadCompleteResponse);
|
||||
}
|
||||
|
||||
/** 查询PDF上传状态 */
|
||||
@GetMapping("/uploads/pdf/status/{uploadId}")
|
||||
@ApiOperation(value = "查询PDF上传状态", notes = "获取PDF上传任务的当前状态")
|
||||
public Response<UploadStatusResponse> getPdfUploadStatus(@ApiParam(value = "上传任务ID", required = true) @PathVariable String uploadId) {
|
||||
UploadStatusResponse pdfUploadStatus = uploadService.getPdfUploadStatus(uploadId);
|
||||
return Response.success(pdfUploadStatus);
|
||||
}
|
||||
|
||||
// ===== 视频分片上传接口 =====
|
||||
|
||||
/** 初始化视频上传任务 */
|
||||
@PostMapping("/uploads/video/init")
|
||||
@ApiOperation(value = "初始化视频上传", notes = "创建新的视频上传任务并返回上传参数")
|
||||
public Response<UploadInitResponse> initVideoUpload(@ApiParam(value = "视频上传初始化请求", required = true) @RequestBody UploadInitRequest request) {
|
||||
UploadTask uploadTask = uploadService.initVideoUpload(request);
|
||||
return Response.success(UploadInitResponse.builder()
|
||||
.uploadId(uploadTask.getUploadId())
|
||||
.chunkSize(uploadTask.getChunkSize())
|
||||
.totalChunks(uploadTask.getTotalChunks())
|
||||
.expiresAt(uploadTask.getExpiresAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
/** 上传视频分片 */
|
||||
@PostMapping("/uploads/video/chunk")
|
||||
@ApiOperation(value = "上传视频分片", notes = "上传视频文件的单个分片")
|
||||
public Response<UploadChunkResponse> uploadVideoChunk(
|
||||
@ApiParam(value = "视频文件分片", required = true) @RequestParam("chunk") MultipartFile chunk,
|
||||
@ApiParam(value = "上传任务ID", required = true) @RequestParam("uploadId") String uploadId,
|
||||
@ApiParam(value = "分片索引(从0开始)", required = true) @RequestParam("chunkIndex") int chunkIndex,
|
||||
@ApiParam(value = "分片总数", required = true) @RequestParam("totalChunks") int totalChunks) {
|
||||
|
||||
UploadChunkResponse uploadChunkResponse = uploadService.uploadVideoChunk(uploadId, chunk, chunkIndex, totalChunks);
|
||||
return Response.success(uploadChunkResponse);
|
||||
}
|
||||
|
||||
/** 完成视频上传 */
|
||||
@PostMapping("/uploads/video/complete")
|
||||
@ApiOperation(value = "完成视频上传", notes = "完成视频上传并合并所有分片")
|
||||
public Response<UploadCompleteResponse> completeVideoUpload(@ApiParam(value = "视频上传完成请求", required = true) @RequestBody UploadCompleteRequest request) {
|
||||
UploadCompleteResponse uploadCompleteResponse = uploadService.completeVideoUpload(
|
||||
request.getUploadId(),
|
||||
request.getFileName(),
|
||||
request.getTotalSize(),
|
||||
request.getEmail(),
|
||||
request.getSecureToken());
|
||||
return Response.success(uploadCompleteResponse);
|
||||
}
|
||||
|
||||
/** 查询视频上传状态 */
|
||||
@GetMapping("/uploads/video/status/{uploadId}")
|
||||
@ApiOperation(value = "查询视频上传状态", notes = "获取视频上传任务的当前状态")
|
||||
public Response<UploadStatusResponse> getVideoUploadStatus(@ApiParam(value = "上传任务ID", required = true) @PathVariable String uploadId) {
|
||||
UploadStatusResponse videoUploadStatus = uploadService.getVideoUploadStatus(uploadId);
|
||||
return Response.success(videoUploadStatus);
|
||||
}
|
||||
|
||||
@PostMapping("/contestants/save")
|
||||
@ApiOperation(value = "保存参赛者信息", notes = "保存或更新参赛者信息及已上传的文件")
|
||||
public Response<Map<String,Object>> submit(@ApiParam(value = "参赛者信息", required = true) @RequestBody ContestantDTO request) {
|
||||
return Response.success(globalAwardService.saveContestant(request));
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/{id}")
|
||||
@ApiOperation(value = "根据id查询参赛者", notes = "根据id获取参赛者信息")
|
||||
public Response<ContestantDTO> getContestantByID(@ApiParam(value = "参赛者id", required = true) @PathVariable("id") String id) {
|
||||
ContestantDTO dto = globalAwardService.getContestantByID(id);
|
||||
return Response.success(dto);
|
||||
}
|
||||
|
||||
@GetMapping("/checkEmail")
|
||||
public Response<String> checkEmail(@RequestParam("email") String email) {
|
||||
globalAwardService.checkEmail(email);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@GetMapping("/checkCode")
|
||||
public Response<CheckOTPVO> checkCode(@RequestParam("email") String email, @RequestParam("code") String code) {
|
||||
return Response.success(globalAwardService.checkCode(email, code));
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/export")
|
||||
@ApiOperation(value = "导出参赛者列表为Excel", notes = "导出所有参赛者信息为xlsx并触发下载")
|
||||
public void exportContestants(HttpServletResponse response) throws Exception {
|
||||
byte[] data = globalAwardService.exportContestants();
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"contestants.xlsx\"");
|
||||
response.setContentLength(data.length);
|
||||
response.getOutputStream().write(data);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
@PostMapping("/contestants/export/files")
|
||||
@ApiOperation(value = "导出参赛者文件为ZIP", notes = "根据参赛者编号范围导出PDF、视频和信息文件为ZIP,直接响应给浏览器")
|
||||
public void exportContestantFiles(@ApiParam(value = "参赛者文件导出请求", required = true) @RequestBody ContestantExportRequest request, HttpServletResponse response) throws Exception {
|
||||
byte[] zipData = globalAwardService.exportContestantFilesAsZip(request.getMinContestantNumber(), request.getMaxContestantNumber());
|
||||
if (zipData.length == 0) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
response.getWriter().write("No contestants found in the specified range.");
|
||||
return;
|
||||
}
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"contestants.zip\"");
|
||||
response.setContentLength(zipData.length);
|
||||
response.getOutputStream().write(zipData);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/count")
|
||||
@ApiOperation(value = "查询参赛者总数", notes = "查询数据库中参赛者的总数量和最大参赛者编号")
|
||||
public Response<ContestantCountVO> getContestantCount() {
|
||||
return Response.success(globalAwardService.getContestantCount());
|
||||
}
|
||||
|
||||
@PostMapping("/page/visit")
|
||||
@ApiOperation(value = "记录比赛页面访问量", notes = "记录比赛页面的访问量,包含两种统计方式:每次访问/刷新计一次,以及5秒内刷新只计一次")
|
||||
public Response<Void> recordPageVisit(@ApiParam(value = "会话ID,用于5秒内去重判断", required = false) @RequestParam(value = "sessionId", required = false) String sessionId) {
|
||||
globalAwardService.recordPageVisit(sessionId);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@GetMapping("/page/visit/count")
|
||||
@ApiOperation(value = "获取比赛页面访问量", notes = "获取比赛页面的两种访问量:rawVisitCount(每次访问/刷新计一次)和 uniqueVisitCount(5秒内刷新只计一次)")
|
||||
public Response<PageVisitCountVO> getPageVisitCount() {
|
||||
return Response.success(globalAwardService.getPageVisitCount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
@@ -119,4 +122,14 @@ public class PythonController {
|
||||
return Response.success(superResolutionService.prepareForSR(superResolutionDTO));
|
||||
}
|
||||
|
||||
@CrossOrigin
|
||||
@Operation(summary = "Seg Anything 转发接口")
|
||||
@PostMapping("/segAnything")
|
||||
public Response<String> segAnything(@RequestBody Map<String, Object> payload) {
|
||||
// 将前端传来的 Map 转为 fastjson JSONObject 并转发给 python 服务
|
||||
JSONObject requestJson = (JSONObject) JSON.toJSON(payload);
|
||||
String url = pythonService.segAnything(requestJson);
|
||||
return Response.success(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ai.da.controller;
|
||||
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
@@ -10,6 +11,7 @@ import com.ai.da.model.dto.ProductPurchaseDTO;
|
||||
import com.ai.da.model.dto.QueryCouponsPageDTO;
|
||||
import com.ai.da.model.vo.CheckCouponsVO;
|
||||
import com.ai.da.service.StripeService;
|
||||
import com.ai.da.service.StripeSubscriptionService;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.paypal.http.HttpResponse;
|
||||
import com.paypal.payments.Refund;
|
||||
@@ -40,6 +42,8 @@ public class StripeController {
|
||||
private StripeService stripeService;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
|
||||
@Operation(summary = "创建支付链接")
|
||||
@PostMapping("/createOrder")
|
||||
@@ -53,30 +57,29 @@ public class StripeController {
|
||||
@Operation(summary = "支付通知")
|
||||
@PostMapping("/trade/notify")
|
||||
public void callback(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
try{
|
||||
Boolean result = stripeService.notify(request);
|
||||
if (result){
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
}else {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}catch (Exception e){
|
||||
log.error("Stripe Controller层异常捕捉, {}", e.getMessage());
|
||||
e.printStackTrace();
|
||||
boolean result;
|
||||
try {
|
||||
result = stripeService.notify(request);
|
||||
} catch (Exception e) {
|
||||
log.error("Stripe Controller层异常捕捉, {}", e.getMessage(), e);
|
||||
String key_1 = RedisUtil.STRIPE_EXCEPTION_LOG + DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_HH);
|
||||
String key_2 = key_1 + ":" + DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_hh_mm_ss);
|
||||
String key_2 = key_1 + ":" + DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS);
|
||||
String stackTrace = stripeService.getStackTrace(e, 10);
|
||||
redisUtil.addToString(key_2, stackTrace);
|
||||
Long size = redisUtil.getSize(key_1);
|
||||
// 给我发送邮件
|
||||
if (webhookReminderFlag.equals("1") && size == 3){
|
||||
if ("1".equals(webhookReminderFlag) && size == 3) {
|
||||
SendEmailUtil.commonExceptionReminder("Stripe Webhook 回调处理出现异常", new String[]{"xupei3360@163.com"});
|
||||
}
|
||||
result = false;
|
||||
}
|
||||
if (result) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
} else {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "申请退款")
|
||||
/* @Operation(summary = "申请退款")
|
||||
@GetMapping("/trade/refund/{orderNo}/{reason}")
|
||||
public Response<HttpResponse<Refund>> refund(@PathVariable String orderNo, @PathVariable String reason) throws IOException {
|
||||
String response = stripeService.refund(null,orderNo,reason);
|
||||
@@ -85,7 +88,7 @@ public class StripeController {
|
||||
}else {
|
||||
return Response.fail("Request for refund failed.");
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
@Operation(summary = "获取订阅")
|
||||
@GetMapping("/getSubscription")
|
||||
@@ -100,7 +103,8 @@ public class StripeController {
|
||||
@Operation(summary = "取消订阅")
|
||||
@GetMapping("/cancelSubscription")
|
||||
public Response<String> cancelSubscription(@RequestParam String subscriptionId, @RequestParam(required = false) String reason) {
|
||||
stripeService.cancelSubscription(subscriptionId, reason);
|
||||
Long accountId = UserContext.getUserHolder().getId();
|
||||
stripeSubscriptionService.cancelSubscription(subscriptionId, reason, accountId);
|
||||
return Response.success("success");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.ai.da.controller;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.task.SubscriptionReminderTask;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionPlan;
|
||||
import com.ai.da.model.dto.SubscriptionPlanDTO;
|
||||
import com.ai.da.model.dto.SubscriptionPlanPageQuery;
|
||||
@@ -8,6 +9,7 @@ import com.ai.da.model.dto.UpdateSubscriptionPlanDTO;
|
||||
import com.ai.da.model.vo.SubscriptionPlanVO;
|
||||
import com.ai.da.service.SubscriptionPlanService;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
@@ -26,6 +28,8 @@ public class SubscriptionPlanController {
|
||||
|
||||
private final SubscriptionPlanService subscriptionPlanService;
|
||||
|
||||
private final SubscriptionReminderTask subscriptionReminderTask;
|
||||
|
||||
@Operation(summary = "创建订阅计划")
|
||||
@PostMapping("/createPlan")
|
||||
public Response<String> createPlan(@Valid @RequestBody SubscriptionPlanDTO subscriptionPlanDTO) {
|
||||
@@ -74,13 +78,15 @@ public class SubscriptionPlanController {
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
// @Hidden
|
||||
@Operation(summary = "activeSubscriptionPlan")
|
||||
@GetMapping("/activeSubscriptionPlan")
|
||||
public Response<String> activeSubscriptionPlan() {
|
||||
subscriptionPlanService.activeSubscriptionPlan();
|
||||
subscriptionPlanService.activeSubscriptionPlan(null);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
// @Hidden
|
||||
@Operation(summary = "expireSubscription")
|
||||
@GetMapping("/expireSubscription")
|
||||
public Response<String> expireSubscription() {
|
||||
@@ -88,5 +94,19 @@ public class SubscriptionPlanController {
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "subscriptionReminder")
|
||||
@GetMapping("/subscriptionReminder")
|
||||
public Response<String> subscriptionReminder() {
|
||||
subscriptionReminderTask.subscriptionReminder();
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "trialReminder")
|
||||
@GetMapping("/trialReminder")
|
||||
public Response<String> trialReminder() {
|
||||
subscriptionReminderTask.trialReminder();
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
12
src/main/java/com/ai/da/mapper/primary/ContestantMapper.java
Normal file
12
src/main/java/com/ai/da/mapper/primary/ContestantMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.ai.da.mapper.primary;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.Contestant;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestantMapper extends BaseMapper<Contestant> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface DesignMapper extends CommonMapper<Design> {
|
||||
Long insertDesign(Design design);
|
||||
|
||||
List<UserDesignStatisticDTO> getDesignStatistic(String startTime, String endTime, List<Long> ids, String email,
|
||||
String role, String organizationName);
|
||||
String role, String organizationName, boolean filterBySecond);
|
||||
|
||||
List<Design> selectDeleteList();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.ai.da.common.config.mybatis.plus.CommonMapper;
|
||||
import com.ai.da.mapper.primary.entity.Notification;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -20,5 +21,5 @@ public interface NotificationMapper extends CommonMapper<Notification> {
|
||||
|
||||
void setPersonalNotificationAllRead(String type, Long receiverId, LocalDateTime time);
|
||||
|
||||
List<Long> getUnreadSysNotification(Long accountId);
|
||||
List<Long> getUnreadSysNotification(Long accountId, Date createTime);
|
||||
}
|
||||
|
||||
@@ -14,11 +14,16 @@ public interface SubscriptionPlanMapper extends BaseMapper<SubscriptionPlan> {
|
||||
/**
|
||||
* 关联查询订阅计划信息(包含管理员邮箱)- 分页
|
||||
*/
|
||||
@Select("SELECT sp.*, a.user_email as adminAccEmail, a.user_name as adminAccName " +
|
||||
"FROM t_subscription_plan sp " +
|
||||
"LEFT JOIN t_account a ON sp.admin_acc_id = a.id " +
|
||||
"WHERE sp.is_deleted = 0 " +
|
||||
"${ew.customSqlSegment}")
|
||||
@Select("""
|
||||
SELECT sp.*,
|
||||
a.user_email AS adminAccEmail,
|
||||
a.user_name AS adminAccName,
|
||||
o.name AS organizationName
|
||||
FROM t_subscription_plan sp
|
||||
LEFT JOIN t_account a ON sp.admin_acc_id = a.id
|
||||
LEFT JOIN t_organization o on sp.organization_id = o.id
|
||||
${ew.customSqlSegment}
|
||||
""")
|
||||
Page<SubscriptionPlanVO> selectWithEmailPage(Page<SubscriptionPlanVO> page,
|
||||
@Param(Constants.WRAPPER) Wrapper<?> wrapper);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ai.da.mapper.primary;
|
||||
|
||||
import com.ai.da.common.config.mybatis.plus.CommonMapper;
|
||||
import com.ai.da.mapper.primary.entity.UserPreference;
|
||||
|
||||
public interface UserPreferenceMapper extends CommonMapper<UserPreference> {
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.ai.da.mapper.primary;
|
||||
import com.ai.da.common.config.mybatis.plus.CommonMapper;
|
||||
import com.ai.da.mapper.primary.entity.WorkspaceRelStyle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mapper 接口
|
||||
*
|
||||
@@ -11,5 +13,11 @@ import com.ai.da.mapper.primary.entity.WorkspaceRelStyle;
|
||||
*/
|
||||
public interface WorkspaceRelStyleMapper extends CommonMapper<WorkspaceRelStyle> {
|
||||
|
||||
/**
|
||||
* 根据projectId查询workspaceRelStyles
|
||||
* @param projectId 项目ID
|
||||
* @return workspaceRelStyles列表
|
||||
*/
|
||||
List<WorkspaceRelStyle> selectByProjectId(Long projectId);
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -76,13 +77,13 @@ public class Account implements Serializable {
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createDate;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateDate;
|
||||
|
||||
private Integer isTrial;
|
||||
@@ -142,4 +143,26 @@ public class Account implements Serializable {
|
||||
private String givenName;
|
||||
|
||||
private Long subscriptionPlanId;
|
||||
|
||||
// 在类内部定义的枚举
|
||||
@Getter
|
||||
public enum SystemRole {
|
||||
VISITOR("游客", 0),
|
||||
YEARLY("年付用户", 1),
|
||||
MONTHLY("月付用户", 2),
|
||||
TRIAL("试用用户", 3),
|
||||
EVENT_USER("参加活动获取30天有效期和6000个积分的用户", 4),
|
||||
ENTERPRISE_ADMIN("企业管理员账号", 5),
|
||||
ENTERPRISE_SUB("企业子账号", 6),
|
||||
EDUCATION_ADMIN("学校管理员", 7),
|
||||
EDUCATION_SUB("学校子账号", 8);
|
||||
|
||||
private final String desc;
|
||||
private final int code;
|
||||
|
||||
SystemRole(String desc, int code) {
|
||||
this.desc = desc;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ai.da.mapper.primary.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* submissions 表对应实体 — 参赛选手信息 (Contestant)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("contestants")
|
||||
public class Contestant {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
private String email;
|
||||
|
||||
@TableField("contestant_number")
|
||||
private Integer contestantNumber;
|
||||
|
||||
@TableField("first_name")
|
||||
private String firstName;
|
||||
|
||||
@TableField("last_name")
|
||||
private String lastName;
|
||||
|
||||
private String gender;
|
||||
|
||||
private String occupation;
|
||||
|
||||
private Integer age;
|
||||
|
||||
@TableField("country_region_city")
|
||||
private String countryRegionCity;
|
||||
|
||||
@TableField("phone_number")
|
||||
private String phoneNumber;
|
||||
|
||||
@TableField("design_title")
|
||||
private String designTitle;
|
||||
|
||||
@TableField("design_description")
|
||||
private String designDescription;
|
||||
|
||||
@TableField("pdf_path")
|
||||
private String pdfPath;
|
||||
|
||||
@TableField("video_path")
|
||||
private String videoPath;
|
||||
|
||||
@TableField("video_duration")
|
||||
private Integer videoDuration;
|
||||
|
||||
@TableField("video_size")
|
||||
private Long videoSize;
|
||||
|
||||
@TableField("pdf_size")
|
||||
private Long pdfSize;
|
||||
|
||||
@TableField("portfolio_url")
|
||||
private String portfolioUrl;
|
||||
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -62,4 +62,9 @@ public class DesignItemDetailPrint {
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateDate;
|
||||
|
||||
/**
|
||||
* 对象信息(JSON格式)
|
||||
*/
|
||||
private String object;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ public class OrderInfo extends BaseEntity{
|
||||
|
||||
private String note;
|
||||
|
||||
private byte autoRenewal;
|
||||
|
||||
private String paymentType;//支付方式
|
||||
|
||||
// 可用于标记用户订单是否首次订阅
|
||||
|
||||
@@ -67,6 +67,11 @@ public class SubscriptionPlan extends BaseEntity{
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 国家或地区
|
||||
*/
|
||||
private String countryOrRegion;
|
||||
|
||||
// 在类内部定义的枚举
|
||||
@Getter
|
||||
public enum SubscriptionStatus {
|
||||
|
||||
@@ -90,6 +90,19 @@ public class TDesignPythonOutfitDetail implements Serializable {
|
||||
*/
|
||||
@Schema(description = "图层优先级")
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 镜像模式
|
||||
*/
|
||||
@Schema(description = "镜像模式")
|
||||
private String transpose;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
@Schema(description = "旋转角度")
|
||||
private Double rotate;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ai.da.mapper.primary.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("user_preference")
|
||||
public class UserPreference implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long accountId;
|
||||
private String path;
|
||||
private LocalDateTime dataTime;
|
||||
private String category;
|
||||
private String style;
|
||||
private Long workspaceRelStyleId;
|
||||
private Long projectId;
|
||||
private Long designItemId;
|
||||
|
||||
}
|
||||
@@ -23,5 +23,8 @@ public class UserPreferenceLogTest implements Serializable {
|
||||
private Long accountId;
|
||||
private String path;
|
||||
private LocalDateTime dataTime;
|
||||
private String category;
|
||||
private String style;
|
||||
private Long sysFileId;
|
||||
|
||||
}
|
||||
|
||||
74
src/main/java/com/ai/da/model/dto/ContestantDTO.java
Normal file
74
src/main/java/com/ai/da/model/dto/ContestantDTO.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Contestant request DTO for Global Award
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "参赛者信息", description = "全球奖项大赛参赛者信息数据传输对象")
|
||||
public class ContestantDTO {
|
||||
@ApiModelProperty(value = "邮箱地址", required = true, example = "user@example.com")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty(value = "名字", required = true, example = "John")
|
||||
private String firstName;
|
||||
|
||||
@ApiModelProperty(value = "姓氏", required = true, example = "Doe")
|
||||
private String lastName;
|
||||
|
||||
@ApiModelProperty(value = "性别", required = true, example = "Male", allowableValues = "Male,Female,Other")
|
||||
private String gender;
|
||||
|
||||
@ApiModelProperty(value = "职业", required = true, example = "Designer")
|
||||
private String occupation;
|
||||
|
||||
@ApiModelProperty(value = "年龄", required = true, example = "25")
|
||||
private Integer age;
|
||||
|
||||
@ApiModelProperty(value = "国家/地区/城市", required = true, example = "China/Shanghai/Shanghai")
|
||||
private String countryRegionCity;
|
||||
|
||||
@ApiModelProperty(value = "电话号码", required = true, example = "+86 138 0000 0000")
|
||||
private String phoneNumber;
|
||||
|
||||
@ApiModelProperty(value = "作品集链接", required = false, example = "https://portfolio.example.com")
|
||||
private String portfolioUrl;
|
||||
|
||||
@ApiModelProperty(value = "设计作品标题", required = true, example = "Modern Office Building Design")
|
||||
private String designTitle;
|
||||
|
||||
@ApiModelProperty(value = "设计作品描述", required = true, example = "A modern office building design featuring sustainable materials...")
|
||||
private String designDescription;
|
||||
|
||||
@ApiModelProperty(value = "PDF文件路径", required = false, example = "contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String pdfPath;
|
||||
|
||||
@ApiModelProperty(value = "视频文件路径", required = false, example = "contestants/user@example.com/2024/01/video_1234567890.mp4")
|
||||
private String videoPath;
|
||||
|
||||
@ApiModelProperty(value = "视频时长(秒)", required = false, example = "120")
|
||||
private Integer videoDuration;
|
||||
|
||||
@ApiModelProperty(value = "视频大小(字节)", required = false, example = "10485760")
|
||||
private Long videoSize;
|
||||
|
||||
@ApiModelProperty(value = "PDF 文件大小(字节)", required = false, example = "524288")
|
||||
private Long pdfSize;
|
||||
|
||||
// /**
|
||||
// * 是否确认覆盖已存在记录(false 表示发现已有记录时仅返回 existingRecord,不覆盖)
|
||||
// */
|
||||
// @ApiModelProperty(value = "是否确认覆盖已存在记录", required = false, example = "false")
|
||||
// private Boolean confirm = false;
|
||||
|
||||
@NotBlank
|
||||
private String secureToken;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 参赛者文件导出请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "参赛者文件导出请求", description = "用于导出指定范围的参赛者文件")
|
||||
public class ContestantExportRequest {
|
||||
|
||||
@ApiModelProperty(value = "最小参赛者编号", required = true, example = "10000")
|
||||
private Integer minContestantNumber;
|
||||
|
||||
@ApiModelProperty(value = "最大参赛者编号", required = true, example = "10010")
|
||||
private Integer maxContestantNumber;
|
||||
}
|
||||
@@ -43,6 +43,10 @@ public class DesignSingleIncludeLayersDTO implements Serializable {
|
||||
@Schema(description = "项目id")
|
||||
private Long projectId;
|
||||
|
||||
@NotBlank(message = "designType cannot be empty")
|
||||
@Schema(description = "default -> 新增sketch || merge")
|
||||
private String designType;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DesignSingleIncludeLayersDTO{" +
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.Gradient;
|
||||
@@ -67,4 +68,18 @@ public class DesignSingleItemDTO implements Serializable {
|
||||
|
||||
private PartialDesignDTO partialDesign;
|
||||
|
||||
@Schema(description = "镜像模式 ")
|
||||
private int[] transpose;
|
||||
|
||||
@Schema(description = "45")
|
||||
private double rotate;
|
||||
|
||||
@Hidden
|
||||
@Schema(description = "带overall印花未分割图片")
|
||||
private String undividedLayerBase64;
|
||||
|
||||
@Hidden
|
||||
@Schema(description = "带overall/single印花未分割图片")
|
||||
private String undividedLayerWithSinglePrintBase64;
|
||||
|
||||
}
|
||||
|
||||
55
src/main/java/com/ai/da/model/dto/ImageProcessRequest.java
Normal file
55
src/main/java/com/ai/da/model/dto/ImageProcessRequest.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片处理请求体
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class ImageProcessRequest {
|
||||
|
||||
/**
|
||||
* OSS桶名(bucket_name)
|
||||
*/
|
||||
private String bucket_name;
|
||||
|
||||
/**
|
||||
* OSS对象名(object_name)
|
||||
*/
|
||||
private String object_name;
|
||||
|
||||
/**
|
||||
* 输入图片路径列表(input_image_paths)
|
||||
*/
|
||||
private List<String> input_image_paths;
|
||||
|
||||
/**
|
||||
* 图像宽度(width)
|
||||
*/
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 图像高度(height)
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 文本提示(prompt)
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 推理步数(steps)
|
||||
*/
|
||||
private Integer steps;
|
||||
|
||||
/**
|
||||
* 引导系数(guidance)
|
||||
*/
|
||||
private Double guidance;
|
||||
|
||||
}
|
||||
@@ -20,17 +20,17 @@ public class PartialDesignDTO implements Serializable {
|
||||
@Schema(description = "图片的base64格式")
|
||||
private String partialDesignBase64;
|
||||
|
||||
@Schema(description = "图层信息")
|
||||
private List<String> layers;
|
||||
/* @Schema(description = "图层信息")
|
||||
private List<String> layers;*/
|
||||
|
||||
public PartialDesignDTO(String partialDesignMinioPath) {
|
||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||
}
|
||||
|
||||
public PartialDesignDTO(String partialDesignMinioPath, List<String> layers) {
|
||||
/* public PartialDesignDTO(String partialDesignMinioPath, List<String> layers) {
|
||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||
this.layers = layers;
|
||||
}
|
||||
}*/
|
||||
|
||||
public PartialDesignDTO(String partialDesignMinioPath, String partialDesignPath) {
|
||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -24,6 +25,7 @@ public class ProductPurchaseDTO {
|
||||
@Schema(description = "EcoMonth || Month || Year")
|
||||
private String subscribeType;
|
||||
|
||||
@Hidden
|
||||
@Schema(description = "是否自动续订 one_time || recurring")
|
||||
private Boolean autoRenewal;
|
||||
|
||||
|
||||
@@ -45,4 +45,10 @@ public class SubscriptionPlanDTO {
|
||||
@Schema(description = "订阅计划命名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "订阅计划状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String CountryOrRegion;
|
||||
|
||||
}
|
||||
|
||||
@@ -12,9 +12,12 @@ public class SubscriptionPlanPageQuery extends QueryPageByTimeDTO {
|
||||
@Schema(description = "组织id")
|
||||
private Long organizationId;
|
||||
|
||||
@Schema(description = "管理id")
|
||||
@Schema(description = "管理员id")
|
||||
private Long adminAccId;
|
||||
|
||||
@Schema(description = "状态 PENDING||ACTIVE||EXPIRED")
|
||||
private List<String> status;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String countryOrRegion;
|
||||
}
|
||||
|
||||
@@ -32,4 +32,7 @@ public class UpdateSubscriptionPlanDTO {
|
||||
@Schema(description = "订阅重命名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String countryOrRegion;
|
||||
|
||||
}
|
||||
|
||||
33
src/main/java/com/ai/da/model/dto/UploadChunkResponse.java
Normal file
33
src/main/java/com/ai/da/model/dto/UploadChunkResponse.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分片上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "分片上传响应", description = "单个文件分片上传成功的响应数据")
|
||||
public class UploadChunkResponse {
|
||||
|
||||
/**
|
||||
* 分片索引
|
||||
*/
|
||||
@ApiModelProperty(value = "分片索引(从0开始)", required = true, example = "0")
|
||||
private Integer chunkIndex;
|
||||
|
||||
/**
|
||||
* 是否上传成功
|
||||
*/
|
||||
@ApiModelProperty(value = "是否上传成功", required = true, example = "true")
|
||||
private Boolean uploaded;
|
||||
|
||||
/**
|
||||
* 分片大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "分片大小(字节)", required = true, example = "1048576")
|
||||
private Long size;
|
||||
}
|
||||
53
src/main/java/com/ai/da/model/dto/UploadCompleteRequest.java
Normal file
53
src/main/java/com/ai/da/model/dto/UploadCompleteRequest.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
/**
|
||||
* 完成上传请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "完成上传请求", description = "文件上传完成时使用的请求参数")
|
||||
public class UploadCompleteRequest {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@NotBlank(message = "上传任务ID不能为空")
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
@ApiModelProperty(value = "原始文件名", required = true, example = "design.pdf")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件总大小(字节)
|
||||
*/
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Positive(message = "文件大小必须大于0")
|
||||
@ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760")
|
||||
private Long totalSize;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
@NotBlank(message = "用户邮箱不能为空")
|
||||
@ApiModelProperty(value = "用户邮箱", required = true, example = "user@example.com")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 安全令牌(邮箱验证令牌)
|
||||
*/
|
||||
@NotBlank(message = "安全令牌不能为空")
|
||||
@ApiModelProperty(value = "安全令牌", required = true, example = "abc123def456")
|
||||
private String secureToken;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 完成上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "完成上传响应", description = "文件上传完成并合并成功的响应数据")
|
||||
public class UploadCompleteResponse {
|
||||
|
||||
/**
|
||||
* 文件在MinIO中的路径
|
||||
*/
|
||||
@ApiModelProperty(value = "文件在MinIO中的存储路径", required = true, example = "contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 文件的完整URL
|
||||
*/
|
||||
@ApiModelProperty(value = "文件的完整访问URL", required = true, example = "https://minio.example.com/contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String fileUrl;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "文件大小(字节)", required = true, example = "10485760")
|
||||
private Long fileSize;
|
||||
}
|
||||
53
src/main/java/com/ai/da/model/dto/UploadInitRequest.java
Normal file
53
src/main/java/com/ai/da/model/dto/UploadInitRequest.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
/**
|
||||
* 初始化上传请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "初始化上传请求", description = "文件上传初始化时使用的请求参数")
|
||||
public class UploadInitRequest {
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
@ApiModelProperty(value = "文件名", required = true, example = "design.pdf")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Positive(message = "文件大小必须大于0")
|
||||
@ApiModelProperty(value = "文件大小(字节)", required = true, example = "10485760")
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 文件类型(MIME类型)
|
||||
*/
|
||||
@NotBlank(message = "文件类型不能为空")
|
||||
@ApiModelProperty(value = "文件类型(MIME类型)", required = true, example = "application/pdf")
|
||||
private String fileType;
|
||||
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
@ApiModelProperty(value = "用户邮箱", required = true, example = "user@example.com")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 安全令牌(邮箱验证令牌)
|
||||
*/
|
||||
@NotBlank(message = "安全令牌不能为空")
|
||||
@ApiModelProperty(value = "安全令牌", required = true, example = "abc123def456")
|
||||
private String secureToken;
|
||||
}
|
||||
41
src/main/java/com/ai/da/model/dto/UploadInitResponse.java
Normal file
41
src/main/java/com/ai/da/model/dto/UploadInitResponse.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 初始化上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "初始化上传响应", description = "文件上传初始化成功的响应数据")
|
||||
public class UploadInitResponse {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 分片大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "每个分片的大小(字节)", required = true, example = "1048576")
|
||||
private Integer chunkSize;
|
||||
|
||||
/**
|
||||
* 总分片数
|
||||
*/
|
||||
@ApiModelProperty(value = "文件被分成多少个分片", required = true, example = "10")
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 任务过期时间
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务过期时间", required = true, example = "2024-01-20T10:30:00")
|
||||
private LocalDateTime expiresAt;
|
||||
}
|
||||
59
src/main/java/com/ai/da/model/dto/UploadStatusResponse.java
Normal file
59
src/main/java/com/ai/da/model/dto/UploadStatusResponse.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 上传状态响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "上传状态响应", description = "查询上传任务当前状态的响应数据")
|
||||
public class UploadStatusResponse {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 上传状态
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务状态", required = true, example = "uploading", allowableValues = "initiated,uploading,completed,failed,expired")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 上传进度百分比 (0-100)
|
||||
*/
|
||||
@ApiModelProperty(value = "上传进度百分比(0-100)", required = true, example = "60.0")
|
||||
private Double progress;
|
||||
|
||||
/**
|
||||
* 已上传分片索引集合
|
||||
*/
|
||||
@ApiModelProperty(value = "已上传分片的索引集合", required = true, example = "[0,1,2,3,4]")
|
||||
private Set<Integer> uploadedChunks;
|
||||
|
||||
/**
|
||||
* 总分片数
|
||||
*/
|
||||
@ApiModelProperty(value = "文件被分成多少个分片", required = true, example = "10")
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 文件总大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760")
|
||||
private Long totalSize;
|
||||
|
||||
/**
|
||||
* 已上传大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "已上传的数据大小(字节)", required = true, example = "6291456")
|
||||
private Long uploadedSize;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.AccountExtend;
|
||||
@@ -79,4 +79,10 @@ public class AccountLoginVO {
|
||||
|
||||
private String givenName;
|
||||
|
||||
private Long organizationId;
|
||||
|
||||
private String organizationName;
|
||||
|
||||
private Long subscriptionPlanId;
|
||||
|
||||
}
|
||||
|
||||
16
src/main/java/com/ai/da/model/vo/CheckOTPVO.java
Normal file
16
src/main/java/com/ai/da/model/vo/CheckOTPVO.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CheckOTPVO {
|
||||
|
||||
private String secureToken;
|
||||
|
||||
private ContestantDTO contestantDTO;
|
||||
}
|
||||
17
src/main/java/com/ai/da/model/vo/ContestantCountVO.java
Normal file
17
src/main/java/com/ai/da/model/vo/ContestantCountVO.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ContestantCountVO {
|
||||
|
||||
private Long count;
|
||||
|
||||
private Integer maxContestantNumber;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.Gradient;
|
||||
@@ -62,11 +62,11 @@ public class DesignItemClothesDetailVO {
|
||||
@Schema(description = "渐变色信息")
|
||||
private Gradient gradient;
|
||||
|
||||
@Schema(description = "未分割的图层")
|
||||
/* @Schema(description = "未分割的图层")
|
||||
private String undividedLayer;
|
||||
|
||||
@Schema(description = "添加single印花的未分割的图层")
|
||||
private String undividedLayerWithSinglePrint;
|
||||
private String undividedLayerWithSinglePrint;*/
|
||||
|
||||
@Schema(description = "局部design")
|
||||
private PartialDesignDTO partialDesign;
|
||||
|
||||
@@ -59,4 +59,16 @@ public class DesignPythonOutfitVO {
|
||||
* 图层优先级 从10开始,优先级数字越大越靠近上层
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 镜像模式
|
||||
*/
|
||||
@Schema(description = "镜像模式")
|
||||
private int[] transpose;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
@Schema(description = "旋转角度")
|
||||
private Double rotate;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ public class DesignSinglePrint implements Serializable {
|
||||
@Schema(description = "印花优先级")
|
||||
private Integer priority;
|
||||
|
||||
private String object;
|
||||
|
||||
public DesignSinglePrint() {
|
||||
}
|
||||
|
||||
|
||||
23
src/main/java/com/ai/da/model/vo/PageVisitCountVO.java
Normal file
23
src/main/java/com/ai/da/model/vo/PageVisitCountVO.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PageVisitCountVO {
|
||||
|
||||
/**
|
||||
* 每次访问或刷新都计一次(不去重)
|
||||
*/
|
||||
private Long rawVisitCount;
|
||||
|
||||
/**
|
||||
* 5秒内刷新只算一次(去重后的真实访客数)
|
||||
*/
|
||||
private Long uniqueVisitCount;
|
||||
}
|
||||
@@ -34,4 +34,8 @@ public class QueryUserConditionsVO extends PageQueryBaseVo {
|
||||
|
||||
private Integer systemUser;
|
||||
|
||||
private Long subscriptionPlanId;
|
||||
|
||||
private Long organizationId;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ public class SubscriptionPlanVO {
|
||||
@Schema(description = "组织id")
|
||||
private Long organizationId;
|
||||
|
||||
@Schema(description = "组织名称")
|
||||
private String organizationName;
|
||||
|
||||
@Schema(description = "当前订阅开始时间")
|
||||
private Long currentPeriodStart;
|
||||
|
||||
@@ -42,4 +45,7 @@ public class SubscriptionPlanVO {
|
||||
@Schema(description = "命名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String countryOrRegion;
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ public class DesignPythonBasic {
|
||||
|
||||
private String single_overall;
|
||||
|
||||
private String preview_submit;
|
||||
// private String preview_submit;
|
||||
|
||||
private String switch_category;
|
||||
/**
|
||||
@@ -40,4 +40,7 @@ public class DesignPythonBasic {
|
||||
|
||||
private Boolean layer_order = Boolean.FALSE;
|
||||
|
||||
// default | merge
|
||||
private String design_type = "default";
|
||||
|
||||
}
|
||||
|
||||
@@ -87,6 +87,22 @@ public class DesignPythonItem {
|
||||
*/
|
||||
private String seg_mask_url;
|
||||
|
||||
/**
|
||||
* 镜像模式
|
||||
*/
|
||||
private int[] transpose;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
private double rotate;
|
||||
|
||||
/**
|
||||
* 前端处理了print之后的结果图,python对该图进行分割
|
||||
* designType为merge时,该字段必须有值,否则会导致python端没有数据返回
|
||||
*/
|
||||
private String merge_image_path;
|
||||
|
||||
public static List<String> OUTWEAR_DRESS_BLOUSE = Arrays.asList(CollectionLevel2TypeEnum.OUTWEAR.getRealName(),
|
||||
CollectionLevel2TypeEnum.DRESS.getRealName(), CollectionLevel2TypeEnum.BLOUSE.getRealName());
|
||||
|
||||
@@ -143,7 +159,8 @@ public class DesignPythonItem {
|
||||
|
||||
public DesignPythonItem(String type, String path, String color, PrintToPython print, Long businessId,
|
||||
Long image_id, List<Long> offset, Float[] resize_scale, Integer priority, String gradient,
|
||||
String gradientString, String seg_mask_url) {
|
||||
String gradientString, String seg_mask_url, int[] transpose, double rotate,
|
||||
String merge_image_path) {
|
||||
this.type = type;
|
||||
this.path = path;
|
||||
this.color = color;
|
||||
@@ -157,6 +174,9 @@ public class DesignPythonItem {
|
||||
this.gradient = gradient;
|
||||
this.gradientString = gradientString;
|
||||
this.seg_mask_url = seg_mask_url;
|
||||
this.transpose = transpose;
|
||||
this.rotate = rotate;
|
||||
this.merge_image_path = merge_image_path;
|
||||
}
|
||||
|
||||
public DesignPythonItem(String type, String path, String color, PrintToPython print, String icon, Long businessId, Long image_id) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.ai.da.python.vo;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class DesignPythonItemPrint {
|
||||
@@ -53,7 +53,7 @@ public class DesignPythonItemPrint {
|
||||
if (ifDesign){
|
||||
this.print_path_list = print_path_list;
|
||||
this.location = Collections.singletonList(Arrays.asList(0.0f, 0.0f));
|
||||
this.print_scale_list = Collections.singletonList(Arrays.asList(0.0f, 0.0f));
|
||||
this.print_scale_list = Collections.singletonList(Arrays.asList(1.0f, 1.0f));
|
||||
this.print_angle_list = Arrays.asList(0.0, 0.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,4 +18,6 @@ public class DesignPythonObjects {
|
||||
List<Library> addLibrary;
|
||||
|
||||
private String requestId;
|
||||
|
||||
private String callback_url;
|
||||
}
|
||||
|
||||
@@ -246,4 +246,6 @@ public interface AccountService extends IService<Account> {
|
||||
void setEduAdminToExpire(Account adminAccount);
|
||||
|
||||
String getOrganizationTypeByRole(Integer roleNum);
|
||||
|
||||
void validateUserValidaExpire(Account account);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public interface ConvenientInquiryService extends IService<Questionnaire> {
|
||||
|
||||
IPage<Account> getUserInfo(QueryUserConditionsVO queryUserConditionsVO);
|
||||
|
||||
List<Map<String, Object>> getAllUserIdList();
|
||||
IPage<Map<String, Object>> getAllUserIdList(Integer pageNum, Integer pageSize, String email);
|
||||
|
||||
PageBaseResponse<PaymentInfoVO> queryTransactionRecords(QueryPaymentInfoDTO queryPaymentInfoDTO);
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public interface DesignItemService extends IService<DesignItem> {
|
||||
|
||||
DesignSingleVO designSingleIncludeLayers(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO);
|
||||
|
||||
Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers);
|
||||
Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers, DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO);
|
||||
|
||||
Map<String, String> setTypeAndUndividedLayer(JSONArray layers);
|
||||
|
||||
|
||||
68
src/main/java/com/ai/da/service/GlobalAwardService.java
Normal file
68
src/main/java/com/ai/da/service/GlobalAwardService.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.ai.da.service;
|
||||
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.model.vo.PageVisitCountVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface GlobalAwardService {
|
||||
String uploadPdf(MultipartFile file, String email) throws Exception;
|
||||
|
||||
String uploadVideo(MultipartFile file, String email) throws Exception;
|
||||
|
||||
Map<String, Object> saveContestant(ContestantDTO request);
|
||||
|
||||
com.ai.da.model.dto.ContestantDTO getContestantByID(String email);
|
||||
|
||||
void checkEmail(String email);
|
||||
|
||||
CheckOTPVO checkCode(String email, String otp);
|
||||
|
||||
void checkSecurityToken(String email, String securityToken);
|
||||
|
||||
/**
|
||||
* 导出参赛者列表为 Excel(二进制)
|
||||
* @return Excel 文件的字节数组
|
||||
*/
|
||||
byte[] exportContestants() throws Exception;
|
||||
|
||||
/**
|
||||
* 将参赛者列表导出并保存到服务端本地目录(使用服务配置的 uploadDir/exports)
|
||||
*/
|
||||
void saveContestantsToLocal() throws Exception;
|
||||
|
||||
/**
|
||||
* 将参赛者文件打包为 ZIP 并返回字节数组(不落盘,直接响应给浏览器)
|
||||
* @param minContestantNumber 最小参赛者编号
|
||||
* @param maxContestantNumber 最大参赛者编号
|
||||
* @return ZIP 文件的字节数组
|
||||
*/
|
||||
byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception;
|
||||
|
||||
/**
|
||||
* 查询参赛者总数和最大参赛者编号
|
||||
* @return 参赛者数量和最大参赛者编号
|
||||
*/
|
||||
ContestantCountVO getContestantCount();
|
||||
|
||||
/**
|
||||
* 记录比赛页面的访问量
|
||||
* <ul>
|
||||
* <li>rawVisitCount: 每次访问或刷新都计一次(不去重)</li>
|
||||
* <li>uniqueVisitCount: 5秒内刷新只算一次(基于会话去重)</li>
|
||||
* </ul>
|
||||
* @param sessionId 会话ID(用于5秒去重判断)
|
||||
*/
|
||||
void recordPageVisit(String sessionId);
|
||||
|
||||
/**
|
||||
* 获取比赛页面的两种访问量
|
||||
* @return 原始访问量和去重访问量
|
||||
*/
|
||||
PageVisitCountVO getPageVisitCount();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public interface OrderInfoService extends IService<OrderInfo> {
|
||||
OrderInfo createOrderByProductId(Integer productId, String paymentType, HttpServletRequest request);
|
||||
|
||||
OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
||||
HttpServletRequest request, byte autoRenewal);
|
||||
HttpServletRequest request);
|
||||
|
||||
void saveCodeUrl(String orderNo, String codeUrl);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.paypal.orders.Order;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.PaymentMethod;
|
||||
import com.stripe.model.checkout.Session;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -23,9 +25,15 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
||||
|
||||
void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO, String type);
|
||||
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice);
|
||||
void createOrUpdatePaymentInfoForStripe(Session session);
|
||||
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge);
|
||||
Map<String, String> getPaymentMethodInfo(String sessionId, String subscriptionId);
|
||||
|
||||
PaymentMethod getPaymentMethodBySubscriptionId(String subscriptionId);
|
||||
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice, Map<String, String> paymentMethodInfo, List<Session.Discount> discounts);
|
||||
|
||||
// PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge);
|
||||
|
||||
List<PaymentInfo> getPaymentInfoByOrderNo(String orderId, String order);
|
||||
|
||||
@@ -35,5 +43,9 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
||||
|
||||
List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode);
|
||||
|
||||
PaymentInfo updatePaymentRefundStatus(Charge charge);
|
||||
// PaymentInfo updatePaymentRefundStatus(Charge charge);
|
||||
|
||||
void updatePaymentRefundStatusByChargeId(Charge charge, String status);
|
||||
|
||||
void updatePaymentRefundStatusByInvoiceId(String invoiceId, String status);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ public interface RabbitMQService {
|
||||
|
||||
void publishMessageToGenerate(String message);
|
||||
|
||||
void publishMessageToGenerateResult(String message);
|
||||
|
||||
void publishMessageToSR(String message);
|
||||
|
||||
Integer getMessageCount(String queueUrl);
|
||||
|
||||
@@ -24,10 +24,18 @@ public interface RefundInfoService extends IService<RefundInfo> {
|
||||
|
||||
List<RefundInfo> getByChargeId(String chargeId);
|
||||
|
||||
RefundInfo getByRefundId(String refundId);
|
||||
|
||||
RefundInfo createRefundForStripe(Refund refund);
|
||||
|
||||
RefundInfo updateRefundStatusForStripe(Refund refund);
|
||||
|
||||
RefundInfo updateRefundForStripe(Charge charge);
|
||||
|
||||
RefundInfo handleRefundCreated(Refund refund);
|
||||
|
||||
RefundInfo handleRefundSucceeded(Refund refund);
|
||||
|
||||
RefundInfo handleRefundFailed(Refund refund);
|
||||
|
||||
}
|
||||
|
||||
@@ -21,42 +21,20 @@ public interface StripeService {
|
||||
|
||||
SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId);
|
||||
|
||||
String refund(String amount, String orderId, String reason);
|
||||
|
||||
void checkOrderStatus(String orderNo);
|
||||
|
||||
List<String> getSubscriptionIds(String name, String userEmail) throws StripeException;
|
||||
|
||||
Map<String, String> getPaymentMethodByInvoiceId(String invoiceId);
|
||||
|
||||
void cancelSubscription(String orderNo, String cancelReason);
|
||||
|
||||
void cancelSubscriptionTemp(String subscriptionId);
|
||||
|
||||
Map<String, String> getPaymentMethod(String paymentMethodId);
|
||||
|
||||
boolean sendEmail(String subscriptionId, String type, String orderNo);
|
||||
|
||||
String getLanguage(String language, String country, String type);
|
||||
|
||||
/*void updateSubscription(String subscriptionId);
|
||||
|
||||
void resume(String subscriptionId);*/
|
||||
|
||||
// void subscriptionReminder();
|
||||
|
||||
void checkSubscriptionExpiration();
|
||||
|
||||
String createSubscriptionTemp(String name, String email);
|
||||
|
||||
String changeCustomerPayment(String name, String email);
|
||||
|
||||
boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo);
|
||||
|
||||
List<Map<String,String>> getCustomerPaymentMethod(String name, String email);
|
||||
|
||||
String detachCustomerAllPaymentMethod(String name, String email);
|
||||
|
||||
// Map getIp(HttpServletRequest request);
|
||||
|
||||
String getStackTrace(Exception e, int maxLines);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ai.da.service;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.stripe.model.Subscription;
|
||||
|
||||
/**
|
||||
* Stripe 订阅服务接口
|
||||
*
|
||||
* 订阅事件处理已迁移至策略处理器:
|
||||
* - customer.subscription.created -> CheckoutSessionCompletedHandler
|
||||
* - customer.subscription.updated -> SubscriptionUpdatedHandler
|
||||
* - customer.subscription.deleted -> SubscriptionDeletedHandler
|
||||
* - customer.subscription.trial_will_end -> SubscriptionUpdatedHandler
|
||||
*
|
||||
* 本接口中保留需要被其他组件调用的辅助方法
|
||||
*/
|
||||
public interface StripeSubscriptionService {
|
||||
|
||||
/**
|
||||
* 发送订阅相关邮件
|
||||
* @param subscription Stripe Subscription object (may be null)
|
||||
* @param type 邮件类型
|
||||
* @param orderNo 订单号
|
||||
* @param passedSubscriptionInfo 本地订阅记录 (用于避免事务未提交时重新查询,可为空)
|
||||
*/
|
||||
boolean sendSubscriptionEmail(Subscription subscription, String type, String orderNo,
|
||||
com.ai.da.mapper.primary.entity.SubscriptionInfo passedSubscriptionInfo);
|
||||
|
||||
/**
|
||||
* 发送首次订阅失败邮件
|
||||
*/
|
||||
void sendFailedNewOrderEmail(String orderNo);
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
void cancelSubscription(String subscriptionId, String cancelReason, Long accountId);
|
||||
|
||||
/**
|
||||
* 发送续费失败邮件
|
||||
*/
|
||||
// void sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo);
|
||||
//
|
||||
// /**
|
||||
// * 获取用户最新的订阅信息
|
||||
// */
|
||||
// SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId);
|
||||
//
|
||||
// /**
|
||||
// * 更新订阅取消原因
|
||||
// */
|
||||
// void updateCancelReason(String subscriptionId, String reason);
|
||||
//
|
||||
// /**
|
||||
// * 创建或更新订阅信息
|
||||
// */
|
||||
// SubscriptionInfo createOrUpdateSubscriptionInfo(Subscription subscription);
|
||||
}
|
||||
13
src/main/java/com/ai/da/service/StripeWebhookService.java
Normal file
13
src/main/java/com/ai/da/service/StripeWebhookService.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.ai.da.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public interface StripeWebhookService {
|
||||
|
||||
/**
|
||||
* 处理 Stripe webhook 回调
|
||||
* @param request HTTP 请求
|
||||
* @return true=处理成功(返回200),false=处理失败(返回500,Stripe会重试)
|
||||
*/
|
||||
Boolean notify(HttpServletRequest request);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public interface SubscriptionPlanService extends IService<SubscriptionPlan> {
|
||||
|
||||
void switchSubAccSubscriptionPlan(Long subscriptionPlanId, Long subAccId);
|
||||
|
||||
void activeSubscriptionPlan();
|
||||
void activeSubscriptionPlan(Long planId);
|
||||
|
||||
void expireSubscription();
|
||||
}
|
||||
|
||||
@@ -39,9 +39,10 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -83,6 +84,9 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Resource
|
||||
private AccountMapper accountMapper;
|
||||
|
||||
@@ -132,6 +136,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Resource
|
||||
private com.ai.da.common.security.config.SecurityProperties securityProperties;
|
||||
|
||||
@Resource
|
||||
private UserFollowService userFollowService;
|
||||
|
||||
@@ -237,12 +244,13 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
AccountLoginVO response = CopyUtil.copyObject(account, AccountLoginVO.class);
|
||||
response.setEmail(account.getUserEmail());
|
||||
String token = LocalCacheUtils.getTokenCache(String.valueOf(account.getId()));
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
/*if (StringUtils.isNotBlank(token)) {
|
||||
//用户已登入
|
||||
response.setToken(token);
|
||||
} else {
|
||||
response.setToken(createAccountToken(account));
|
||||
}
|
||||
}*/
|
||||
response.setToken(createAccountToken(account));
|
||||
response.setUserId(account.getId());
|
||||
response.setSystemUser(account.getSystemUser());
|
||||
// 设置头像
|
||||
@@ -276,7 +284,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
// 定义常量(临时)
|
||||
private static final Integer SYSTEM_USER_TYPE_EDU_ADMIN = 7;
|
||||
|
||||
private void validateUserValidaExpire(Account account) {
|
||||
public void validateUserValidaExpire(Account account) {
|
||||
Long currentTime = new Date().getTime();
|
||||
if (account.getSystemUser().equals(0)) {
|
||||
return;
|
||||
@@ -294,7 +302,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if (isEduAdmin) {
|
||||
setEduAdminToExpire(account);
|
||||
} else {
|
||||
toVisitor(account);
|
||||
// 这里调用代理的 toVisitor 方法
|
||||
AccountService proxy = applicationContext.getBean(AccountService.class);
|
||||
proxy.toVisitor(account);
|
||||
// return;
|
||||
throw new BusinessException("account.expired", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
@@ -344,7 +354,11 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
principal.setLanguage(account.getLanguage());
|
||||
principal.setCountry(account.getCountry());
|
||||
String token2 = jwtTokenHelper.createToken(principal);
|
||||
// 本地 JVM 缓存(适配旧逻辑)
|
||||
LocalCacheUtils.setTokenCache(String.valueOf(account.getId()), token2);
|
||||
// 同步写入 Redis,重启后仍然可用
|
||||
long jwtExpiration = securityProperties.getJwtExpiration();
|
||||
redisUtil.setLoginToken(account.getId(), token2, jwtExpiration);
|
||||
return token2;
|
||||
}
|
||||
|
||||
@@ -600,21 +614,25 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
|
||||
@Override
|
||||
public Boolean logout(AccountLogoutDTO accountLogoutDTO) {
|
||||
//jwt本身失效比较难做 统一用缓存实现 删除缓存就失效
|
||||
String token = LocalCacheUtils.getTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
LocalCacheUtils.delTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
|
||||
}
|
||||
// jwt 本身失效比较难做,统一用缓存实现:删除缓存即失效
|
||||
String userIdStr = String.valueOf(accountLogoutDTO.getUserId());
|
||||
LocalCacheUtils.delTokenCache(userIdStr);
|
||||
// 同时删除 Redis 中的 token,防止服务重启后仍然有效
|
||||
redisUtil.deleteLoginToken(accountLogoutDTO.getUserId());
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean isLogin(AccountLogoutDTO accountLogoutDTO) {
|
||||
String token = LocalCacheUtils.getTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
|
||||
String userIdStr = String.valueOf(accountLogoutDTO.getUserId());
|
||||
// 先查本地缓存
|
||||
String token = LocalCacheUtils.getTokenCache(userIdStr);
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
return Boolean.FALSE;
|
||||
// 本地没有时,再查 Redis,保证服务重启后也能判断登录状态
|
||||
String redisToken = redisUtil.getLoginToken(accountLogoutDTO.getUserId());
|
||||
return StringUtils.isNotBlank(redisToken);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -812,6 +830,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
LocalCacheUtils.delTokenCache(String.valueOf(accountDelete.getId()));
|
||||
}
|
||||
// 删除 Redis 中对应的登录 token
|
||||
redisUtil.deleteLoginToken(accountDelete.getId());
|
||||
if (!userName.equals(userToBeUpdate.getUserName())) {
|
||||
// accountMapper.deleteById(accountDelete);
|
||||
log.info("排查用户被删除原因:deleteNoLoginRequired,true, 删除用户(改为降为游客)");
|
||||
@@ -1065,6 +1085,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
LocalCacheUtils.delTokenCache(String.valueOf(account.getId()));
|
||||
}
|
||||
// 删除 Redis 中对应的登录 token
|
||||
redisUtil.deleteLoginToken(account.getId());
|
||||
// accountMapper.deleteById(account.getId());
|
||||
log.info("排查用户被删除原因:deleteNoLoginRequiredNew,删除用户(改为降为游客)");
|
||||
accountMapper.toVisitor(account.getId());
|
||||
@@ -1269,7 +1291,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
account.setSystemUser(3);
|
||||
account.setIsTrial(1);
|
||||
account.setCredits(BigDecimal.valueOf(50));
|
||||
account.setValidEndTime(toDayEnd(Instant.now().plus(5, ChronoUnit.DAYS).toEpochMilli()));
|
||||
// 广场用户试用延长至7天
|
||||
account.setValidEndTime(toDayEnd(Instant.now().plus(7, ChronoUnit.DAYS).toEpochMilli()));
|
||||
account.setIsBeginner(1);
|
||||
account.setValidStartTime(Instant.now().toEpochMilli());
|
||||
account.setCreateDate(new Date());
|
||||
@@ -1623,6 +1646,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
log.warn("当前用户 {} 在AiDA中没有账号", email);
|
||||
throw new BusinessException("user.has.no.account", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
// 解决循环依赖问题
|
||||
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
||||
// 2、先判断当前用户是否已经填写过问卷
|
||||
CreditsDetail record = creditsService.getByAccountIdAndChangeEvent(account.getId(), "Fill out the questionnaire", "+100");
|
||||
@@ -1933,6 +1957,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
return baseMapper.selectList(queryWrapper);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void toVisitor(Account account) {
|
||||
accountMapper.toVisitor(account.getId());
|
||||
}
|
||||
@@ -2421,6 +2446,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
Account adminAcc = accountMapper.selectById(authPrincipalVo.getId());
|
||||
int subUserRole = getSubUserRole(adminAcc.getSystemUser());
|
||||
if (Objects.isNull(adminAcc.getSubscriptionPlanId())) {
|
||||
throw new BusinessException("relate.to.any.subscription");
|
||||
}
|
||||
|
||||
if (addSubAccountDTO.getId() == null) {
|
||||
return createSubAccount(addSubAccountDTO, adminAcc, subUserRole);
|
||||
@@ -2488,7 +2516,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
}
|
||||
|
||||
// 判断当前账号的有效期是否与管理员同步
|
||||
if (subAccount.getValidEndTime() < adminAcc.getValidEndTime()){
|
||||
if (Objects.isNull(subAccount.getValidEndTime()) || subAccount.getValidEndTime() < adminAcc.getValidEndTime()){
|
||||
subAccount.setValidEndTime(adminAcc.getValidEndTime());
|
||||
}
|
||||
|
||||
@@ -3175,6 +3203,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
}
|
||||
|
||||
response.setUsernameModify(getNicknameModifyTimes());
|
||||
response.setOrganizationId(account.getOrganizationId());
|
||||
response.setOrganizationName(account.getOrganizationName());
|
||||
response.setSubscriptionPlanId(account.getSubscriptionPlanId());
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -3352,12 +3383,14 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
Account account = accountMapper.selectById(accountId);
|
||||
if (!Objects.isNull(account.getValidEndTime())
|
||||
&& account.getValidEndTime().equals(currentPeriodEnd * 1000)) {
|
||||
log.info("accountId:{}未更新账号有效期。current validEnd:{}, new validEnd:{}", accountId, account.getValidEndTime(), currentPeriodEnd);
|
||||
return false;
|
||||
} else {
|
||||
account.setValidEndTime(currentPeriodEnd * 1000);
|
||||
accountMapper.updateById(account);
|
||||
log.info("accountId:{} 将账号有效期更新到 {}", accountId, currentPeriodEnd);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3370,34 +3403,36 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if (description.equals(ProductEnum.DailySubscription.getName())) {
|
||||
productCredits = ProductEnum.DailySubscription.getCredits();
|
||||
account.setSystemUser(3);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_WEEKLY.getValue())));
|
||||
} else if (description.equals(ProductEnum.MonthlySubscription.getName())) {
|
||||
productCredits = ProductEnum.MonthlySubscription.getCredits();
|
||||
account.setSystemUser(2);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_MONTHLY.getValue())));
|
||||
} else if (description.equals(ProductEnum.Eco_MonthlySubscription.getName())) {
|
||||
productCredits = ProductEnum.Eco_MonthlySubscription.getCredits();
|
||||
account.setSystemUser(2);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_MONTHLY_ECO.getValue())));
|
||||
} else if (description.equals(ProductEnum.AnnualSubscription.getName())) {
|
||||
productCredits = ProductEnum.AnnualSubscription.getCredits();
|
||||
account.setSystemUser(1);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_YEARLY.getValue())));
|
||||
} else {
|
||||
log.error("未知订阅类型: {}", description);
|
||||
return;
|
||||
}
|
||||
account.setCredits(BigDecimal.valueOf(productCredits));
|
||||
accountMapper.updateById(account);
|
||||
log.info("accountId:{},更新用户角色为{},总积分为{}", accountId, account.getSystemUser(), productCredits);
|
||||
|
||||
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
||||
// 先判断是否已添加添加积分变更记录
|
||||
CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo);
|
||||
// 添加积分变更记录(订单续订时的积分变更也需要记录)
|
||||
creditsService.insertToCreditsDetail(accountId,
|
||||
description + "--Stripe",
|
||||
String.valueOf(productCredits),
|
||||
"set", orderNo);
|
||||
/*CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo);
|
||||
if (Objects.isNull(creditsDetail)) {
|
||||
creditsService.insertToCreditsDetail(accountId,
|
||||
description + "--Stripe",
|
||||
String.valueOf(productCredits),
|
||||
"positive", orderNo);
|
||||
}
|
||||
}*/
|
||||
} else {
|
||||
log.error("orderNo: {} 无法找到对应的记录", orderNo);
|
||||
}
|
||||
@@ -3547,7 +3582,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
// 只有当前子账号数量为0时允许批量上传
|
||||
QueryWrapper<Account> qw = new QueryWrapper<>();
|
||||
qw.lambda().eq(Account::getSystemUser, 8)
|
||||
.eq(Account::getOrganizationId, parent.getOrganizationId());
|
||||
.eq(Account::getOrganizationId, parent.getOrganizationId())
|
||||
.eq(Account::getSubscriptionPlanId, parent.getSubscriptionPlanId());
|
||||
List<Account> accounts = accountMapper.selectList(qw);
|
||||
if (!accounts.isEmpty()) {
|
||||
throw new BusinessException("permit.bulk.creation", ResultEnum.PROMPT.getCode());
|
||||
|
||||
@@ -34,7 +34,6 @@ import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
@@ -80,9 +79,7 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
affiliate.setPromotionMethod(promotionMethod);
|
||||
baseMapper.insert(affiliate);
|
||||
// 邮件通知审批者
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||
}else {
|
||||
@@ -440,9 +437,7 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
affiliateEmailParamsDTO.setUnpaidEarnings(String.valueOf(unpaidCommission));
|
||||
affiliateEmailParamsDTO.setPaidEarnings(String.valueOf(paidCommission));
|
||||
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
// 邮件通知
|
||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary");
|
||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), affiliateEmailParamsDTO, "summary");
|
||||
@@ -607,4 +602,8 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
coupon.setUnpaidCommission(unpaidCommission);
|
||||
}
|
||||
|
||||
private String[] buildMerchantReceiverEmail() {
|
||||
return SendEmailUtil.buildMerchantReceiverEmail();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -520,14 +520,16 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
||||
.filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName()))
|
||||
.map(DesignCollectionPrintElementDTO::getId)
|
||||
.collect(Collectors.toList());
|
||||
List<CollectionElement> printBoardElements = new ArrayList<>();
|
||||
elementVO.setPrintBoardElements(printBoardElements);
|
||||
if (!CollectionUtils.isEmpty(printBoardIds)) {
|
||||
// 从数据库批量查询printBoard元素
|
||||
List<CollectionElement> printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds);
|
||||
printBoardElements.addAll(collectionElementMapper.selectBatchIds(printBoardIds));
|
||||
// 验证查询结果的完整性
|
||||
if (CollectionUtil.isEmpty(printBoardElements) || printBoardElements.size() != printBoardIds.size()) {
|
||||
throw new BusinessException("get.printBoards.data.is.mismatch");
|
||||
}
|
||||
elementVO.setPrintBoardElements(printBoardElements);
|
||||
// elementVO.setPrintBoardElements(printBoardElements);
|
||||
usedElementIds.addAll(printBoardIds); // 记录已使用的元素ID
|
||||
}
|
||||
// 处理类型为LIBRARY的printBoard元素
|
||||
@@ -543,7 +545,8 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
||||
Map<Long, DesignCollectionPrintElementDTO> idToMap = designDTO.getPrintBoards()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v));
|
||||
libraryCollectionElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap));
|
||||
printBoardElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap));
|
||||
// libraryCollectionElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +562,8 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
||||
Map<Long, DesignCollectionPrintElementDTO> idToMap = designDTO.getPrintBoards()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v));
|
||||
generateCollectionElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap));
|
||||
printBoardElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap));
|
||||
// generateCollectionElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.xssf.usermodel.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -149,7 +148,17 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
|| ADMIN_IDS.contains(account.getId())
|
||||
|| ADMIN_IDS_READ_ONLY.contains(account.getId())
|
||||
)) {
|
||||
if (StringUtil.isNullOrEmpty(startTime)) startTime = "2024-02-01 00:00:00";
|
||||
boolean filterBySecond ;
|
||||
if (StringUtil.isNullOrEmpty(startTime)) {
|
||||
startTime = "2024-02-01 00:00:00";
|
||||
filterBySecond = true;
|
||||
} else {
|
||||
LocalDateTime thresholdTime = LocalDateTime.of(2024, 5, 1, 0, 0, 0);
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(startTime, formatter);
|
||||
filterBySecond = startDateTime.isBefore(thresholdTime);
|
||||
}
|
||||
|
||||
if (StringUtil.isNullOrEmpty(endTime)) {
|
||||
// yyyy-MM-dd HH:mm:ss "HH"表示24小时制 "hh"表示12小时制
|
||||
endTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
@@ -173,7 +182,7 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
default:
|
||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
return designMapper.getDesignStatistic(startTime, endTime, ids, email, role, account.getOrganizationName());
|
||||
return designMapper.getDesignStatistic(startTime, endTime, ids, email, role, account.getOrganizationName(), filterBySecond);
|
||||
} else {
|
||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
@@ -695,14 +704,19 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
|
||||
String username = UserContext.getUserHolder().getUsername();
|
||||
Account account = new Account();
|
||||
account.setId(accountId);
|
||||
// 修改用户有效期截止日期、用户类型、积分
|
||||
if (!Objects.isNull(validEndTime)) {
|
||||
account.setValidEndTime(validEndTime);
|
||||
log.info("管理员:{},修改用户 {} 信息,将账号到期时间置为:{}", username, accountId, validEndTime);
|
||||
}
|
||||
if (!Objects.isNull(systemUser)) {
|
||||
if (!Objects.isNull(systemUser) && !systemUser.equals(0)) {
|
||||
account.setSystemUser(systemUser);
|
||||
log.info("管理员:{},修改用户 {} 信息,将账号身份置为:{}", username, accountId, systemUser);
|
||||
} else if (systemUser.equals(0)){
|
||||
// 将用户身份设置为游客
|
||||
accountService.toVisitor(account);
|
||||
return true;
|
||||
}
|
||||
/*if (!StringUtils.isNullOrEmpty(systemUser)) {
|
||||
int systemUser = 0;
|
||||
@@ -728,7 +742,6 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
}
|
||||
|
||||
// todo 如果修改管理员账号的积分上限或子账号数量,则其所有子账号的积分上限需要重新计算
|
||||
account.setId(accountId);
|
||||
account.setUpdateDate(new Date());
|
||||
return accountMapper.updateById(account) == 1;
|
||||
// accountService.update(account,null);
|
||||
@@ -774,6 +787,14 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
queryWrapper.lt("create_date", queryUserConditionsVO.getEndTime());
|
||||
}
|
||||
|
||||
if (!Objects.isNull(queryUserConditionsVO.getSubscriptionPlanId())) {
|
||||
queryWrapper.eq("subscription_plan_id", queryUserConditionsVO.getSubscriptionPlanId());
|
||||
}
|
||||
|
||||
if (!Objects.isNull(queryUserConditionsVO.getOrganizationId())) {
|
||||
queryWrapper.eq("organization_id", queryUserConditionsVO.getOrganizationId());
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (!StringUtils.isNullOrEmpty(queryUserConditionsVO.getOrder()) && !StringUtils.isNullOrEmpty(queryUserConditionsVO.getOrderBy())) {
|
||||
String orderBy = "id";
|
||||
@@ -798,27 +819,46 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
return accountMapper.selectPage(new Page<>(queryUserConditionsVO.getPage(), queryUserConditionsVO.getSize()), queryWrapper);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getAllUserIdList() {
|
||||
public IPage<Map<String, Object>> getAllUserIdList(Integer pageNum, Integer pageSize, String email) {
|
||||
Long accountId = UserContext.getUserHolder().getId();
|
||||
Account account = accountMapper.selectById(accountId);
|
||||
// 允许查看数据的用户id
|
||||
if (Objects.nonNull(account.getSystemUser())
|
||||
&& (account.getSystemUser().equals(5)
|
||||
|| account.getSystemUser().equals(7)
|
||||
|| ADMIN_IDS.contains(account.getId())
|
||||
|| ADMIN_IDS_READ_ONLY.contains(account.getId())
|
||||
)) {
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.select("id as value, user_name as label");
|
||||
if ((account.getSystemUser().equals(7) || account.getSystemUser().equals(5))
|
||||
&& !StringUtil.isNullOrEmpty(account.getOrganizationName())) {
|
||||
queryWrapper.lambda().eq(Account::getOrganizationName, account.getOrganizationName());
|
||||
}
|
||||
return accountMapper.selectMaps(queryWrapper);
|
||||
} else {
|
||||
|
||||
// 权限校验
|
||||
if (Objects.isNull(account.getSystemUser())
|
||||
|| (!account.getSystemUser().equals(5)
|
||||
&& !account.getSystemUser().equals(7)
|
||||
&& !ADMIN_IDS.contains(account.getId())
|
||||
&& !ADMIN_IDS_READ_ONLY.contains(account.getId()))) {
|
||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
// return maps.stream().map(map -> (Long)map.get("id")).collect(Collectors.toList());
|
||||
|
||||
// 创建分页对象
|
||||
Page<Account> page = new Page<>(pageNum, pageSize);
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.select("id", "user_name", "user_email");
|
||||
|
||||
if ((account.getSystemUser().equals(7) || account.getSystemUser().equals(5))
|
||||
&& !StringUtil.isNullOrEmpty(account.getOrganizationName())) {
|
||||
queryWrapper.lambda().eq(Account::getOrganizationName, account.getOrganizationName());
|
||||
}
|
||||
|
||||
if (!StringUtil.isNullOrEmpty(email)) {
|
||||
queryWrapper.lambda().like(Account::getUserEmail, email);
|
||||
}
|
||||
|
||||
// 执行分页查询
|
||||
IPage<Account> accountPage = accountMapper.selectPage(page, queryWrapper);
|
||||
|
||||
// 转换为 IPage<Map> 并重命名字段
|
||||
return accountPage.convert(acc -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("value", acc.getId());
|
||||
map.put("label", acc.getUserName());
|
||||
map.put("email", acc.getUserEmail());
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -838,19 +878,20 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
if (!StringUtil.isNullOrEmpty(queryPaymentInfoDTO.getOrder()) && queryPaymentInfoDTO.getOrder().equals("ASC")) {
|
||||
order = "ASC";
|
||||
}
|
||||
String status = StringUtil.isNullOrEmpty(queryPaymentInfoDTO.getStatus()) ? "Success" : queryPaymentInfoDTO.getStatus();
|
||||
List<PaymentInfoVO> paymentInfoVOS = paymentInfoMapper.queryPaymentInfo(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
||||
queryPaymentInfoDTO.getType(), status,
|
||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(),
|
||||
size, offset, order, queryPaymentInfoDTO.getPayer());
|
||||
// 查询数据总量
|
||||
Long total = paymentInfoMapper.queryPaymentInfoCount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
||||
queryPaymentInfoDTO.getType(), status,
|
||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
||||
// 查询符合查询条件的总金额
|
||||
BigDecimal payerTotal = paymentInfoMapper.queryTotalPaymentAmount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
||||
queryPaymentInfoDTO.getType(), status,
|
||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
||||
// 总页数
|
||||
|
||||
@@ -83,7 +83,7 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
*
|
||||
* @param changeEvent 导致积分变更的事件
|
||||
* @param credits 变更的积分
|
||||
* @param changeType 变更类型 : positive->增 negative->减
|
||||
* @param changeType 变更类型 : positive->增 negative->减 set->重置
|
||||
*/
|
||||
@Override
|
||||
public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo) {
|
||||
@@ -94,9 +94,11 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
if ("positive".equals(changeType)) {
|
||||
// finalCredits = account.getCredits().add(new BigDecimal(credits));
|
||||
changeCredits = "+" + credits;
|
||||
} else {
|
||||
} else if ("negative".equals(changeType)) {
|
||||
// finalCredits = account.getCredits().subtract(new BigDecimal(credits));
|
||||
changeCredits = "-" + credits;
|
||||
} else {
|
||||
changeCredits = credits;
|
||||
}
|
||||
creditsDetail.setAccountId(accountId);
|
||||
creditsDetail.setChangeEvent(changeEvent);
|
||||
@@ -107,6 +109,7 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
creditsDetail.setCreateTime(LocalDateTime.now());
|
||||
|
||||
baseMapper.insert(creditsDetail);
|
||||
log.info("creditsDetail inserted");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -269,9 +272,11 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
BigDecimal existingCredits = account.getCredits();
|
||||
BigDecimal subtract = existingCredits.subtract(new BigDecimal(value));
|
||||
BigDecimal creditsUsage = null;
|
||||
if (!StringUtil.isNullOrEmpty(account.getOrganizationName())){
|
||||
// if (!StringUtil.isNullOrEmpty(account.getOrganizationName())){
|
||||
if (Objects.nonNull(account.getSubscriptionPlanId())){
|
||||
creditsUsage = Objects.isNull(account.getCreditsUsage()) ? BigDecimal.ZERO : account.getCreditsUsage();
|
||||
creditsUsage = creditsUsage.add(new BigDecimal(value));
|
||||
BigDecimal added = creditsUsage.add(new BigDecimal(value));
|
||||
creditsUsage = added.compareTo(account.getCreditsUsageLimit()) > 0 ? account.getCreditsUsageLimit() : added;
|
||||
}
|
||||
accountService.updateCreditsAndEndTime(account, subtract.toString(), null, creditsUsage);
|
||||
|
||||
|
||||
@@ -363,9 +363,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
public List<TDesignPythonOutfitDetail> saveDesignSingleItemDetailAndLayers(DesignPythonObjects pythonObjects
|
||||
, Long designId, Long designItemId, Long userId
|
||||
, JSONObject outfit, String timeZone, List<DesignSingleItemDTO> designSingleItemDTOList
|
||||
, Map<String, List<String>> priorityAndUndividedLayer
|
||||
/*, Map<String, List<String>> priorityAndUndividedLayer*/
|
||||
, boolean changeModelFlag
|
||||
, Long modelId, String modelType, boolean isSingleCollectionFlag) {
|
||||
, Long modelId, String modelType, boolean isSingleCollectionFlag, String designType) {
|
||||
|
||||
DesignItem designItem = new DesignItem();
|
||||
// String url = pythonObjects.getObjects().get(0).getBasic().getSave_name();
|
||||
@@ -424,11 +424,15 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
designItemDetail.setColor(detail.getColor());
|
||||
designItemDetail.setIconPath(detail.getIcon());
|
||||
// designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getType().toLowerCase()));
|
||||
/*// 取消存储UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||
if (!detail.getType().equals("Body")) {
|
||||
designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(0));
|
||||
designItemDetail.setUndividedLayerWithSinglePrint(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(1));
|
||||
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(0))) {
|
||||
designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getPriority().toString()).getFirst());
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(1))) {
|
||||
designItemDetail.setUndividedLayerWithSinglePrint(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(1));
|
||||
}
|
||||
}*/
|
||||
// 印花存储在design_item_detail_print表中 这里还要存吗?
|
||||
// DesignPythonItemPrint printObject = detail.getPrintToPython();
|
||||
// designItemDetail.setPrintPath(Objects.isNull(printObject) ? "" : printObject.getPath());
|
||||
@@ -436,7 +440,18 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
// designItemDetail.setPrintJson(JSON.toJSONString(printObject));
|
||||
|
||||
designItemDetail.setPartialDesign(Objects.isNull(detail.getPrint()) ? null : detail.getPrint().getPartial());
|
||||
designItemDetail.setGradientString(detail.getGradientString());
|
||||
|
||||
designItemDetails.add(designItemDetail);
|
||||
|
||||
// 处理gradientString为null的情况,强制更新为null
|
||||
if (Objects.isNull(detail.getGradientString()) && Objects.nonNull(designItemDetail.getId())) {
|
||||
UpdateWrapper<DesignItemDetail> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", designItemDetail.getId());
|
||||
updateWrapper.set("gradient_string", null);
|
||||
updateWrapper.set("update_date", DateUtil.getByTimeZone(timeZone));
|
||||
designItemDetailService.update(null, updateWrapper);
|
||||
}
|
||||
});
|
||||
|
||||
// 逻辑删除未复用的旧记录
|
||||
@@ -481,6 +496,11 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
priorityOffset = designSingleItemDTOList.stream()
|
||||
.collect(Collectors.toMap(DesignSingleItemDTO::getPriority, DesignSingleItemDTO::getOffset));
|
||||
}
|
||||
|
||||
// 创建 priority 到 DesignSingleItemDTO 的映射,用于获取 transpose 和 rotate
|
||||
Map<Integer, DesignSingleItemDTO> priorityToItemDTOMap = designSingleItemDTOList.stream()
|
||||
.collect(Collectors.toMap(DesignSingleItemDTO::getPriority, dto -> dto, (old, newVal) -> old));
|
||||
|
||||
List<TDesignPythonOutfitDetail> list = new ArrayList<>();
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
JSONObject jsonObject = layers.getJSONObject(i);
|
||||
@@ -509,6 +529,12 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
designPythonOutfitDetail.setOffset(String.valueOf(priorityOffset.get(Math.abs(priority))));
|
||||
}
|
||||
designPythonOutfitDetail.setPriority(priority);
|
||||
// 从前端传入的 DesignSingleItemDTO 中获取 transpose 和 rotate,不再从 Python 返回的数据获取
|
||||
DesignSingleItemDTO itemDTO = priorityToItemDTOMap.get(Math.abs(priority));
|
||||
if (itemDTO != null) {
|
||||
designPythonOutfitDetail.setTranspose(itemDTO.getTranspose() != null ? Arrays.toString(itemDTO.getTranspose()) : null);
|
||||
designPythonOutfitDetail.setRotate(itemDTO.getRotate());
|
||||
}
|
||||
|
||||
list.add(designPythonOutfitDetail);
|
||||
}
|
||||
@@ -683,6 +709,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DesignSingleVO designSingleIncludeLayers(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO) {
|
||||
if (!designSingleIncludeLayersDTO.getDesignType().equals("merge") && !designSingleIncludeLayersDTO.getDesignType().equals("default")) {
|
||||
throw new BusinessException("The value of DesignType can only be 'default' or 'merge' ");
|
||||
}
|
||||
// 记录入参 base64数据太长,所以这里去掉
|
||||
DesignSingleIncludeLayersDTO clone = SerializationUtils.clone(designSingleIncludeLayersDTO);
|
||||
clone.getDesignSingleItemDTOList().forEach(i -> {
|
||||
@@ -706,6 +735,17 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
log.info("set partialDesignBase64为空,便于日志打印");
|
||||
i.getPartialDesign().setPartialDesignBase64(null);
|
||||
}
|
||||
// undividedLayerBase64 undividedLayerWithSinglePrintBase64 置空
|
||||
/*// 前端合成的未分割的图
|
||||
if (!StringUtil.isNullOrEmpty(i.getUndividedLayerBase64())) {
|
||||
log.info("set UndividedLayerBase64为空,便于日志打印");
|
||||
i.setUndividedLayerBase64(null);
|
||||
}
|
||||
// 前端合成的未分割的图
|
||||
if (!StringUtil.isNullOrEmpty(i.getUndividedLayerWithSinglePrintBase64())) {
|
||||
log.info("set UndividedLayerWithSinglePrintBase64为空,便于日志打印");
|
||||
i.setUndividedLayerWithSinglePrintBase64(null);
|
||||
}*/
|
||||
});
|
||||
|
||||
log.info("designSingle request入参 ==> " + JSONObject.toJSONString(clone));
|
||||
@@ -800,7 +840,7 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
maskBase64ToPath(designSingleIncludeLayersDTO, setNull);
|
||||
// maskBase64ToPath(designSingleIncludeLayersDTO, Boolean.TRUE);
|
||||
|
||||
partialDesignBase64ToImage(designSingleIncludeLayersDTO, userId, designSingleIncludeLayersDTO.getIsPreview());
|
||||
partialDesignBase64ToImage(designSingleIncludeLayersDTO, userId, designSingleIncludeLayersDTO.getIsPreview(), designSingleIncludeLayersDTO.getDesignType());
|
||||
|
||||
// 组装入参
|
||||
DesignPythonObjects objects = pythonService.covertDesignSingleParam(
|
||||
@@ -818,13 +858,13 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
JSONObject outfit = data.getJSONObject("0");
|
||||
|
||||
JSONArray layers = outfit.getJSONArray("layers");
|
||||
Map<String, List<String>> priorityAndUndividedLayer = setPriorityAndUndividedLayer(layers);
|
||||
// Map<String, List<String>> priorityAndUndividedLayer = setPriorityAndUndividedLayer(layers, designSingleIncludeLayersDTO);
|
||||
if (!designSingleIncludeLayersDTO.getIsPreview()) {
|
||||
// 更新及保存图层信息
|
||||
tDesignPythonOutfitDetails = saveDesignSingleItemDetailAndLayers(objects, design.getId(), designSingleIncludeLayersDTO.getDesignItemId()
|
||||
, userId, outfit, designSingleIncludeLayersDTO.getTimeZone()
|
||||
, designSingleIncludeLayersDTO.getDesignSingleItemDTOList()
|
||||
, priorityAndUndividedLayer, changeModelFlag, modelId, modelType, isSingleCollectionFlag);
|
||||
/*, priorityAndUndividedLayer*/, changeModelFlag, modelId, modelType, isSingleCollectionFlag, designSingleIncludeLayersDTO.getDesignType());
|
||||
|
||||
saveCollectionElement(designSingleIncludeLayersDTO);
|
||||
} else {
|
||||
@@ -858,8 +898,8 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
outfit.getString("synthesis_url"),
|
||||
designSingleIncludeLayersDTO.getDesignSingleItemDTOList(),
|
||||
detailsVO,
|
||||
design.getSingleOverall(),
|
||||
priorityAndUndividedLayer);
|
||||
design.getSingleOverall()/*,
|
||||
priorityAndUndividedLayer*/);
|
||||
}
|
||||
|
||||
// 方法1:仅查询(无事务)
|
||||
@@ -919,12 +959,12 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
item.setMaskUrl(path);
|
||||
}
|
||||
}
|
||||
log.info("服装{} 的maskUrl为null", item.getType());
|
||||
// log.info("服装{} 的maskUrl为null", item.getType());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void partialDesignBase64ToImage(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO, Long accountId, boolean preview) {
|
||||
private void partialDesignBase64ToImage(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO, Long accountId, boolean preview, String designType) {
|
||||
designSingleIncludeLayersDTO.getDesignSingleItemDTOList().forEach(item -> {
|
||||
PartialDesignDTO partialDesignDTO = item.getPartialDesign();
|
||||
if (!Objects.isNull(item.getPartialDesign())
|
||||
@@ -946,21 +986,90 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
item.getPartialDesign().setPartialDesignMinioPath(newPath);
|
||||
} else if (Objects.isNull(item.getPartialDesign())
|
||||
|| StringUtil.isNullOrEmpty(item.getPartialDesign().getPartialDesignMinioPath())) {
|
||||
item.setPartialDesign(new PartialDesignDTO(null));
|
||||
if (designType.equals("merge")) {
|
||||
// 先去数据库进行查找,如果数据库中也是空,则提示需要提供,否则无法生成
|
||||
DesignItemDetail designItemDetail = designItemDetailService.getById(item.getId());
|
||||
if (Objects.isNull(designItemDetail)){
|
||||
log.error("未知designItemDetailId: {}", item.getId());
|
||||
throw new BusinessException("designItemDetails.not.found");
|
||||
} else if (StringUtil.isNullOrEmpty(designItemDetail.getPartialDesign())) {
|
||||
item.setPartialDesign(new PartialDesignDTO(designItemDetail.getUndividedLayer()));
|
||||
/*log.error("merge模式下,必须提供partialDesign");
|
||||
throw new BusinessException("required.partialDesign");*/
|
||||
} else {
|
||||
item.setPartialDesign(new PartialDesignDTO(designItemDetail.getPartialDesign()));
|
||||
}
|
||||
} else {
|
||||
item.setPartialDesign(new PartialDesignDTO(null));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void undividedLayerBase64ToImage(List<DesignSingleItemDTO> designSingleItemDTOS) {
|
||||
designSingleItemDTOS.forEach(item -> {
|
||||
if (!StringUtil.isNullOrEmpty(item.getUndividedLayerBase64())) {
|
||||
if (item.getUndividedLayerBase64().startsWith("data:image") && !item.getUndividedLayerBase64().startsWith("https://")) {
|
||||
// 将原图地址作为修改后的图片地址,放在不同的桶
|
||||
String filename = "image/image_" + UUID.randomUUID();
|
||||
String path = minioUtil.base64UploadToPath(item.getUndividedLayerBase64(), clothingBucket, filename);
|
||||
log.info("undividedLayer, 新的path为{}", path);
|
||||
if (StringUtil.isNullOrEmpty(path)) {
|
||||
log.error("undividedLayer图片base64上传失败");
|
||||
throw new BusinessException("file.upload.fail");
|
||||
}
|
||||
item.setUndividedLayerBase64(path);
|
||||
} else {
|
||||
item.setUndividedLayerBase64(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!StringUtil.isNullOrEmpty(item.getUndividedLayerWithSinglePrintBase64())) {
|
||||
if (item.getUndividedLayerWithSinglePrintBase64().startsWith("data:image") && !item.getUndividedLayerWithSinglePrintBase64().startsWith("https://")) {
|
||||
// 将原图地址作为修改后的图片地址,放在不同的桶
|
||||
String filename = "image/image_" + UUID.randomUUID();
|
||||
String path = minioUtil.base64UploadToPath(item.getUndividedLayerWithSinglePrintBase64(), clothingBucket, filename);
|
||||
log.info("getUndividedLayerWithSinglePrint, 新的path为{}", path);
|
||||
if (StringUtil.isNullOrEmpty(path)) {
|
||||
log.error("getUndividedLayerWithSinglePrintBase64图片base64上传失败");
|
||||
throw new BusinessException("file.upload.fail");
|
||||
}
|
||||
item.setUndividedLayerWithSinglePrintBase64(path);
|
||||
} else {
|
||||
item.setUndividedLayerWithSinglePrintBase64(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers) {
|
||||
HashMap<String, List<String>> priorityAndLayer = new HashMap<>();
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
JSONObject jsonObject = layers.getJSONObject(i);
|
||||
String priority = jsonObject.getString("priority");
|
||||
String category = jsonObject.getString("image_category").split("_")[0];
|
||||
if (!category.equals("body") && !priorityAndLayer.containsKey(priority))
|
||||
priorityAndLayer.put(priority, Arrays.asList(jsonObject.getString("pattern_overall_image_url"), jsonObject.getString("pattern_print_image_url")));
|
||||
public Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers, DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO) {
|
||||
String designType = "default";
|
||||
if (Objects.nonNull(designSingleIncludeLayersDTO)) {
|
||||
designType = designSingleIncludeLayersDTO.getDesignType();
|
||||
}
|
||||
HashMap<String, List<String>> priorityAndLayer = new HashMap<>();
|
||||
if (designType.equals("default")) {
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
JSONObject jsonObject = layers.getJSONObject(i);
|
||||
String priority = jsonObject.getString("priority");
|
||||
String category = jsonObject.getString("image_category").split("_")[0];
|
||||
if (!category.equals("body") && !priorityAndLayer.containsKey(priority)) {
|
||||
// pattern_overall_image_url | pattern_print_image_url 这俩字段来源有俩,merge模式下,来自前端,default模式下,来自python
|
||||
priorityAndLayer.put(priority, Arrays.asList(jsonObject.getString("pattern_overall_image_url"), jsonObject.getString("pattern_print_image_url")));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (designSingleIncludeLayersDTO.getIsPreview()) {
|
||||
// 如果是预览,则不处理、不存储前端传过来的数据
|
||||
return priorityAndLayer;
|
||||
}
|
||||
undividedLayerBase64ToImage(designSingleIncludeLayersDTO.getDesignSingleItemDTOList());
|
||||
for (DesignSingleItemDTO designSingleItemDTO : designSingleIncludeLayersDTO.getDesignSingleItemDTOList()) {
|
||||
priorityAndLayer.put(designSingleItemDTO.getPriority().toString(), Arrays.asList(designSingleItemDTO.getUndividedLayerBase64(), designSingleItemDTO.getUndividedLayerWithSinglePrintBase64()));
|
||||
}
|
||||
}
|
||||
|
||||
return priorityAndLayer;
|
||||
}
|
||||
|
||||
@@ -1075,8 +1184,8 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
String currentFullBodyView,
|
||||
List<DesignSingleItemDTO> designSingleItemDTOList,
|
||||
List<DesignPythonOutfitVO> layersObject,
|
||||
String singleOrOverall,
|
||||
Map<String, List<String>> priorityAndUndividedLayer) {
|
||||
String singleOrOverall/*,
|
||||
Map<String, List<String>> priorityAndUndividedLayer*/) {
|
||||
|
||||
DesignSingleVO designSingleVO = new DesignSingleVO();
|
||||
ArrayList<DesignItemClothesDetailVO> clothes = new ArrayList<>();
|
||||
@@ -1116,10 +1225,15 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
String preSignedUrl = StringUtil.isNullOrEmpty(partialDesignMinioPath) ? null : minioUtil.getPreSignedUrl(partialDesignMinioPath, CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true);
|
||||
designItemClothesDetailVO.setPartialDesign(new PartialDesignDTO(partialDesignMinioPath, preSignedUrl));
|
||||
|
||||
/*// 取消存储/返回UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||
if (priorityAndUndividedLayer.containsKey(singleItem.getPriority().toString())) {
|
||||
designItemClothesDetailVO.setUndividedLayer(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(0), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
designItemClothesDetailVO.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(1), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(0))) {
|
||||
designItemClothesDetailVO.setUndividedLayer(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).getFirst(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(1))) {
|
||||
designItemClothesDetailVO.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(1), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
}
|
||||
}*/
|
||||
body.setLayersObject(layersObject.stream().filter(layers -> layers.getImageCategory().equals("body")).collect(Collectors.toList()));
|
||||
|
||||
clothes.add(designItemClothesDetailVO);
|
||||
@@ -1201,6 +1315,7 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
designItemDetailPrint.setPosition(print.getLocation().toString());
|
||||
designItemDetailPrint.setAngle(print.getAngle());
|
||||
designItemDetailPrint.setPriority(priority);
|
||||
designItemDetailPrint.setObject(print.getObject());
|
||||
|
||||
designItemDetailPrints.add(designItemDetailPrint);
|
||||
});
|
||||
@@ -1294,6 +1409,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
}
|
||||
}
|
||||
|
||||
if (Objects.isNull(designSingleItem.getPrintObject()) || Objects.isNull(designSingleItem.getPrintObject().getPrints())) {
|
||||
return;
|
||||
}
|
||||
// 添加print到library
|
||||
designSingleItem.getPrintObject().getPrints().forEach(print -> {
|
||||
if (!StringUtil.isNullOrEmpty(print.getDesignType()) && print.getDesignType().equals("Collection")) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONException;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -45,6 +46,7 @@ import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
@@ -100,8 +102,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
private final UserLikeGroupService userLikeGroupService;
|
||||
private final UserLikeService userLikeService;
|
||||
private final UserBehaviorMapper userBehaviorMapper;
|
||||
private final UserPreferenceLogMapper userPreferenceLogMapper;
|
||||
private final UserPreferenceMapper userPreferenceMapper;
|
||||
private final WorkspaceService workspaceService;
|
||||
private final WorkspaceRelStyleMapper workspaceRelStyleMapper;
|
||||
|
||||
@Value("${minio.endpoint}")
|
||||
private String endpoint;
|
||||
@@ -713,7 +716,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
(existing, replacement) -> replacement));
|
||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
log.info("all typeLayers Map:{}", typeAndUndividedLayer);
|
||||
Map<String, List<String>> priorityAndUndividedLayer = designItemService.setPriorityAndUndividedLayer(layers);
|
||||
Map<String, List<String>> priorityAndUndividedLayer = designItemService.setPriorityAndUndividedLayer(layers, null);
|
||||
for (DesignPythonItem detail : item.getItems()) {
|
||||
if (null == detail) {
|
||||
continue;
|
||||
@@ -753,7 +756,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
print.setPosition("[0.0,0.0]");
|
||||
// print.setScale(1d);
|
||||
// todo mark 将print默认scale置为0.3
|
||||
print.setScale(Arrays.toString(new Float[]{0.3f, 0.3f}));
|
||||
print.setScale(Arrays.toString(new Float[]{1.0f, 1.0f}));
|
||||
print.setAngle(0.0);
|
||||
print.setPriority(1);
|
||||
QueryWrapper<CollectionElement> getPrintboardLevel2TypeQw = new QueryWrapper<>();
|
||||
@@ -761,7 +764,20 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
getPrintboardLevel2TypeQw.lambda().orderByDesc(CollectionElement::getCreateDate);
|
||||
getPrintboardLevel2TypeQw.last("limit 1");
|
||||
CollectionElement one = collectionElementService.getOne(getPrintboardLevel2TypeQw);
|
||||
print.setLevel2Type(one.getLevel2Type());
|
||||
if (Objects.isNull(one)) {
|
||||
QueryWrapper<Library> libraryQueryWrapper = new QueryWrapper<>();
|
||||
libraryQueryWrapper.lambda().eq(Library::getUrl, print.getPath());
|
||||
libraryQueryWrapper.lambda().orderByDesc(Library::getCreateDate);
|
||||
getPrintboardLevel2TypeQw.last("limit 1");
|
||||
Library library = libraryService.getOne(libraryQueryWrapper);
|
||||
if (Objects.isNull(library)) {
|
||||
print.setLevel2Type("Pattern");
|
||||
} else {
|
||||
print.setLevel2Type(library.getLevel2Type());
|
||||
}
|
||||
} else {
|
||||
print.setLevel2Type(one.getLevel2Type());
|
||||
}
|
||||
print.setCreateDate(LocalDateTime.now());
|
||||
designItemDetailPrintService.save(print);
|
||||
}
|
||||
@@ -851,7 +867,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
Map<String, Integer> typePriority = list.stream().collect(Collectors.toMap(d -> d.getImageCategory().split("_")[0],
|
||||
d -> Math.abs(d.getPriority()),
|
||||
(existing, replacement) -> replacement));
|
||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
// Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
for (DesignPythonItem detail : item.getItems()) {
|
||||
if (null == detail) {
|
||||
continue;
|
||||
@@ -862,9 +878,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
designItemDetail.setDesignItemId(designItemId);
|
||||
designItemDetail.setCollectionElementId(detail.getElementId());
|
||||
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
||||
if (!detail.getType().equals("Body")) {
|
||||
/*if (!detail.getType().equals("Body")) {
|
||||
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
||||
}
|
||||
}*/
|
||||
|
||||
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
||||
designItemDetail.setPath(detail.getBody_path());
|
||||
@@ -1108,18 +1124,20 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
//修改designItem为like状态
|
||||
designItemService.updateLikeStatus(designLikeDTO.getDesignItemId(), (byte) 1);
|
||||
// 记录喜欢的系统sketch
|
||||
addSystemLikeSketch(designItem);
|
||||
addSystemLikeSketch(designItem, designLikeDTO.getProjectId());
|
||||
// 更新项目更新时间
|
||||
projectService.modifyProjectUpdateTime(designLikeDTO.getProjectId());
|
||||
return new DesignLikeVO(userLikeSortId, userGroupId, groupDetailId, pictureName, userLike.getId(), userLikeSort.getSort());
|
||||
}
|
||||
|
||||
private void addSystemLikeSketch(DesignItem designItem) {
|
||||
public void addSystemLikeSketch(DesignItem designItem, Long projectId) {
|
||||
AuthPrincipalVo userHolder = UserContext.getUserHolder();
|
||||
QueryWrapper<DesignItemDetail> qw = new QueryWrapper<>();
|
||||
qw.lambda().eq(DesignItemDetail::getDesignItemId, designItem.getId());
|
||||
qw.lambda().ne(DesignItemDetail::getType, "Body");
|
||||
List<DesignItemDetail> designItemDetails = designItemDetailMapper.selectList(qw);
|
||||
List<WorkspaceRelStyle> workspaceRelStyles = workspaceRelStyleMapper.selectByProjectId(projectId);
|
||||
|
||||
for (DesignItemDetail designItemDetail : designItemDetails) {
|
||||
if (designItemDetail.getPath().startsWith("aida-sys-image")) {
|
||||
String[] split = designItemDetail.getPath().split("/");
|
||||
@@ -1132,16 +1150,34 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
userBehavior.setCreateTime(LocalDateTime.now());
|
||||
userBehaviorMapper.insert(userBehavior);
|
||||
|
||||
UserPreferenceLogTest userPreferenceLogTest = new UserPreferenceLogTest();
|
||||
userPreferenceLogTest.setPath(designItemDetail.getPath());
|
||||
userPreferenceLogTest.setAccountId(userHolder.getId());
|
||||
// userPreferenceLogTest.setUserLikeGroupId(userLike.getUserLikeGroupId());
|
||||
userPreferenceLogTest.setDataTime(designItemDetail.getCreateDate().toInstant()
|
||||
UserPreference userPreference = new UserPreference();
|
||||
userPreference.setPath(designItemDetail.getPath());
|
||||
SysFile sysFile = sysFileService.getOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUrl, designItemDetail.getPath()));
|
||||
userPreference.setAccountId(userHolder.getId());
|
||||
if (sysFile != null) {
|
||||
userPreference.setCategory(sysFile.getLevel3Type().toLowerCase() + "_" + sysFile.getLevel2Type().toLowerCase());
|
||||
userPreference.setStyle(sysFile.getStyle());
|
||||
} else {
|
||||
log.error("sysFile not found:{}", designItemDetail.getPath());
|
||||
SendEmailUtil.commonExceptionReminder("url在sysFile里找不到" + designItemDetail.getPath(), new String[]{"litianxiangxtt@163.com"});
|
||||
|
||||
continue;
|
||||
}
|
||||
userPreference.setDataTime(new Date().toInstant()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime());
|
||||
userPreferenceLogMapper.insert(userPreferenceLogTest);
|
||||
userPreference.setDesignItemId(designItem.getId());
|
||||
userPreference.setProjectId(projectId);
|
||||
if (workspaceRelStyles == null||workspaceRelStyles.isEmpty()) {
|
||||
//查不到记录,style应该是all
|
||||
userPreference.setWorkspaceRelStyleId(0L);
|
||||
} else {
|
||||
userPreference.setWorkspaceRelStyleId(workspaceRelStyles.get(0).getStyleId());
|
||||
}
|
||||
userPreferenceMapper.insert(userPreference);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private List<Long> validateMergeElement(List<CollectionElement> oldElements, List<DesignItemDetail> designItemDetails) {
|
||||
@@ -1309,12 +1345,13 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
d.setScope(o.getPath().startsWith("aida-sys-image") ? "sys" : "user");
|
||||
d.setLevel1Type(converTypeToLevel1(o.getType()));
|
||||
d.setGradient(JSONObject.parseObject(o.getGradientString(), Gradient.class));
|
||||
/*// 取消存储/返回UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||
if (!StringUtil.isNullOrEmpty(o.getUndividedLayer())) {
|
||||
d.setUndividedLayer(minioUtil.getPreSignedUrl(o.getUndividedLayer(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(o.getUndividedLayerWithSinglePrint())) {
|
||||
d.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(o.getUndividedLayerWithSinglePrint(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
||||
}
|
||||
}*/
|
||||
// 根据designItemDetailId获取印花
|
||||
List<DesignItemDetailPrint> prints = designItemDetailPrintService.getByDesignItemDetailId(o.getId(), "print");
|
||||
prints.removeIf(print -> !minioUtil.doesObjectExist(print.getPath()));
|
||||
@@ -1563,6 +1600,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
}
|
||||
}
|
||||
designSinglePrint.setScale(scales);
|
||||
designSinglePrint.setObject(detailPrint.getObject());
|
||||
prints.add(designSinglePrint);
|
||||
} else {
|
||||
// 多个印花
|
||||
@@ -1583,7 +1621,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
scales = Arrays.asList(scale, scale);
|
||||
}
|
||||
}
|
||||
prints.add(new DesignSinglePrint(
|
||||
DesignSinglePrint designSinglePrint = new DesignSinglePrint(
|
||||
print.getLevel2Type(),
|
||||
minioUtil.getPreSignedUrl(print.getPath(), 24 * 60),
|
||||
print.getPath(),
|
||||
@@ -1591,7 +1629,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
scales,
|
||||
print.getAngle(),
|
||||
print.getPriority(),
|
||||
ifSingle));
|
||||
ifSingle);
|
||||
designSinglePrint.setObject(print.getObject());
|
||||
prints.add(designSinglePrint);
|
||||
// }
|
||||
});
|
||||
}
|
||||
@@ -1643,6 +1683,24 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
.lt("create_date", endTime)
|
||||
.select("count(id) as count");
|
||||
|
||||
// 如果startTime早于2024-05-01 00:00:00,添加额外的筛选条件
|
||||
LocalDateTime thresholdTime = LocalDateTime.of(2024, 5, 1, 0, 0, 0);
|
||||
|
||||
try {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(startTime, formatter);
|
||||
|
||||
if (startDateTime.isBefore(thresholdTime)) {
|
||||
// 使用 notLike 来排除以 ":01" 或 ":02" 结尾的时间
|
||||
// 方案2.1:使用 apply 方法执行数据库函数 (create_date 字段的实际存储格式(可能包含毫秒),所以取最后三个字符进行匹配)
|
||||
queryWrapper.apply("DATE_FORMAT(create_date, '%s') NOT IN ('01', '02')");
|
||||
/*queryWrapper.notLike("create_date", "%:01")
|
||||
.notLike("create_date", "%:02");*/
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse startTime: {}, skip time-based filtering", startTime, e);
|
||||
}
|
||||
|
||||
if (!Objects.isNull(accountIds) && !accountIds.isEmpty()) {
|
||||
queryWrapper.in("account_id", accountIds);
|
||||
}
|
||||
@@ -1650,7 +1708,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
List<Map<String, Object>> result = baseMapper.selectMaps(queryWrapper);
|
||||
if (result != null && !result.isEmpty()) {
|
||||
Object countObj = result.get(0).get("count");
|
||||
return (Long) countObj;
|
||||
return countObj != null ? ((Number) countObj).longValue() : 0L;
|
||||
} else {
|
||||
return 0L;
|
||||
}
|
||||
@@ -2519,7 +2577,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
Map<String, Integer> typePriority = list.stream().collect(Collectors.toMap(d -> d.getImageCategory().split("_")[0],
|
||||
d -> Math.abs(d.getPriority()),
|
||||
(existing, replacement) -> replacement));
|
||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
// Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
for (DesignPythonItem detail : item.getItems()) {
|
||||
if (null == detail) {
|
||||
continue;
|
||||
@@ -2530,9 +2588,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
designItemDetail.setDesignItemId(designItemId);
|
||||
designItemDetail.setCollectionElementId(detail.getElementId());
|
||||
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
||||
if (!detail.getType().equals("Body")) {
|
||||
/*if (!detail.getType().equals("Body")) {
|
||||
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
||||
}
|
||||
}*/
|
||||
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
||||
designItemDetail.setPath(detail.getBody_path());
|
||||
//BODY不关联businessId
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ai.da.common.response.ResultEnum;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.ai.da.common.utils.MailUtil;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
import com.ai.da.common.utils.SendEmailUtil;
|
||||
import com.ai.da.mapper.primary.AccountMapper;
|
||||
import com.ai.da.mapper.primary.EmailLogMapper;
|
||||
import com.ai.da.mapper.primary.EmailTemplateMapper;
|
||||
@@ -585,9 +586,7 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
public boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||
try {
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
List<String> merchantReceiver = Arrays.asList(/*merchantEmail,*/ developer);
|
||||
List<String> merchantReceiver = buildMerchantReceiverList();
|
||||
|
||||
String merchantSubject = null;
|
||||
String merchantTemplate = null;
|
||||
@@ -723,15 +722,13 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
private final static String CREDITS_PURCHASE_MERCHANT = "133275_AiDA 积分购买通知-merchant.html";
|
||||
public void creditsPurchaseReminder(String username, String quantity, String amount) {
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developerEmail = "xupei@code-create.com.hk";
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
// 设置试用订单相关数据
|
||||
jsonObject.put("username", username);
|
||||
jsonObject.put("quantity", quantity);
|
||||
jsonObject.put("totalFee", amount);
|
||||
|
||||
sendEmail(Arrays.asList(/*merchantEmail,*/ developerEmail), jsonObject, CREDITS_PURCHASE_MERCHANT, "New Credit Purchase Order", null, null);
|
||||
sendEmail(buildMerchantReceiverList(), jsonObject, CREDITS_PURCHASE_MERCHANT, "New Credit Purchase Order", null, null);
|
||||
}
|
||||
|
||||
private final static String COMMON_EXCEPTION_REMINDER = "135279_common-exception-reminder.html";
|
||||
@@ -742,6 +739,10 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
sendEmail(destination, param, COMMON_EXCEPTION_REMINDER, "AiDA发生异常,请及时处理", null, null);
|
||||
}
|
||||
|
||||
private List<String> buildMerchantReceiverList() {
|
||||
return Arrays.asList(SendEmailUtil.buildMerchantReceiverEmail());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
@@ -59,10 +61,12 @@ import org.bytedeco.javacv.Java2DFrameConverter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
@@ -194,10 +198,13 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
generate.setText(text);
|
||||
Long elementId = generateThroughImageTextDTO.getCollectionElementId();
|
||||
// validateGeneraType(generate, text, elementId);
|
||||
if (!StringUtil.isNullOrEmpty(text)) {
|
||||
text = modifyPrompt(text, generate, generateThroughImageTextDTO.getLevel1Type(), generateThroughImageTextDTO.getAgeGroup());
|
||||
if (!(generateThroughImageTextDTO.getLevel1Type().equals(MOOD_BOARD.getRealName())&&generateThroughImageTextDTO.getModelName().equals("high"))){
|
||||
if (!StringUtil.isNullOrEmpty(text)) {
|
||||
text = modifyPrompt(text, generate, generateThroughImageTextDTO.getLevel1Type(), generateThroughImageTextDTO.getAgeGroup());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// todo 这一步现在还是有必要的吗?
|
||||
// 2.1 sketch或print在t_collection_element表/t_library表中的信息是否需要更新 如 level2Type
|
||||
CollectionElement collectionElement = collectionElementService.editLevel2Type(elementId, generateThroughImageTextDTO.getLevel2Type(), generateThroughImageTextDTO.getDesignType());
|
||||
@@ -218,6 +225,8 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
version = "fast";
|
||||
params.put("version", "fast");
|
||||
}
|
||||
// 4、将请求信息落库,将本次generate的请求信息添加到t_generate表中
|
||||
saveGenerateImmediately(generate);
|
||||
// 3.1 确定不同类型的印花分别调哪个接口
|
||||
if (generateThroughImageTextDTO.getLevel1Type().equals(PRINT_BOARD.getRealName())) {
|
||||
switch (generateThroughImageTextDTO.getLevel2Type()) {
|
||||
@@ -243,15 +252,28 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
||||
}
|
||||
} else {
|
||||
GenerateToPythonDTO generateToPythonDTO = new GenerateToPythonDTO(generateThroughImageTextDTO.getUniqueId(), text, Objects.isNull(collectionElement) ? "" : collectionElement.getUrl(),
|
||||
mode, category, generateThroughImageTextDTO.getGender(), version);
|
||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
||||
if (Objects.equals(version, "fast")) {
|
||||
GenerateToPythonDTO generateToPythonDTO = new GenerateToPythonDTO(generateThroughImageTextDTO.getUniqueId(), text, Objects.isNull(collectionElement) ? "" : collectionElement.getUrl(),
|
||||
mode, category, generateThroughImageTextDTO.getGender(), version);
|
||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
||||
} else {
|
||||
|
||||
|
||||
path = CommonConstant.GENERATE_PATH_FLUX2_KLEIN;
|
||||
// 构建object_name: {userId}/{category}/{uuid}.png
|
||||
String objectName = generateThroughImageTextDTO.getUserId() + "/" + category + "/" + UUID.randomUUID() + ".png";
|
||||
|
||||
ImageProcessRequest imageProcessRequest = ImageProcessRequest.builder()
|
||||
.object_name(objectName)
|
||||
.bucket_name(userBucket)
|
||||
.prompt(text).build();
|
||||
jsonString = JSON.toJSONString(imageProcessRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Boolean requestResult = pythonService.generateSketchOrPrint(jsonString, port, path);
|
||||
Boolean requestResult = pythonService.generateSketchOrPrint(jsonString, port, path, generateThroughImageTextDTO.getUniqueId());
|
||||
|
||||
// 4、将请求信息落库,将本次generate的请求信息添加到t_generate表中
|
||||
save(generate);
|
||||
|
||||
// 5、将本次请求存入redis
|
||||
String key = generateResultKey + ":" + generateThroughImageTextDTO.getUniqueId();
|
||||
@@ -266,6 +288,40 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
|
||||
}
|
||||
|
||||
public void saveGenerateImmediately(Generate generate) {
|
||||
save(generate);
|
||||
// 使用 TransactionSynchronizationManager 在事务真正提交后再设锁
|
||||
// 否则 save() 完成后事务尚未 commit,MQ 消费者立即读到 null
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
String lockKey = "generate:lock:" + generate.getUniqueId();
|
||||
redisUtil.addToString(lockKey, "1", 60L);
|
||||
log.debug("Save lock set after commit for uniqueId: {}", generate.getUniqueId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void waitForSaveLock(String uniqueId) {
|
||||
String lockKey = "generate:lock:" + uniqueId;
|
||||
int maxRetries = 30;
|
||||
int retryIntervalMs = 200;
|
||||
for (int i = 0; i < maxRetries; i++) {
|
||||
if (Boolean.TRUE.equals(redisUtil.hasKey(lockKey))) {
|
||||
log.debug("Save lock acquired for uniqueId: {} after {} retries", uniqueId, i);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(retryIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("Interrupted while waiting for save lock: {}", uniqueId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.warn("Save lock timeout for uniqueId: {}, proceeding anyway", uniqueId);
|
||||
}
|
||||
|
||||
public GenerateModeEnum getMode(GenerateThroughImageTextDTO generateThroughImageTextDTO) {
|
||||
if (!StringUtil.isNullOrEmpty(generateThroughImageTextDTO.getText())) {
|
||||
if (Objects.nonNull(generateThroughImageTextDTO.getCollectionElementId())) {
|
||||
@@ -284,11 +340,16 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void processGenerateResult(String taskId, String url, String category) {
|
||||
log.info("============ProcessGenerateResult listening==========");
|
||||
log.debug("taskId: " + taskId);
|
||||
String status = null;
|
||||
// 1、处理模型返回的数据
|
||||
GenerateDetail generateDetail = new GenerateDetail();
|
||||
GenerateCollectionItemVO generateCollectionItemVO = new GenerateCollectionItemVO();
|
||||
Generate generate;
|
||||
try {
|
||||
// 等待 HTTP 线程写入完成后再查库
|
||||
waitForSaveLock(taskId);
|
||||
generate = selectByUniqueId(taskId);
|
||||
} catch (MybatisPlusException e) {
|
||||
log.error(e.getMessage());
|
||||
@@ -311,14 +372,15 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
generateDetail.setUrl(url);
|
||||
generateDetail.setGenerateId(generate.getId());
|
||||
generateDetail.setCreateDate(LocalDateTime.now());
|
||||
generateDetail.setMd5(md5);
|
||||
generateDetail.setMd5("");
|
||||
// 将相应的url保存到数据库
|
||||
generateDetailMapper.insert(generateDetail);
|
||||
log.debug("generateDetail: " + generateDetail.toString());
|
||||
|
||||
// String uuid = taskId.substring(0, taskId.substring(0, taskId.lastIndexOf("-")).lastIndexOf("-"));
|
||||
String key = generateResultKey + ":" + taskId;
|
||||
String imageName = url.substring(url.lastIndexOf("/") + 1);
|
||||
String status = imageName.equals("white_image.jpg") ? "Invalid" : "Success";
|
||||
status = imageName.equals("white_image.jpg") ? "Invalid" : "Success";
|
||||
if (StringUtil.isNullOrEmpty(category)) {
|
||||
Generate generateRecord = selectByUniqueId(taskId);
|
||||
category = generateRecord.getLevel2Type();
|
||||
@@ -326,6 +388,8 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
GenerateResultVO generateResultVO = new GenerateResultVO(taskId, generateDetail.getId(), url, status, category);
|
||||
// 更新redis
|
||||
redisUtil.addToString(key, new Gson().toJson(generateResultVO), CommonConstant.GENERATE_RESULT_EXPIRE_TIME);
|
||||
log.debug("generateResultVO: " + generateResultVO.toString());
|
||||
|
||||
|
||||
// 执行积分扣除
|
||||
// ** 注:如果生成的图片都是空白 则不扣积分
|
||||
@@ -785,8 +849,9 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
long requestEndTime = System.currentTimeMillis();
|
||||
log.info("HTTP请求完成 - 响应状态: {}, 耗时: {}ms, taskId: {}",
|
||||
response.code(), (requestEndTime - requestStartTime), taskId);
|
||||
String result = response.body().string();
|
||||
if (!response.isSuccessful()) {
|
||||
log.warn("Google API响应失败,状态码: {} for taskId: {}", response.code(), taskId);
|
||||
log.warn("Google API响应失败,状态码: {} for taskId: {},结果:{}", response.code(), taskId, result);
|
||||
if (attempt < maxRetries) {
|
||||
Thread.sleep(retryDelay * attempt); // 递增延迟
|
||||
continue;
|
||||
@@ -795,7 +860,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
}
|
||||
|
||||
String result = response.body().string();
|
||||
|
||||
// log.info("Google 响应结果:{}", result);
|
||||
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(result);
|
||||
|
||||
@@ -1065,6 +1130,12 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
|
||||
String result = response.body().string();
|
||||
|
||||
if (response.code() != 200) {
|
||||
log.error("Google API 请求失败 - taskId: {}, 尝试: {}, URL: {}, 状态码: {}, 响应结果: {}",
|
||||
taskId, attempt, endpoint, response.code(), result);
|
||||
throw new BusinessException("system.error");
|
||||
}
|
||||
|
||||
// log.info("Google 响应结果:{}", result);
|
||||
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(result);
|
||||
|
||||
@@ -1203,6 +1274,9 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
* @param modelName advanced high normal
|
||||
*/
|
||||
private HashMap<String, String> chooseModelAndPrompt(GenerateThroughImageTextDTO generateDTO, String modelName) {
|
||||
if (StringUtil.isNullOrEmpty(modelName)) {
|
||||
throw new BusinessException("system error");
|
||||
}
|
||||
HashMap<String, String> modelAndPromptMap = new HashMap<>();
|
||||
boolean isUseImage;
|
||||
if (Objects.isNull(generateDTO.getCollectionElementId())
|
||||
@@ -1218,7 +1292,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
String style = generateDTO.getText().substring(0, firstCommaIndex).trim();
|
||||
|
||||
String prompt = generateDTO.getText().substring(firstCommaIndex + 1).trim();
|
||||
prompt = getPrintboardPrompt(style, prompt);
|
||||
prompt = getPrintboardPrompt(style, prompt, modelName, isUseImage);
|
||||
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||
|
||||
|
||||
@@ -1239,7 +1313,14 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
|
||||
} else if (ModelConstants.MOODBOARD.equals(generateDTO.getLevel1Type())) {
|
||||
String prompt = generateDTO.getText() + "high-resolution, ultra-detailed, realistic textures, perfect anatomy, cinematic lighting, 8k render, editorial photography style";
|
||||
String userInput = generateDTO.getText();
|
||||
String systemPrompt = "high-resolution, ultra-detailed, realistic textures, cinematic lighting, 8k render, editorial photography style";
|
||||
String prompt;
|
||||
if (userInput == null || userInput.trim().isEmpty()) {
|
||||
throw new BusinessException("prompt null");
|
||||
} else {
|
||||
prompt = "Theme: " + userInput.trim() + "\nRequirement: " + systemPrompt;
|
||||
}
|
||||
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||
|
||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||
@@ -1250,8 +1331,31 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
modelAndPromptMap.put(ModelConstants.USE_MODEL, ModelConstants.LOCAL_MODEL);
|
||||
}
|
||||
} else if (ModelConstants.SKETCHBOARD.equals(generateDTO.getLevel1Type())) {
|
||||
String prompt = generateDTO.getText() + "rules:front view sketch only,plain white background, single garment only, orthographic, centered on white background, borderless canvas, thin monochrome black line art.\n" +
|
||||
String style = "";
|
||||
String userPrompt = "";
|
||||
// 找到第一个逗号的位置
|
||||
int firstCommaIndex = generateDTO.getText().indexOf(",");
|
||||
if (firstCommaIndex != -1) {
|
||||
// 截取第一个逗号前的内容作为style
|
||||
style = generateDTO.getText().substring(0, firstCommaIndex).trim();
|
||||
// 截取第一个逗号后的所有内容作为userPrompt(去除首尾空格)
|
||||
userPrompt = generateDTO.getText().substring(firstCommaIndex + 1).trim();
|
||||
|
||||
if ("Lolita".equals(style)) {
|
||||
style = "洛丽塔";
|
||||
}
|
||||
} else {
|
||||
// 兼容无逗号的情况:style为空,全部内容作为userPrompt
|
||||
userPrompt = generateDTO.getText().trim();
|
||||
}
|
||||
|
||||
String prompt = userPrompt + "rules:front view sketch only,plain white background, single garment only, orthographic, centered on white background, borderless canvas, thin monochrome black line art.\n" +
|
||||
" No clothes hanger, no fake clothes hanger, no human-related lines, no color fill, no words, no text, no black background, no boundary or frame.";
|
||||
|
||||
if (!style.trim().isEmpty() && !"all".equalsIgnoreCase(style)) {
|
||||
prompt += ".sketch style:" + style.trim();
|
||||
}
|
||||
|
||||
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||
if (isUseImage) {
|
||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||
@@ -1449,6 +1553,13 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
if (imagePath != null) {
|
||||
requestBuilder.image(finalImagePath1);
|
||||
}
|
||||
if (useModel.equals(ModelConstants.PRINTBOARD_HIGH_I2I)|| useModel.equals(ModelConstants.PRINTBOARD_HIGH_T2I)) {
|
||||
GenerateImagesRequest.OptimizePromptOptions optimizePromptOptions = new GenerateImagesRequest.OptimizePromptOptions();
|
||||
optimizePromptOptions.setMode("fast");
|
||||
requestBuilder.optimizePromptOptions(optimizePromptOptions);
|
||||
//由于PRINTBOARD_HIGH_T2I,PRINTBOARD_HIGH_I2I与PRINTBOARD_ADVANCED_I2I使用模型一致,为了区别积分扣除,PRINTBOARD_HIGH_I2I加入了-fast或者-high,但传入模型时需要去掉-fast或者-high,用PRINTBOARD_ADVANCED_I2I的常量做替代
|
||||
requestBuilder.model(ModelConstants.PRINTBOARD_ADVANCED_I2I);
|
||||
}
|
||||
|
||||
// 保存生成记录到数据库
|
||||
Generate generate = new Generate(
|
||||
@@ -1560,19 +1671,44 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
|
||||
|
||||
private String getPrintboardPrompt(String style, String prompt) {
|
||||
private String getPrintboardPrompt(String style, String userInput, String modelName, boolean isUseImage) {
|
||||
String systemPrompt = null;
|
||||
String prompt;
|
||||
|
||||
if ("Painting Style".equals(style)) {
|
||||
prompt = "1.Requirements: Create a seamless, tiling fashion printboard pattern for apparel. The output must be stylish, contemporary, and suitable for real garment printing. Design pattern, seamless, highly detailed, elegant composition, visually balanced, professional textile print\n" +
|
||||
"2.Core Theme: " + prompt + "\n" +
|
||||
"3.Style: painting_style-The painting style refers to the overall approach, techniques, and artistic philosophy used in the artwork. For fashion designs that will be applied to printboards, it is important to define the unique stylistic elements that would translate well into wearable patterns.";
|
||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||
systemPrompt = "Tileable seamless pattern, elegant composition, visually balanced, Light watercolor, Giplie Studio (style) pattern with even color field background, high-quality digital print, zero perspective depth, harmonious visual balance, consistent color tone.";
|
||||
} else if (ModelConstants.HIGH.equals(modelName)) {
|
||||
systemPrompt = "Design pattern, seamless, highly detailed, elegant composition, visually balanced. \n" +
|
||||
"Painting style: traditional painting, hand-painted, brush strokes.";
|
||||
}
|
||||
} else if ("Illustration Style".equals(style)) {
|
||||
prompt = "1.Requirements: Create a seamless, tiling fashion printboard pattern for apparel. The output must be stylish, contemporary, and suitable for real garment printing. Design pattern, seamless, highly detailed, elegant composition, visually balanced, professional textile print\n" +
|
||||
"2.Core Theme: " + prompt + "\n" +
|
||||
"3.Style: illustration_style-Illustration style focuses on the visual storytellingaspect, often used to depict narratives, characters, or thematic concepts. Forfashion, this style can introduce vivid and artistic interpretations, often aligned with specific themes.";
|
||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||
systemPrompt = "Tileable seamless pattern, elegant composition, visually balanced, flat graphic, clean lines pattern with even color field background, high-quality digital print, zero perspective depth, harmonious visual balance, consistent color tone. ";
|
||||
} else if (ModelConstants.HIGH.equals(modelName)) {
|
||||
systemPrompt = "Design pattern, seamless, highly detailed, elegant composition, visually balanced. \n" +
|
||||
"Illustration Style: flat graphic, clean lines.";
|
||||
}
|
||||
} else if ("Real Style".equals(style)) {
|
||||
prompt = "1.Requirements: Create a seamless, tiling fashion printboard pattern for apparel. The output must be stylish, contemporary, and suitable for real garment printing. Design pattern, seamless, highly detailed, elegant composition, visually balanced, professional textile print\n" +
|
||||
"2.Core Theme: " + prompt + "\n" +
|
||||
"3.Style: real_style-Real style in fashion is all about authenticity. It featuresnatural fabrics, simple cuts that mirror real life silhouettes, and colors inspired bythe everyday world, exuding a down-to-earth and genuine charm.";
|
||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||
systemPrompt = "Tileable seamless pattern, even color field background, photorealistic style pattern, high-quality digital print, zero perspective depth, harmonious visual balance, consistent color tone. ";
|
||||
} else if (ModelConstants.HIGH.equals(modelName)) {
|
||||
systemPrompt = "Design pattern, seamless, highly detailed, elegant composition, visually balanced. \n" +
|
||||
"Flat textile pattern printed directly on fabric surface, no three-dimensional objects, no items placed on cloth. \n" +
|
||||
"Real style: fabric print, realistic woven/printed pattern, detailed surface pattern only";
|
||||
}
|
||||
} else {
|
||||
throw new BusinessException("style error:" + style);
|
||||
}
|
||||
|
||||
if (userInput == null || userInput.trim().isEmpty()) {
|
||||
if (isUseImage) {
|
||||
prompt = "Theme: Image content" + "\nRequirement: " + systemPrompt;
|
||||
} else {
|
||||
throw new BusinessException("prompt null");
|
||||
}
|
||||
} else {
|
||||
prompt = "Theme: " + userInput.trim() + "\nRequirement: " + systemPrompt;
|
||||
}
|
||||
return prompt;
|
||||
}
|
||||
@@ -1970,7 +2106,9 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
public Generate selectByUniqueId(String uniqueId) {
|
||||
QueryWrapper<Generate> qw = new QueryWrapper<>();
|
||||
qw.eq("unique_id", uniqueId);
|
||||
|
||||
log.debug("selectByUniqueId: " + uniqueId);
|
||||
Generate one = getOne(qw);
|
||||
log.debug("Generate: " + one);
|
||||
return getOne(qw);
|
||||
}
|
||||
|
||||
@@ -3796,11 +3934,48 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
|
||||
public byte[] downloadVideoOrImage(String url) {
|
||||
try (CloseableHttpClient client = HttpClients.createDefault();
|
||||
InputStream in = client.execute(new HttpGet(url)).getEntity().getContent()) {
|
||||
return IOUtils.toByteArray(in);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
int maxRetries = 3;
|
||||
int retryDelayMs = 1000;
|
||||
IOException lastException = null;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return downloadWithTimeout(url, 30000, 60000);
|
||||
} catch (IOException e) {
|
||||
lastException = e;
|
||||
log.warn("下载失败 (尝试 {}/{}): {}", attempt, maxRetries, e.getMessage());
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
try {
|
||||
Thread.sleep((long) retryDelayMs * attempt); // 递增延迟
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(ie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("下载失败,已重试 " + maxRetries + " 次", lastException);
|
||||
}
|
||||
|
||||
private byte[] downloadWithTimeout(String url, int connectTimeout, int socketTimeout) throws IOException {
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(connectTimeout)
|
||||
.setSocketTimeout(socketTimeout)
|
||||
.setConnectionRequestTimeout(connectTimeout)
|
||||
.build();
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.custom()
|
||||
.setDefaultRequestConfig(requestConfig)
|
||||
.build();
|
||||
CloseableHttpResponse response = client.execute(new HttpGet(url))) {
|
||||
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if (statusCode != 200) {
|
||||
throw new IOException("HTTP Error: " + statusCode);
|
||||
}
|
||||
return IOUtils.toByteArray(response.getEntity().getContent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4141,11 +4316,11 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
// 处理不同状态
|
||||
switch (statusEnum) {
|
||||
case TASK_NOT_FOUND:
|
||||
// 审核没过
|
||||
// 审核没过
|
||||
case REQUEST_MODERATED:
|
||||
// 审核没过
|
||||
// 审核没过
|
||||
case CONTENT_MODERATED:
|
||||
// 出错
|
||||
// 出错
|
||||
case ERROR:
|
||||
return "Fail";
|
||||
case PENDING_F:
|
||||
@@ -4264,7 +4439,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
MotionModeEnum motionModeEnum = MotionModeEnum.of(poseTransformDTO.getMode());
|
||||
switch (motionModeEnum) {
|
||||
case POSE_TO_VIDEO:
|
||||
params.put("pose_id", poseTransformDTO.getPoseId());
|
||||
params.put("pose_id", poseTransformDTO.getPoseId().toString());
|
||||
params.put("image_url", poseTransformDTO.getProductImage());
|
||||
break;
|
||||
case PROMPT_TO_VIDEO:
|
||||
|
||||
656
src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java
Normal file
656
src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java
Normal file
@@ -0,0 +1,656 @@
|
||||
package com.ai.da.service.impl;
|
||||
|
||||
import com.ai.da.common.config.exception.BusinessException;
|
||||
import com.ai.da.common.enums.AuthenticationOperationTypeEnum;
|
||||
import com.ai.da.common.utils.*;
|
||||
import com.ai.da.mapper.primary.AccountMapper;
|
||||
import com.ai.da.mapper.primary.ContestantMapper;
|
||||
import com.ai.da.mapper.primary.NotificationMapper;
|
||||
import com.ai.da.mapper.primary.entity.Account;
|
||||
import com.ai.da.mapper.primary.entity.Contestant;
|
||||
import com.ai.da.mapper.primary.entity.Notification;
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.dto.PublishSysNotificationDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.model.vo.PageVisitCountVO;
|
||||
import com.ai.da.service.GlobalAwardService;
|
||||
import com.ai.da.service.MessageCenterService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
|
||||
@Resource
|
||||
private ContestantMapper contestantMapper;
|
||||
|
||||
private final AccountMapper accountMapper;
|
||||
|
||||
private final MessageCenterService messageCenterService;
|
||||
|
||||
private final NotificationMapper notificationMapper;
|
||||
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
@Value("${file.upload.temp.dir}")
|
||||
private String uploadDir;
|
||||
|
||||
private static final DateTimeFormatter YYYY_MM_DD = DateTimeFormatter.ofPattern("yyyy/MM");
|
||||
|
||||
private static final String tokenCacheKey = AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + ":";
|
||||
|
||||
@Value("${minio.bucket:contestants}")
|
||||
private String minioBucket;
|
||||
|
||||
@Value("${global.award.link}")
|
||||
private String link;
|
||||
|
||||
@Resource
|
||||
private MinioUtil minioUtil;
|
||||
|
||||
@Override
|
||||
public String uploadPdf(MultipartFile file, String email) throws Exception {
|
||||
validatePdf(file);
|
||||
String path = storeFile(file, email, "pdf");
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadVideo(MultipartFile file, String email) throws Exception {
|
||||
validateVideo(file);
|
||||
String path = storeFile(file, email, "video");
|
||||
return path;
|
||||
}
|
||||
|
||||
private void validatePdf(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("File is empty.");
|
||||
}
|
||||
String ct = file.getContentType();
|
||||
if (ct == null || !ct.toLowerCase().contains("pdf")) {
|
||||
throw new BusinessException("Only PDF files are allowed.");
|
||||
}
|
||||
// size limit example 20MB
|
||||
if (file.getSize() > 20L * 1024 * 1024) {
|
||||
throw new BusinessException("PDF file size exceeds the limit.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateVideo(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("File is empty.");
|
||||
}
|
||||
String ct = file.getContentType();
|
||||
if (ct == null || !(ct.toLowerCase().contains("mp4") || ct.toLowerCase().contains("video") )) {
|
||||
throw new BusinessException("Invalid video file type.");
|
||||
}
|
||||
// size limit example 100MB
|
||||
if (file.getSize() > 100L * 1024 * 1024) {
|
||||
throw new BusinessException("Video file size exceeds the limit.");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeEmail(String email) {
|
||||
if (email == null) {
|
||||
return "anonymous";
|
||||
}
|
||||
return email.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
}
|
||||
|
||||
private String storeFile(MultipartFile file, String email, String kind) throws IOException {
|
||||
String normalized = normalizeEmail(email);
|
||||
String datePart = LocalDateTime.now().format(YYYY_MM_DD);
|
||||
String ext = "";
|
||||
String original = file.getOriginalFilename();
|
||||
if (original != null && original.contains(".")) {
|
||||
ext = original.substring(original.lastIndexOf('.'));
|
||||
}
|
||||
String filename = System.currentTimeMillis() + "_" + UUID.randomUUID().toString() + ext;
|
||||
String relativePath = "contestants/" + normalized + "/" + datePart + "/" + filename;
|
||||
|
||||
String uploadedPath = minioUtil.upload(minioBucket, relativePath, file, null);
|
||||
log.info("uploaded via MinioUtil: {}", uploadedPath);
|
||||
return uploadedPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> saveContestant(ContestantDTO request) {
|
||||
Map<String,Object> resp = new HashMap<>();
|
||||
if (request.getEmail() == null) {
|
||||
throw new BusinessException("Email is required.");
|
||||
}
|
||||
|
||||
checkSecurityToken(request.getEmail(), request.getSecureToken());
|
||||
|
||||
QueryWrapper<Contestant> qw = new QueryWrapper<>();
|
||||
qw.eq("email", request.getEmail());
|
||||
Contestant existing = contestantMapper.selectOne(qw);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (existing == null) {
|
||||
// 通过行锁 + 重试机制保证 contestant_number 在并发下自增分配
|
||||
final int maxAttempts = 5;
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
// 获取当前最大 contestant_number 并加行锁(LIMIT 1 FOR UPDATE)
|
||||
QueryWrapper<Contestant> qMax = new QueryWrapper<>();
|
||||
qMax.isNotNull("contestant_number");
|
||||
qMax.orderByDesc("contestant_number");
|
||||
qMax.last("LIMIT 1 FOR UPDATE");
|
||||
Contestant last = contestantMapper.selectOne(qMax);
|
||||
Integer nextNumber = (last == null || last.getContestantNumber() == null) ? 10000 : last.getContestantNumber() + 1;
|
||||
|
||||
Contestant toInsert = Contestant.builder()
|
||||
.email(request.getEmail())
|
||||
.firstName(request.getFirstName())
|
||||
.lastName(request.getLastName())
|
||||
.gender(request.getGender())
|
||||
.occupation(request.getOccupation())
|
||||
.age(request.getAge())
|
||||
.countryRegionCity(request.getCountryRegionCity())
|
||||
.phoneNumber(request.getPhoneNumber())
|
||||
.designTitle(request.getDesignTitle())
|
||||
.designDescription(request.getDesignDescription())
|
||||
.pdfPath(request.getPdfPath())
|
||||
.videoPath(request.getVideoPath())
|
||||
.videoDuration(request.getVideoDuration())
|
||||
.videoSize(request.getVideoSize())
|
||||
.pdfSize(request.getPdfSize())
|
||||
.contestantNumber(nextNumber)
|
||||
.portfolioUrl(request.getPortfolioUrl())
|
||||
.createdAt(now)
|
||||
.updatedAt(now)
|
||||
.build();
|
||||
|
||||
contestantMapper.insert(toInsert);
|
||||
|
||||
resp.put("success", true);
|
||||
sendSiteMsg(toInsert.getId(), toInsert.getEmail());
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
log.warn("Attempt {} to assign contestant_number failed", attempt, e);
|
||||
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
|
||||
if ((msg.contains("duplicate") || msg.contains("uniq_contestant_number") || msg.contains("contestant_number")) && attempt < maxAttempts) {
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw new BusinessException("Failed to assign contestant number after retries.");
|
||||
} else {
|
||||
// update existing contestant
|
||||
existing.setFirstName(request.getFirstName());
|
||||
existing.setLastName(request.getLastName());
|
||||
existing.setGender(request.getGender());
|
||||
existing.setOccupation(request.getOccupation());
|
||||
existing.setAge(request.getAge());
|
||||
existing.setCountryRegionCity(request.getCountryRegionCity());
|
||||
existing.setPhoneNumber(request.getPhoneNumber());
|
||||
existing.setDesignTitle(request.getDesignTitle());
|
||||
existing.setDesignDescription(request.getDesignDescription());
|
||||
existing.setPdfPath(request.getPdfPath());
|
||||
existing.setVideoPath(request.getVideoPath());
|
||||
existing.setVideoDuration(request.getVideoDuration());
|
||||
existing.setVideoSize(request.getVideoSize());
|
||||
existing.setPdfSize(request.getPdfSize());
|
||||
existing.setPortfolioUrl(request.getPortfolioUrl());
|
||||
existing.setUpdatedAt(now);
|
||||
contestantMapper.updateById(existing);
|
||||
resp.put("success", true);
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] exportContestants() throws Exception {
|
||||
List<Contestant> list = contestantMapper.selectList(new QueryWrapper<>());
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("contestants");
|
||||
int rowIdx = 0;
|
||||
Row header = sheet.createRow(rowIdx++);
|
||||
String[] headers = new String[] {
|
||||
"contestantNumber", "email", "firstName", "lastName", "gender", "occupation",
|
||||
"age", "countryRegionCity", "phoneNumber", "designTitle", "designDescription",
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "portfolioUrl", "createdAt", "updatedAt"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell c = header.createCell(i);
|
||||
c.setCellValue(headers[i]);
|
||||
}
|
||||
|
||||
for (Contestant cst : list) {
|
||||
Row r = sheet.createRow(rowIdx++);
|
||||
int ci = 0;
|
||||
r.createCell(ci++).setCellValue(cst.getContestantNumber() == null ? "" : cst.getContestantNumber().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getEmail() == null ? "" : cst.getEmail());
|
||||
r.createCell(ci++).setCellValue(cst.getFirstName() == null ? "" : cst.getFirstName());
|
||||
r.createCell(ci++).setCellValue(cst.getLastName() == null ? "" : cst.getLastName());
|
||||
r.createCell(ci++).setCellValue(cst.getGender() == null ? "" : cst.getGender());
|
||||
r.createCell(ci++).setCellValue(cst.getOccupation() == null ? "" : cst.getOccupation());
|
||||
r.createCell(ci++).setCellValue(cst.getAge() == null ? "" : cst.getAge().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getCountryRegionCity() == null ? "" : cst.getCountryRegionCity());
|
||||
r.createCell(ci++).setCellValue(cst.getPhoneNumber() == null ? "" : cst.getPhoneNumber());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignTitle() == null ? "" : cst.getDesignTitle());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignDescription() == null ? "" : cst.getDesignDescription());
|
||||
r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoDuration() == null ? "" : cst.getVideoDuration().toString());
|
||||
if (cst.getVideoSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double vMb = cst.getVideoSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", vMb));
|
||||
}
|
||||
if (cst.getPdfSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double pMb = cst.getPdfSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", pMb));
|
||||
}
|
||||
r.createCell(ci++).setCellValue(cst.getPortfolioUrl() == null ? "" : cst.getPortfolioUrl());
|
||||
r.createCell(ci++).setCellValue(cst.getCreatedAt() == null ? "" : cst.getCreatedAt().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getUpdatedAt() == null ? "" : cst.getUpdatedAt().toString());
|
||||
}
|
||||
|
||||
workbook.write(out);
|
||||
out.flush();
|
||||
return out.toByteArray();
|
||||
} catch (IOException e) {
|
||||
log.error("export contestants failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveContestantsToLocal() throws Exception {
|
||||
List<Contestant> list = contestantMapper.selectList(new QueryWrapper<>());
|
||||
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||
String ts = LocalDateTime.now().format(fmt);
|
||||
Path exportDir = Paths.get(uploadDir == null ? "uploads" : uploadDir, "exports");
|
||||
Files.createDirectories(exportDir);
|
||||
Path outPath = exportDir.resolve("contestants_" + ts + ".xlsx");
|
||||
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(); FileOutputStream fos = new FileOutputStream(outPath.toFile())) {
|
||||
Sheet sheet = workbook.createSheet("contestants");
|
||||
int rowIdx = 0;
|
||||
Row header = sheet.createRow(rowIdx++);
|
||||
String[] headers = new String[] {
|
||||
"contestantNumber", "email", "firstName", "lastName", "gender", "occupation",
|
||||
"age", "countryRegionCity", "phoneNumber", "designTitle", "designDescription",
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "portfolioUrl", "createdAt", "updatedAt"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell c = header.createCell(i);
|
||||
c.setCellValue(headers[i]);
|
||||
}
|
||||
|
||||
for (Contestant cst : list) {
|
||||
Row r = sheet.createRow(rowIdx++);
|
||||
int ci = 0;
|
||||
r.createCell(ci++).setCellValue(cst.getContestantNumber() == null ? "" : cst.getContestantNumber().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getEmail() == null ? "" : cst.getEmail());
|
||||
r.createCell(ci++).setCellValue(cst.getFirstName() == null ? "" : cst.getFirstName());
|
||||
r.createCell(ci++).setCellValue(cst.getLastName() == null ? "" : cst.getLastName());
|
||||
r.createCell(ci++).setCellValue(cst.getGender() == null ? "" : cst.getGender());
|
||||
r.createCell(ci++).setCellValue(cst.getOccupation() == null ? "" : cst.getOccupation());
|
||||
r.createCell(ci++).setCellValue(cst.getAge() == null ? "" : cst.getAge().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getCountryRegionCity() == null ? "" : cst.getCountryRegionCity());
|
||||
r.createCell(ci++).setCellValue(cst.getPhoneNumber() == null ? "" : cst.getPhoneNumber());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignTitle() == null ? "" : cst.getDesignTitle());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignDescription() == null ? "" : cst.getDesignDescription());
|
||||
r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoDuration() == null ? "" : cst.getVideoDuration().toString());
|
||||
if (cst.getVideoSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double vMb = cst.getVideoSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", vMb));
|
||||
}
|
||||
if (cst.getPdfSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double pMb = cst.getPdfSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", pMb));
|
||||
}
|
||||
r.createCell(ci++).setCellValue(cst.getPortfolioUrl() == null ? "" : cst.getPortfolioUrl());
|
||||
r.createCell(ci++).setCellValue(cst.getCreatedAt() == null ? "" : cst.getCreatedAt().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getUpdatedAt() == null ? "" : cst.getUpdatedAt().toString());
|
||||
}
|
||||
|
||||
workbook.write(fos);
|
||||
fos.flush();
|
||||
log.info("Exported contestants to local file: {}", outPath.toString());
|
||||
} catch (IOException e) {
|
||||
log.error("save contestants to local failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContestantDTO getContestantByID(String id) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("id is required.");
|
||||
}
|
||||
Contestant existing = contestantMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
return null;
|
||||
}
|
||||
ContestantDTO dto = new ContestantDTO();
|
||||
// dto.setEmail(existing.getEmail());
|
||||
dto.setFirstName(existing.getFirstName());
|
||||
dto.setLastName(existing.getLastName());
|
||||
dto.setGender(existing.getGender());
|
||||
dto.setOccupation(existing.getOccupation());
|
||||
dto.setAge(existing.getAge());
|
||||
dto.setCountryRegionCity(existing.getCountryRegionCity());
|
||||
dto.setPhoneNumber(existing.getPhoneNumber());
|
||||
dto.setDesignTitle(existing.getDesignTitle());
|
||||
dto.setDesignDescription(existing.getDesignDescription());
|
||||
dto.setPdfPath(existing.getPdfPath());
|
||||
dto.setVideoPath(existing.getVideoPath());
|
||||
dto.setVideoDuration(existing.getVideoDuration());
|
||||
dto.setPdfSize(existing.getPdfSize());
|
||||
dto.setVideoSize(existing.getVideoSize());
|
||||
dto.setPortfolioUrl(existing.getPortfolioUrl());
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否符合申请要求,发送验证码
|
||||
* @param email AiDA邮箱
|
||||
*/
|
||||
public void checkEmail(String email) {
|
||||
List<Integer> validRole = Arrays.asList(1, 2, 7, 8);
|
||||
// 1. 验证邮箱在aida中有无账号
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda().eq(Account::getUserEmail, email);
|
||||
List<Account> accounts = accountMapper.selectList(queryWrapper);
|
||||
if (accounts.isEmpty()) {
|
||||
throw new BusinessException("Please register and subscribe to AiDA, then resubmit your application.");
|
||||
}
|
||||
|
||||
// 2. 验证账号是否是付费用户(如果首次提交是,但是修改的时候已经不是了,how?不允许修改吗)
|
||||
if (validRole.contains(accounts.getFirst().getSystemUser())) {
|
||||
String randomVerifyCode = RandomsUtil.generateVerifyCode(100000L, 999999L);
|
||||
LocalCacheUtils.setVerifyCodeCache(
|
||||
AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + "_" + email, randomVerifyCode);
|
||||
SendEmailUtil.send(email, null,
|
||||
SendEmailUtil.LOGIN_TEMPLATE_ID, randomVerifyCode);
|
||||
} else {
|
||||
throw new BusinessException("Please subscribe to AiDA, then resubmit your application.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码是否正确
|
||||
* @param email 邮箱
|
||||
* @param otp 一次性验证码
|
||||
* @return 临时token和之前提交的表单内容
|
||||
*/
|
||||
public CheckOTPVO checkCode(String email, String otp) {
|
||||
String otpCache = LocalCacheUtils.getVerifyCodeCache(AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + "_" + email);
|
||||
assert otpCache != null;
|
||||
if (otpCache.equals(otp)) {
|
||||
// 1. 生成唯一token
|
||||
String secureToken = UUID.randomUUID().toString().replace("-", "");
|
||||
redisUtil.addToString(tokenCacheKey + email, secureToken, 3 * 24 * 60 * 60L);
|
||||
|
||||
return new CheckOTPVO(secureToken, getContestantByID(email));
|
||||
} else {
|
||||
throw new BusinessException("Verification code is incorrect. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
public void checkSecurityToken(String email, String securityToken) {
|
||||
String key = tokenCacheKey + email;
|
||||
if (StringUtils.isBlank(securityToken)) {
|
||||
log.error("security token 缺失");
|
||||
throw new BusinessException("Please complete email verification first.");
|
||||
}
|
||||
|
||||
String tokenCache = redisUtil.getFromString(key);
|
||||
if (StringUtils.isBlank(tokenCache)) {
|
||||
log.error("security token 过期");
|
||||
throw new BusinessException("Email verification has expired. Please verify again.");
|
||||
} else if (!tokenCache.equals(securityToken)){
|
||||
log.error("security token 与缓存不符");
|
||||
throw new BusinessException("Identity verification failed. Please complete email verification first.");
|
||||
}
|
||||
}
|
||||
|
||||
// 发送站内信
|
||||
public void sendSiteMsg(String applicationId, String email) {
|
||||
Long userId;
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda().eq(Account::getUserEmail, email);
|
||||
List<Account> accounts = accountMapper.selectList(queryWrapper);
|
||||
if (accounts.isEmpty()) {
|
||||
throw new BusinessException("Please register and subscribe to AiDA, then resubmit your application.");
|
||||
}else {
|
||||
userId = accounts.get(0).getId();
|
||||
}
|
||||
PublishSysNotificationDTO sysNotificationDTO = new PublishSysNotificationDTO();
|
||||
Notification notification = new Notification();
|
||||
notification.setType("system");
|
||||
notification.setReceiverId(userId);
|
||||
sysNotificationDTO.setTitle("System Notification 系统通知");
|
||||
// todo
|
||||
sysNotificationDTO.setContent(link + applicationId);
|
||||
notification.setContent(JSON.toJSONString(sysNotificationDTO));
|
||||
notification.setIsRead(0);
|
||||
notification.setCreateTime(LocalDateTime.now());
|
||||
notificationMapper.insert(notification);
|
||||
// 这里推送消息是在接受到视频生成结束后发生的,所以UserContext中没有用户信息
|
||||
messageCenterService.pushMessage("system", userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception {
|
||||
if (minContestantNumber == null || maxContestantNumber == null) {
|
||||
throw new BusinessException("minContestantNumber and maxContestantNumber are required.");
|
||||
}
|
||||
if (minContestantNumber > maxContestantNumber) {
|
||||
throw new BusinessException("minContestantNumber cannot be greater than maxContestantNumber.");
|
||||
}
|
||||
|
||||
// 1. 根据 contestantNumber 范围查询参赛者
|
||||
QueryWrapper<Contestant> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda()
|
||||
.ge(Contestant::getContestantNumber, minContestantNumber)
|
||||
.le(Contestant::getContestantNumber, maxContestantNumber)
|
||||
.orderByAsc(Contestant::getContestantNumber);
|
||||
List<Contestant> contestants = contestantMapper.selectList(queryWrapper);
|
||||
|
||||
if (contestants.isEmpty()) {
|
||||
log.info("No contestants found in range [{}, {}]", minContestantNumber, maxContestantNumber);
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// 2. 在内存中构建 ZIP
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(baos)) {
|
||||
|
||||
for (Contestant contestant : contestants) {
|
||||
Integer contestantNumber = contestant.getContestantNumber();
|
||||
if (contestantNumber == null) {
|
||||
log.warn("Contestant {} has no contestantNumber, skipping", contestant.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
String dirPrefix = contestantNumber + "/";
|
||||
|
||||
// 添加 PDF 文件
|
||||
String pdfPath = contestant.getPdfPath();
|
||||
if (StringUtils.isNotBlank(pdfPath)) {
|
||||
addMinioFileToZip(zos, pdfPath, dirPrefix + "design.pdf");
|
||||
}
|
||||
|
||||
// 添加视频文件
|
||||
String videoPath = contestant.getVideoPath();
|
||||
if (StringUtils.isNotBlank(videoPath)) {
|
||||
String fileName = videoPath.contains("/") ?
|
||||
videoPath.substring(videoPath.lastIndexOf("/") + 1) : "video.mp4";
|
||||
addMinioFileToZip(zos, videoPath, dirPrefix + fileName);
|
||||
}
|
||||
|
||||
// 添加参赛者信息 txt 文件
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("=== Contestant Information ===\n\n");
|
||||
sb.append("ID: ").append(nullSafe(contestant.getId())).append("\n");
|
||||
sb.append("Email: ").append(nullSafe(contestant.getEmail())).append("\n");
|
||||
sb.append("Contestant Number: ").append(contestantNumber).append("\n");
|
||||
sb.append("First Name: ").append(nullSafe(contestant.getFirstName())).append("\n");
|
||||
sb.append("Last Name: ").append(nullSafe(contestant.getLastName())).append("\n");
|
||||
sb.append("Gender: ").append(nullSafe(contestant.getGender())).append("\n");
|
||||
sb.append("Occupation: ").append(nullSafe(contestant.getOccupation())).append("\n");
|
||||
sb.append("Age: ").append(contestant.getAge() != null ? contestant.getAge() : "N/A").append("\n");
|
||||
sb.append("Country/Region/City: ").append(nullSafe(contestant.getCountryRegionCity())).append("\n");
|
||||
sb.append("Phone Number: ").append(nullSafe(contestant.getPhoneNumber())).append("\n");
|
||||
sb.append("Design Title: ").append(nullSafe(contestant.getDesignTitle())).append("\n");
|
||||
sb.append("Design Description: ").append(nullSafe(contestant.getDesignDescription())).append("\n");
|
||||
sb.append("PDF Path: ").append(nullSafe(pdfPath)).append("\n");
|
||||
sb.append("PDF Size (bytes): ").append(contestant.getPdfSize() != null ? contestant.getPdfSize() : "N/A").append("\n");
|
||||
sb.append("Video Path: ").append(nullSafe(videoPath)).append("\n");
|
||||
sb.append("Video Duration (seconds): ").append(contestant.getVideoDuration() != null ? contestant.getVideoDuration() : "N/A").append("\n");
|
||||
sb.append("Video Size (bytes): ").append(contestant.getVideoSize() != null ? contestant.getVideoSize() : "N/A").append("\n");
|
||||
sb.append("Portfolio URL: ").append(nullSafe(contestant.getPortfolioUrl())).append("\n");
|
||||
sb.append("Created At: ").append(contestant.getCreatedAt() != null ? contestant.getCreatedAt() : "N/A").append("\n");
|
||||
sb.append("Updated At: ").append(contestant.getUpdatedAt() != null ? contestant.getUpdatedAt() : "N/A").append("\n");
|
||||
|
||||
ZipEntry infoEntry = new ZipEntry(dirPrefix + "contestant_info.txt");
|
||||
zos.putNextEntry(infoEntry);
|
||||
zos.write(sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
zos.closeEntry();
|
||||
log.info("Added contestant {} info to zip", contestantNumber);
|
||||
}
|
||||
|
||||
zos.finish();
|
||||
log.info("ZIP built for {} contestants, size: {} bytes", contestants.size(), baos.size());
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MinIO 文件流式写入 ZIP,不落盘
|
||||
* @param zos ZIP 输出流
|
||||
* @param minioPath MinIO 路径(格式: bucketName/objectPath)
|
||||
* @param entryName ZIP 条目名称
|
||||
*/
|
||||
private void addMinioFileToZip(java.util.zip.ZipOutputStream zos, String minioPath, String entryName) {
|
||||
if (StringUtils.isBlank(minioPath)) {
|
||||
return;
|
||||
}
|
||||
int index = minioPath.indexOf("/");
|
||||
if (index == -1) {
|
||||
log.warn("Invalid MinIO path: {}", minioPath);
|
||||
return;
|
||||
}
|
||||
String bucketName = minioPath.substring(0, index);
|
||||
String objectName = minioPath.substring(index + 1);
|
||||
|
||||
try (InputStream in = minioUtil.download(bucketName, objectName)) {
|
||||
ZipEntry entry = new ZipEntry(entryName);
|
||||
zos.putNextEntry(entry);
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
zos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
zos.closeEntry();
|
||||
log.info("Added {} to zip ({} bytes)", entryName, entry.getSize());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add {} to zip: {}", entryName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContestantCountVO getContestantCount() {
|
||||
long count = contestantMapper.selectCount(null);
|
||||
Integer maxContestantNumber = null;
|
||||
QueryWrapper<Contestant> qMax = new QueryWrapper<>();
|
||||
qMax.isNotNull("contestant_number");
|
||||
qMax.orderByDesc("contestant_number");
|
||||
qMax.last("LIMIT 1");
|
||||
Contestant last = contestantMapper.selectOne(qMax);
|
||||
if (last != null) {
|
||||
maxContestantNumber = last.getContestantNumber();
|
||||
}
|
||||
return ContestantCountVO.builder()
|
||||
.count(count)
|
||||
.maxContestantNumber(maxContestantNumber)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String nullSafe(String value) {
|
||||
return value != null ? value : "N/A";
|
||||
}
|
||||
|
||||
private static final String RAW_VISIT_COUNT_KEY = "GLOBAL_AWARD:visit:raw";
|
||||
private static final String UNIQUE_VISIT_SET_KEY = "GLOBAL_AWARD:visit:unique";
|
||||
private static final String SESSION_VISIT_KEY_PREFIX = "GLOBAL_AWARD:visit:session:";
|
||||
private static final long SESSION_DEDUP_SECONDS = 5L;
|
||||
|
||||
@Override
|
||||
public void recordPageVisit(String sessionId) {
|
||||
redisUtil.increaseCount(RAW_VISIT_COUNT_KEY);
|
||||
|
||||
if (StringUtils.isNotBlank(sessionId)) {
|
||||
String sessionKey = SESSION_VISIT_KEY_PREFIX + sessionId;
|
||||
if (!redisUtil.hasKey(sessionKey)) {
|
||||
redisUtil.increaseCount(UNIQUE_VISIT_SET_KEY);
|
||||
redisUtil.addToString(sessionKey, "1", SESSION_DEDUP_SECONDS);
|
||||
}
|
||||
} else {
|
||||
redisUtil.increaseCount(UNIQUE_VISIT_SET_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageVisitCountVO getPageVisitCount() {
|
||||
Long raw = redisUtil.getIncrementCount(RAW_VISIT_COUNT_KEY);
|
||||
Long unique = redisUtil.getIncrementCount(UNIQUE_VISIT_SET_KEY);
|
||||
return PageVisitCountVO.builder()
|
||||
.rawVisitCount(raw != null ? raw : 0L)
|
||||
.uniqueVisitCount(unique != null ? unique : 0L)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.netty.util.internal.StringUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -53,7 +54,11 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
private final PythonTAllInfoService pythonTAllInfoService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LibraryModelPointVO saveOrEditTemplatePoint(LibraryModelPointDTO libraryModelPointDTO) {
|
||||
// 参数校验
|
||||
validateInputParams(libraryModelPointDTO);
|
||||
|
||||
LibraryModelPointVO libraryModelPointVO = CopyUtil.copyObject(libraryModelPointDTO, LibraryModelPointVO.class);
|
||||
|
||||
// 不管是保存还是另存为,都需要传模特的libraryId
|
||||
@@ -71,7 +76,8 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
// 更新模特图片
|
||||
if (flag) {
|
||||
libModel.setUrl(url);
|
||||
libModel.setMd5(MD5Utils.encryptFile(minioUtil.getPreSignedUrl(url, CommonConstant.MINIO_IMAGE_EXPIRE_TIME), false));
|
||||
String preSignedUrl = minioUtil.getPreSignedUrl(url, CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
|
||||
libModel.setMd5(MD5Utils.encryptFile(preSignedUrl, false));
|
||||
List<Integer> imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(url);
|
||||
libModel.setWidth(imagesWidthAndHeight.get(0));
|
||||
libModel.setHigh(imagesWidthAndHeight.get(1));
|
||||
@@ -104,25 +110,10 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
|
||||
} else {
|
||||
// 不覆盖,即另存为
|
||||
// 新增模特library信息
|
||||
Library saveAsModel = new Library();
|
||||
saveAsModel.setAccountId(libModel.getAccountId());
|
||||
saveAsModel.setLevel1Type(libModel.getLevel1Type());
|
||||
saveAsModel.setLevel2Type(libModel.getLevel2Type());
|
||||
String ageGroup = StringUtil.isNullOrEmpty(libModel.getLevel3Type()) ? "Adult" : libModel.getLevel3Type();
|
||||
saveAsModel.setLevel3Type(ageGroup);
|
||||
saveAsModel.setName(DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD));
|
||||
saveAsModel.setUrl(url);
|
||||
saveAsModel.setMd5(MD5Utils.encryptFile(minioUtil.getPreSignedUrl(url, CommonConstant.MINIO_IMAGE_EXPIRE_TIME), false));
|
||||
List<Integer> imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(url);
|
||||
saveAsModel.setWidth(imagesWidthAndHeight.get(0));
|
||||
saveAsModel.setHigh(imagesWidthAndHeight.get(1));
|
||||
saveAsModel.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
||||
libraryService.save(saveAsModel);
|
||||
// 更新新的模特在library中的id,用于后面新建模特点位信息用
|
||||
libraryModelPointDTO.setLibraryId(saveAsModel.getId());
|
||||
Library saveAsModel = createNewLibraryCopy(libModel, libraryModelPointDTO);
|
||||
|
||||
// 新增模特点位信息
|
||||
libraryModelPointDTO.setLibraryId(saveAsModel.getId()); // 更新libraryId为新创建的模型ID
|
||||
LibraryModelPoint libraryModelPoint = resolvePoint(libraryModelPointDTO);
|
||||
libraryModelPoint.setModelType("Library");
|
||||
libraryModelPoint.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
||||
@@ -130,22 +121,50 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
libraryModelPointVO.setTemplateId(libraryModelPoint.getId());
|
||||
libraryModelPointVO.setRelationId(libraryModelPoint.getRelationId());
|
||||
}
|
||||
//编辑
|
||||
/*if (!StringUtils.isEmpty(libraryModelPointDTO.getModelSex())) {
|
||||
Library byId = libraryService.getById(libraryModelPointDTO.getLibraryId());
|
||||
if (!byId.getLevel2Type().equals(libraryModelPointDTO.getModelSex())) {
|
||||
if (byId.getLevel2Type().equals(Sex.FEMALE.getValue())) {
|
||||
libraryService.checkModel(Sex.FEMALE.getValue(), Collections.singletonList(byId.getId()), 1);
|
||||
}else {
|
||||
libraryService.checkModel(Sex.MALE.getValue(), Collections.singletonList(byId.getId()), 1);
|
||||
}
|
||||
byId.setLevel2Type(libraryModelPointDTO.getModelSex());
|
||||
libraryService.updateById(byId);
|
||||
}
|
||||
}*/
|
||||
return libraryModelPointVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证输入参数
|
||||
*/
|
||||
private void validateInputParams(LibraryModelPointDTO libraryModelPointDTO) {
|
||||
if (libraryModelPointDTO == null) {
|
||||
throw new BusinessException("libraryModelPointDTO cannot be null");
|
||||
}
|
||||
if (libraryModelPointDTO.getLibraryId() == null || libraryModelPointDTO.getLibraryId() <= 0) {
|
||||
throw new BusinessException("libraryId is required");
|
||||
}
|
||||
if (StringUtils.isEmpty(libraryModelPointDTO.getModelPath())) {
|
||||
throw new BusinessException("modelPath is required");
|
||||
}
|
||||
if (StringUtils.isEmpty(libraryModelPointDTO.getTimeZone())) {
|
||||
throw new BusinessException("timeZone is required");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的库模型副本
|
||||
*/
|
||||
private Library createNewLibraryCopy(Library originalModel, LibraryModelPointDTO libraryModelPointDTO) {
|
||||
// 新增模特library信息
|
||||
Library saveAsModel = new Library();
|
||||
saveAsModel.setAccountId(originalModel.getAccountId());
|
||||
saveAsModel.setLevel1Type(originalModel.getLevel1Type());
|
||||
saveAsModel.setLevel2Type(originalModel.getLevel2Type());
|
||||
String ageGroup = StringUtil.isNullOrEmpty(originalModel.getLevel3Type()) ? "Adult" : originalModel.getLevel3Type();
|
||||
saveAsModel.setLevel3Type(ageGroup);
|
||||
saveAsModel.setName(DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD));
|
||||
saveAsModel.setUrl(libraryModelPointDTO.getModelPath());
|
||||
String preSignedUrl = minioUtil.getPreSignedUrl(libraryModelPointDTO.getModelPath(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
|
||||
saveAsModel.setMd5(MD5Utils.encryptFile(preSignedUrl, false));
|
||||
List<Integer> imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(libraryModelPointDTO.getModelPath());
|
||||
saveAsModel.setWidth(imagesWidthAndHeight.get(0));
|
||||
saveAsModel.setHigh(imagesWidthAndHeight.get(1));
|
||||
saveAsModel.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
||||
libraryService.save(saveAsModel);
|
||||
return saveAsModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LibraryModelPointVO saveOrEditTemplatePointOld(LibraryModelPointDTO libraryModelPointDTO) {
|
||||
// Library library = libraryService.getById(libraryModelPointDTO.getLibraryId());
|
||||
|
||||
@@ -79,6 +79,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
throw new BusinessException("type.cannot.be.empty");
|
||||
}
|
||||
Long accountId = UserContext.getUserHolder().getId();
|
||||
Account account = accountService.getById(accountId);
|
||||
// 查动态
|
||||
if (!StringUtils.isNullOrEmpty(getNotificationDTO.getType()) && getNotificationDTO.getType().equals("newPosted")) {
|
||||
return getNewPosted(accountId, getNotificationDTO.getPage(), getNotificationDTO.getSize());
|
||||
@@ -92,6 +93,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
|
||||
if (getNotificationDTO.getType().equals("system")) {
|
||||
queryWrapper.lambda().eq(Notification::getType, "system")
|
||||
.gt(Notification::getCreateTime, account.getCreateDate())
|
||||
.and(wrapper -> wrapper
|
||||
.isNull(Notification::getReceiverId)
|
||||
.or()
|
||||
@@ -103,7 +105,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
|
||||
Page<Notification> notificationPage = baseMapper.selectPage(new Page<>(getNotificationDTO.getPage(), getNotificationDTO.getSize()), queryWrapper);
|
||||
|
||||
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId);
|
||||
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId, account.getCreateDate());
|
||||
IPage<NotificationVO> convert = notificationPage.convert(o -> {
|
||||
NotificationVO notificationVO = CopyUtil.copyObject(o, NotificationVO.class);
|
||||
Account senderAccount = accountService.getById(notificationVO.getSenderId());
|
||||
@@ -192,6 +194,8 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
if (!type.equals("system")) {
|
||||
// 个人未读消息
|
||||
count = getUnreadCountByType(type, receiverId);
|
||||
} else if (Objects.isNull(receiverId)) {
|
||||
count = 1L;
|
||||
} else {
|
||||
// 系统未读消息
|
||||
count = getUnreadSystemNotification(receiverId);
|
||||
@@ -247,6 +251,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
|
||||
private Long getUnreadSystemNotification(Long receiverId) {
|
||||
// Long accountId = UserContext.getUserHolder().getId();
|
||||
Account account = accountService.getById(receiverId);
|
||||
// 计算总的系统通知数量
|
||||
QueryWrapper<Notification> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda().eq(Notification::getType, "system")
|
||||
@@ -255,6 +260,9 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
.or()
|
||||
.eq(Notification::getReceiverId, receiverId)
|
||||
);
|
||||
if (Objects.nonNull(account)) {
|
||||
queryWrapper.lambda().gt(Notification::getCreateTime, account.getCreateDate());
|
||||
}
|
||||
Long totalSysCount = baseMapper.selectCount(queryWrapper);
|
||||
|
||||
// 计算单个用户读了多少条系统数据
|
||||
@@ -302,6 +310,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
// 一键已读
|
||||
public void setReadAll(String type) {
|
||||
Long accountId = UserContext.getUserHolder().getId();
|
||||
Account account = accountService.getById(accountId);
|
||||
// 指定某个用户的某种类型的数据,将未读数据全部已读
|
||||
if (!type.equals("system")) {
|
||||
// 个人消息已读
|
||||
@@ -309,7 +318,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
} else {
|
||||
// 系统消息已读
|
||||
// 1、先确定当前用户未读的系统消息有哪些
|
||||
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId);
|
||||
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId, account.getCreateDate());
|
||||
// 2、将未读的设为已读
|
||||
if (!unreadSysNotificationIds.isEmpty()) setReadStatusSystem(unreadSysNotificationIds);
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -90,7 +91,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
}
|
||||
|
||||
public OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
||||
HttpServletRequest request, byte autoRenewal) {
|
||||
HttpServletRequest request) {
|
||||
|
||||
//获取商品信息
|
||||
// Product product = productMapper.selectById(amount);
|
||||
@@ -276,10 +277,11 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
|
||||
public void updateTotalFeeByOrderNo(String orderNo) {
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("order_no", orderNo);
|
||||
qw.eq("order_no", orderNo).in("trade_state", Arrays.asList("paid", "COMPLETED", ""));
|
||||
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qw);
|
||||
Float sum = paymentInfos.stream()
|
||||
.map(PaymentInfo::getPayerTotal)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(0f, Float::sum);
|
||||
|
||||
baseMapper.update(
|
||||
|
||||
@@ -258,6 +258,11 @@ public class PanToneServiceImpl extends ServiceImpl<PanToneMapper, PanTone> impl
|
||||
d.setH(getRgbByHsvBatchDTO.getH());
|
||||
d.setS(getRgbByHsvBatchDTO.getS());
|
||||
d.setV(getRgbByHsvBatchDTO.getV());
|
||||
// 不使用数据库中存储的RGB值,使用通过hsv计算得到的RGB值
|
||||
int[] rgb = PantoneUtils.hsvToRgb(d.getH(), d.getS(), d.getV());
|
||||
d.setR(rgb[0]);
|
||||
d.setG(rgb[1]);
|
||||
d.setB(rgb[2]);
|
||||
}
|
||||
});
|
||||
Map<Integer, PantoneVO> valueToPantoneVo = templateResposne.stream().collect(Collectors.toMap(
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.ai.da.service.impl;
|
||||
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.common.enums.PayTypeEnum;
|
||||
import com.ai.da.common.enums.PaymentInfoType;
|
||||
import com.ai.da.common.response.PageBaseResponse;
|
||||
import com.ai.da.common.utils.SpringUtils;
|
||||
import com.ai.da.mapper.primary.PaymentInfoMapper;
|
||||
import com.ai.da.mapper.primary.ProductCouponsMapper;
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.PaymentInfo;
|
||||
import com.ai.da.mapper.primary.entity.ProductCoupons;
|
||||
@@ -20,13 +22,17 @@ import com.google.gson.Gson;
|
||||
import com.paypal.orders.Order;
|
||||
import com.stripe.Stripe;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.Subscription;
|
||||
import com.stripe.model.*;
|
||||
import com.stripe.model.checkout.Session;
|
||||
import com.stripe.net.RequestOptions;
|
||||
import com.stripe.param.InvoicePaymentListParams;
|
||||
import com.stripe.param.InvoiceRetrieveParams;
|
||||
import com.stripe.param.SubscriptionRetrieveParams;
|
||||
import com.stripe.param.checkout.SessionRetrieveParams;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -49,6 +55,9 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
|
||||
@Resource
|
||||
private ProductCouponsMapper productCouponsMapper;
|
||||
|
||||
/**
|
||||
* 记录支付日志:微信支付
|
||||
* @param plainText
|
||||
@@ -194,38 +203,199 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
baseMapper.insert(paymentInfo);
|
||||
}
|
||||
|
||||
public void createOrUpdatePaymentInfoForStripe(Session session){
|
||||
String orderId = session.getMetadata().get("orderId");
|
||||
String status = session.getStatus();
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
/**
|
||||
* 为 Stripe Checkout Session 创建支付记录
|
||||
*
|
||||
* 策略:根据 session.getMode() 分流:
|
||||
* - mode=subscription:直接获取 session.getInvoice(),委托给 Invoice 版本(最完整)
|
||||
* - mode=payment:从 session.getPaymentIntentObject() 获取支付信息,兼容无 Invoice 场景
|
||||
*
|
||||
* @param session Stripe Checkout Session
|
||||
*/
|
||||
public void createOrUpdatePaymentInfoForStripe(Session session) {
|
||||
Stripe.apiKey = privateKey;
|
||||
|
||||
String sessionId = session.getId();
|
||||
String orderNo = session.getMetadata().get("orderId");
|
||||
String mode = session.getMode();
|
||||
String type = PaymentInfoType.CREDIT.getType();
|
||||
|
||||
// 从 Session 的 PaymentIntent 获取支付方式信息(两种 mode 都适用)
|
||||
Map<String, String> paymentMethodInfo = handlePaymentMethodBySession(session, mode);
|
||||
|
||||
String invoiceId = session.getInvoice();
|
||||
Invoice invoice = null;
|
||||
if (!StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
try {
|
||||
invoice = Invoice.retrieve(invoiceId);
|
||||
} catch (StripeException e) {
|
||||
log.warn("[createOrUpdatePaymentInfoForStripe(Session)] 订阅模式获取 Invoice 失败,降级为 payment 模式处理,sessionId={},error={}",
|
||||
sessionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
// subscription mode:获取 Invoice,委托给 Invoice 方法(传入已获取的 paymentMethodInfo)
|
||||
if ("subscription".equals(mode)) {
|
||||
if (invoice != null) {
|
||||
createOrUpdatePaymentInfoForStripe(invoice, paymentMethodInfo, session.getDiscounts());
|
||||
log.info("[createOrUpdatePaymentInfoForStripe(Session)] subscription 模式通过 Invoice 创建支付记录,invoiceId={}", invoiceId);
|
||||
return;
|
||||
}
|
||||
type = PaymentInfoType.NEW.getType();
|
||||
}
|
||||
|
||||
|
||||
// payment mode / 降级:使用 session 自有字段创建支付记录
|
||||
String status = session.getPaymentStatus();
|
||||
Long amountTotal = session.getAmountTotal();
|
||||
// stripe 的支付金额单位是分
|
||||
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||
|
||||
PaymentInfo paymentInfo = new PaymentInfo();
|
||||
paymentInfo.setOrderNo(orderId);
|
||||
paymentInfo.setOrderNo(orderNo);
|
||||
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
||||
paymentInfo.setTransactionId(sessionId);
|
||||
paymentInfo.setTransactionId(invoiceId);
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setPayerTotal(divide);
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(session);
|
||||
paymentInfo.setContent(json);
|
||||
// 获取订单信息
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId);
|
||||
if (!Objects.isNull(orderByOrderNo)){
|
||||
paymentInfo.setContent(gson.toJson(session));
|
||||
paymentInfo.setType(type);
|
||||
paymentInfo.setNotified(0);
|
||||
paymentInfo.setPaymentMethod(paymentMethodInfo.getOrDefault("paymentMethod", "N/A"));
|
||||
paymentInfo.setLast4(paymentMethodInfo.getOrDefault("last4", "N/A"));
|
||||
paymentInfo.setHostedInvoiceUrl(invoice == null ? null : invoice.getHostedInvoiceUrl());
|
||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (!Objects.isNull(orderByOrderNo)) {
|
||||
paymentInfo.setCountry(orderByOrderNo.getCountry());
|
||||
paymentInfo.setCity(orderByOrderNo.getCity());
|
||||
paymentInfo.setIpAddress(orderByOrderNo.getIpAddress());
|
||||
}
|
||||
|
||||
baseMapper.insert(paymentInfo);
|
||||
log.info("[createOrUpdatePaymentInfoForStripe(Session)] payment 模式创建支付记录,sessionId={},orderNo={}", sessionId, orderNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一获取支付方式信息
|
||||
* @param sessionId 可选的 sessionId
|
||||
* @param subscriptionId 可选的 subscriptionId
|
||||
* @return paymentMethodInfo Map,包含 paymentMethod 和 last4
|
||||
*/
|
||||
public Map<String, String> getPaymentMethodInfo(String sessionId, String subscriptionId) {
|
||||
PaymentMethod paymentMethod = null;
|
||||
|
||||
if (!StringUtil.isNullOrEmpty(sessionId)) {
|
||||
paymentMethod = getPaymentMethodBySessionId(sessionId);
|
||||
} else if (!StringUtil.isNullOrEmpty(subscriptionId)) {
|
||||
paymentMethod = getPaymentMethodBySubscriptionId(subscriptionId);
|
||||
}
|
||||
|
||||
return getPaymentMethodMap(paymentMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Session 获取支付方式信息
|
||||
* @param session Stripe Checkout Session
|
||||
* @param mode session 模式:subscription 或 payment
|
||||
* @return paymentMethodInfo Map
|
||||
*/
|
||||
public Map<String, String> handlePaymentMethodBySession(Session session, String mode) {
|
||||
PaymentMethod paymentMethod;
|
||||
|
||||
if ("subscription".equals(mode)) {
|
||||
String subscriptionId = session.getSubscription();
|
||||
paymentMethod = getPaymentMethodBySubscriptionId(subscriptionId);
|
||||
} else {
|
||||
paymentMethod = getPaymentMethodBySessionId(session.getId());
|
||||
}
|
||||
|
||||
return getPaymentMethodMap(paymentMethod);
|
||||
}
|
||||
|
||||
public String getPromotionCodeByPromotionCodeId(String promotionCodeId) {
|
||||
if (StringUtil.isNullOrEmpty(promotionCodeId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ProductCoupons promotionCode = productCouponsMapper.selectOne(new QueryWrapper<ProductCoupons>().lambda().eq(ProductCoupons::getPromotionCodeId, promotionCodeId));
|
||||
if (promotionCode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return promotionCode.getPromotionCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 sessionId 获取 PaymentMethod
|
||||
* @param sessionId Stripe Session ID
|
||||
* @return PaymentMethod 对象
|
||||
*/
|
||||
private PaymentMethod getPaymentMethodBySessionId(String sessionId) {
|
||||
Stripe.apiKey = privateKey;
|
||||
SessionRetrieveParams params = SessionRetrieveParams.builder()
|
||||
.addExpand("payment_intent")
|
||||
.addExpand("payment_intent.payment_method")
|
||||
.build();
|
||||
|
||||
Session fullSession;
|
||||
try {
|
||||
fullSession = Session.retrieve(sessionId, params, RequestOptions.builder().build());
|
||||
} catch (StripeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
PaymentIntent paymentIntent = fullSession.getPaymentIntentObject();
|
||||
return paymentIntent != null ? paymentIntent.getPaymentMethodObject() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 subscriptionId 获取 PaymentMethod
|
||||
* @param subscriptionId Stripe Subscription ID
|
||||
* @return PaymentMethod 对象
|
||||
*/
|
||||
public PaymentMethod getPaymentMethodBySubscriptionId(String subscriptionId) {
|
||||
Stripe.apiKey = privateKey;
|
||||
SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder()
|
||||
.addExpand("default_payment_method")
|
||||
.build();
|
||||
try {
|
||||
Subscription subscription = Subscription.retrieve(subscriptionId, params, RequestOptions.builder().build());
|
||||
return subscription.getDefaultPaymentMethodObject();
|
||||
} catch (StripeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, String> getPaymentMethodMap(PaymentMethod paymentMethod) {
|
||||
Map<String, String> paymentMethodInfo = new HashMap<>();
|
||||
if (paymentMethod != null && paymentMethod.getCard() != null) {
|
||||
String brand = paymentMethod.getCard().getBrand();
|
||||
brand = brand.substring(0, 1).toUpperCase() + brand.substring(1);
|
||||
paymentMethodInfo.put("paymentMethod", brand + " " + paymentMethod.getCard().getFunding() + " Card");
|
||||
paymentMethodInfo.put("last4", paymentMethod.getCard().getLast4());
|
||||
} else if (paymentMethod != null) {
|
||||
paymentMethodInfo.put("paymentMethod", StringUtils.capitalize(paymentMethod.getType()));
|
||||
paymentMethodInfo.put("last4", "N/A");
|
||||
} else {
|
||||
paymentMethodInfo.put("paymentMethod", "N/A");
|
||||
paymentMethodInfo.put("last4", "N/A");
|
||||
}
|
||||
return paymentMethodInfo;
|
||||
}
|
||||
|
||||
@Value("${stripe.private-key}")
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 为 Stripe Invoice 创建或更新支付记录
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @param paymentMethodInfo 外部传入的支付方式信息(如从 Session 传入),优先使用,为空时内部重新获取
|
||||
* @return PaymentInfo 支付记录
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice){
|
||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice, Map<String, String> paymentMethodInfo, List<Session.Discount> discounts) {
|
||||
Stripe.apiKey = privateKey;
|
||||
StripeService stripeService = SpringUtils.getBean(StripeService.class);
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
@@ -235,29 +405,61 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
qw.eq("transaction_id", invoiceId);
|
||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||
String status = invoice.getStatus();
|
||||
// 判断是否有优惠码
|
||||
// 判断是否有优惠码 续订不会使用优惠码,故取消这部分代码
|
||||
// Stripe SDK 32.0.0: invoice.getDiscount() 已移除,使用 invoice.getDiscountObject().get() 替代
|
||||
String promotionCode = null;
|
||||
if (Objects.nonNull(invoice.getDiscount()) && !StringUtil.isNullOrEmpty(invoice.getDiscount().getPromotionCode())){
|
||||
ProductCoupons productCoupon = stripeService.getProductCoupon(null, invoice.getDiscount().getPromotionCode());
|
||||
promotionCode = productCoupon.getPromotionCode();
|
||||
if (!CollectionUtils.isEmpty(discounts)) {
|
||||
promotionCode = getPromotionCodeByPromotionCodeId(discounts.getFirst().getPromotionCode());
|
||||
}
|
||||
// 判断当前支付是否已经被记录,确保同一个支付不会被重复记录
|
||||
if (Objects.isNull(paymentInfo)){
|
||||
String orderNo;
|
||||
String orderNo = null;
|
||||
String billingReason = invoice.getBillingReason();
|
||||
String paymentIntentIdForCharge = null;
|
||||
try {
|
||||
if (invoice.getBillingReason().equals("manual")){
|
||||
if ("manual".equals(billingReason)){
|
||||
// 手动创建的发票,针对one-time支付
|
||||
// orderNo = invoice.getLines().getData().get(0).getPrice().getMetadata().get("orderId");
|
||||
// 当支付失败时,chargeId为空
|
||||
String chargeId = invoice.getCharge();
|
||||
orderNo = Charge.retrieve(chargeId).getDescription().replace("AiDA - ", "");
|
||||
}else {
|
||||
String subscriptionId = invoice.getSubscription();
|
||||
// 从subscription中获取orderNo
|
||||
orderNo = Subscription.retrieve(subscriptionId).getDescription().replace("AiDA - ", "");
|
||||
// 获取 PaymentIntent 用于后续获取 chargeId 和支付方式
|
||||
paymentIntentIdForCharge = getPaymentIntentByInvoice(invoice);
|
||||
if (!StringUtil.isNullOrEmpty(paymentIntentIdForCharge)) {
|
||||
PaymentIntent paymentIntent = PaymentIntent.retrieve(paymentIntentIdForCharge);
|
||||
String chargeId = paymentIntent.getLatestCharge();
|
||||
if (!StringUtil.isNullOrEmpty(chargeId)) {
|
||||
Charge charge = Charge.retrieve(chargeId);
|
||||
String description = charge.getDescription();
|
||||
orderNo = description != null ? description.replace("AiDA - ", "") : null;
|
||||
}
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(orderNo)) {
|
||||
orderNo = extractOrderNoFromInvoiceLines(invoice);
|
||||
}
|
||||
} else {
|
||||
// Stripe SDK 32.0.0: invoice.getSubscription() 已移除
|
||||
// 方案A:直接从 invoice.getParent().getSubscriptionDetails().getMetadata() 获取 orderId(SDK 32.0.0 新方式)
|
||||
String orderNoFromParent = getOrderNoFromInvoiceParent(invoice);
|
||||
if (!StringUtil.isNullOrEmpty(orderNoFromParent)) {
|
||||
orderNo = orderNoFromParent;
|
||||
log.info("[createOrUpdatePaymentInfoForStripe] 从 invoice.getParent().getSubscriptionDetails() 获取到 orderNo={}", orderNo);
|
||||
} else {
|
||||
// 方案B:从 subscription 获取 orderNo
|
||||
String subscriptionId = getSubscriptionByInvoice(invoice);
|
||||
if (!StringUtil.isNullOrEmpty(subscriptionId)) {
|
||||
try {
|
||||
Subscription subscription = Subscription.retrieve(subscriptionId);
|
||||
orderNo = getOrderNoBySubscription(subscription);
|
||||
} catch (StripeException e) {
|
||||
log.warn("[createOrUpdatePaymentInfoForStripe] 获取 Subscription 失败,subscriptionId={}, error={}", subscriptionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
// 方案C:备用方案,从 invoice metadata 获取
|
||||
if (StringUtil.isNullOrEmpty(orderNo)) {
|
||||
orderNo = extractOrderNoFromInvoiceMetadata(invoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
throw new RuntimeException(e);
|
||||
log.error("[createOrUpdatePaymentInfoForStripe] 获取订单号失败,invoiceId={}, error={}", invoiceId, e.getMessage());
|
||||
throw new RuntimeException("Failed to retrieve orderNo from invoice: " + invoiceId, e);
|
||||
}
|
||||
Long amountTotal;
|
||||
if (status.equals("paid")){
|
||||
@@ -268,11 +470,9 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
|
||||
// stripe 的支付金额单位是分,在我们数据库中金额单位为 元
|
||||
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||
String type = invoice.getBillingReason().equals("subscription_create") ? "new" :
|
||||
invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason();
|
||||
String type = invoice.getBillingReason().equals("subscription_create") ? PaymentInfoType.NEW.getType() :
|
||||
invoice.getBillingReason().equals("subscription_cycle") ? PaymentInfoType.RENEWAL.getType() : invoice.getBillingReason();
|
||||
|
||||
// 获取支付方式
|
||||
Map<String, String> paymentMethod = stripeService.getPaymentMethodByInvoiceId(invoiceId);
|
||||
// 获取订单信息
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
|
||||
@@ -288,8 +488,8 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
paymentInfo.setContent(json);
|
||||
paymentInfo.setType(type);
|
||||
paymentInfo.setNotified(0);
|
||||
paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod"));
|
||||
paymentInfo.setLast4(paymentMethod.get("last4"));
|
||||
paymentInfo.setPaymentMethod(paymentMethodInfo.get("paymentMethod"));
|
||||
paymentInfo.setLast4(paymentMethodInfo.get("last4"));
|
||||
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
|
||||
paymentInfo.setPromotionCode(promotionCode);
|
||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||
@@ -300,87 +500,85 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
}
|
||||
int row = baseMapper.insertIgnore(paymentInfo);
|
||||
log.info("Payment Info insert affect rows:{}", row);
|
||||
}else {
|
||||
orderInfoService.updateTotalFeeByOrderNo(orderNo);
|
||||
} else {
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setPromotionCode(promotionCode);
|
||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(paymentInfo);
|
||||
}
|
||||
|
||||
return paymentInfo;
|
||||
}
|
||||
|
||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge){
|
||||
Stripe.apiKey = privateKey;
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
// todo 首次支付失败,没有invoiceId,所以如果这个order之后成功支付后,会有多条paymentInfo 是否需要优化??
|
||||
qw.eq("transaction_id", charge.getInvoice());
|
||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||
Charge.PaymentMethodDetails paymentMethodDetails = charge.getPaymentMethodDetails();
|
||||
String paymentMethod;
|
||||
String last4 = "N/A";
|
||||
switch (paymentMethodDetails.getType()){
|
||||
case "alipay":
|
||||
paymentMethod = "Alipay";
|
||||
break;
|
||||
case "bancontact":
|
||||
paymentMethod = "BanContact";
|
||||
break;
|
||||
case "card":
|
||||
Charge.PaymentMethodDetails.Card card = paymentMethodDetails.getCard();
|
||||
String brand = card.getBrand();
|
||||
brand = brand.substring(0, 1).toUpperCase() + brand.substring(1);
|
||||
paymentMethod = brand + " " + card.getFunding() + "card";
|
||||
last4 = card.getLast4();
|
||||
break;
|
||||
case "eps":
|
||||
Charge.PaymentMethodDetails.Eps eps = paymentMethodDetails.getEps();
|
||||
paymentMethod = eps.getBank();
|
||||
break;
|
||||
case "giropay":
|
||||
paymentMethod = "GiroPay";
|
||||
break;
|
||||
case "ideal":
|
||||
Charge.PaymentMethodDetails.Ideal ideal = paymentMethodDetails.getIdeal();
|
||||
paymentMethod = ideal.getBank();
|
||||
break;
|
||||
case "link":
|
||||
paymentMethod = "Link";
|
||||
break;
|
||||
default:
|
||||
paymentMethod = "N/A";
|
||||
}
|
||||
if (Objects.isNull(paymentInfo)){
|
||||
Stripe.apiKey = privateKey;
|
||||
|
||||
String orderNo = charge.getDescription().replace("AiDA - ", "");
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
Float divide = new BigDecimal(charge.getAmount()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||
paymentInfo = new PaymentInfo();
|
||||
paymentInfo.setOrderNo(orderNo);
|
||||
paymentInfo.setTransactionId(charge.getInvoice());
|
||||
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
||||
paymentInfo.setTradeState(charge.getStatus());
|
||||
paymentInfo.setPayerTotal(divide);
|
||||
paymentInfo.setNotified(0);
|
||||
paymentInfo.setPaymentMethod(paymentMethod);
|
||||
paymentInfo.setLast4(last4);
|
||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||
if (!Objects.isNull(orderByOrderNo)){
|
||||
paymentInfo.setCountry(orderByOrderNo.getCountry());
|
||||
paymentInfo.setCity(orderByOrderNo.getCity());
|
||||
paymentInfo.setIpAddress(orderByOrderNo.getIpAddress());
|
||||
private String getPaymentIntentByInvoice(Invoice invoice) {
|
||||
// 从 invoice.getPayments() 获取(适用于已支付完成的 Invoice)
|
||||
// SDK 32.0.0: invoice.getPayments() 可能为 null,需逐层判空
|
||||
try {
|
||||
InvoicePaymentCollection payments = invoice.getPayments();
|
||||
if (payments != null) {
|
||||
List<InvoicePayment> invoicePayments = payments.getData();
|
||||
if (invoicePayments != null && !invoicePayments.isEmpty()) {
|
||||
InvoicePayment firstPayment = invoicePayments.getFirst();
|
||||
if (firstPayment != null) {
|
||||
InvoicePayment.Payment payment = firstPayment.getPayment();
|
||||
if (payment != null) {
|
||||
PaymentIntent paymentIntent = payment.getPaymentIntentObject();
|
||||
if (paymentIntent != null) {
|
||||
return paymentIntent.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int row = baseMapper.insertIgnore(paymentInfo);
|
||||
log.info("Payment Info insert affect rows:{}", row);
|
||||
}else {
|
||||
paymentInfo.setTradeState(charge.getStatus());
|
||||
paymentInfo.setPaymentMethod(paymentMethod);
|
||||
paymentInfo.setLast4(last4);
|
||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(paymentInfo);
|
||||
} catch (Exception e) {
|
||||
log.warn("[getPaymentIntentByInvoice] 获取 PaymentIntent 失败,invoiceId={},error={}", invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getPromotionCodeByInvoice(Invoice invoice) throws StripeException {
|
||||
// 1. 检索 Invoice 并展开 discounts 字段
|
||||
InvoiceRetrieveParams params = InvoiceRetrieveParams.builder()
|
||||
.addExpand("discounts") // 展开折扣数组
|
||||
.build();
|
||||
|
||||
invoice = Invoice.retrieve(invoice.getId(), params, null);
|
||||
|
||||
// 2. 获取折扣列表(注意:Invoice.Discount 不是 List<Discount>)
|
||||
List<Discount> invoiceDiscounts = invoice.getDiscountObjects();
|
||||
|
||||
if (invoiceDiscounts == null || invoiceDiscounts.isEmpty()) {
|
||||
log.info("No discounts applied to this invoice");
|
||||
return null;
|
||||
}
|
||||
|
||||
return paymentInfo;
|
||||
// 3. 遍历每个折扣(通常只有一个)
|
||||
for (Discount discount : invoiceDiscounts) {
|
||||
// // 获取 source 对象(包含优惠券信息)
|
||||
// Discount.Source source = discount.getSource();
|
||||
//
|
||||
// if (source != null && "coupon".equals(source.getType())) {
|
||||
// // source.coupon 可能是 ID 字符串,也可能是已展开的 Coupon 对象
|
||||
// Object couponObj = source.getCoupon();
|
||||
//
|
||||
// if (couponObj instanceof String) {
|
||||
// String couponId = (String) couponObj;
|
||||
// // 需要通过 ID 单独检索 Coupon
|
||||
// Coupon coupon = Coupon.retrieve(couponId);
|
||||
// System.out.println("Coupon ID: " + coupon.getId());
|
||||
// System.out.println("Coupon name: " + coupon.getName());
|
||||
// } else if (couponObj instanceof Coupon) {
|
||||
// Coupon coupon = (Coupon) couponObj;
|
||||
// System.out.println("Coupon ID: " + coupon.getId());
|
||||
// }
|
||||
// }
|
||||
// // 获取其他折扣信息
|
||||
// Long start = discount.getStart(); // 折扣开始时间
|
||||
// Long end = discount.getEnd(); // 折扣结束时间(可能为 null)
|
||||
return discount.getPromotionCode(); // 关联的促销码 ID
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -439,26 +637,234 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
return baseMapper.selectPaidPaymentsByAccountAndPromotion(accountId, promCode);
|
||||
}
|
||||
|
||||
public PaymentInfo updatePaymentRefundStatus(Charge charge){
|
||||
// 判断当前退款是部分退款还是全部退款
|
||||
|
||||
/**
|
||||
* 通过 chargeId 更新支付记录的退款状态
|
||||
* 从 charge 获取关联的 invoiceId,再更新 paymentInfo
|
||||
*
|
||||
* @param charge Stripe Charge 对象
|
||||
* @param status 新的交易状态
|
||||
*/
|
||||
@Override
|
||||
public void updatePaymentRefundStatusByChargeId(Charge charge, String status) {
|
||||
if (charge == null) {
|
||||
log.warn("[updatePaymentRefundStatusByChargeId] charge 为空,跳过");
|
||||
return;
|
||||
}
|
||||
String chargeId = charge.getId();
|
||||
Stripe.apiKey = privateKey;
|
||||
String invoiceId = extractInvoiceIdFromCharge(charge);
|
||||
if (!StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
updatePaymentRefundStatusByInvoiceId(invoiceId, status);
|
||||
} else {
|
||||
log.warn("[updatePaymentRefundStatusByChargeId] 无法从 charge 获取 invoiceId,chargeId={}", chargeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 invoiceId 更新 paymentInfo 表的退款状态
|
||||
*
|
||||
* @param invoiceId Stripe Invoice ID(对应 paymentInfo.transactionId)
|
||||
* @param status 新的交易状态
|
||||
*/
|
||||
@Override
|
||||
public void updatePaymentRefundStatusByInvoiceId(String invoiceId, String status) {
|
||||
if (StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
log.warn("[updatePaymentRefundStatusByInvoiceId] invoiceId 为空,跳过");
|
||||
return;
|
||||
}
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("transaction_id", charge.getInvoice());
|
||||
qw.eq("transaction_id", invoiceId);
|
||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||
if (Objects.nonNull(paymentInfo)){
|
||||
String status ;
|
||||
if (Objects.equals(charge.getAmount(), charge.getAmountRefunded())){
|
||||
status = "Refunded";
|
||||
}else if (charge.getAmount() > charge.getAmountRefunded()){
|
||||
status = "Partial refund";
|
||||
}else {
|
||||
status = "Refund Exception";
|
||||
log.warn("{}, 退款金额高于付款金额, ChargeId为:{}", status, charge.getId());
|
||||
}
|
||||
if (!paymentInfo.getTradeState().equals(status)){
|
||||
if (Objects.nonNull(paymentInfo)) {
|
||||
if (!paymentInfo.getTradeState().equals(status)) {
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(paymentInfo);
|
||||
log.info("[updatePaymentRefundStatusByInvoiceId] 支付记录状态已更新,invoiceId={},status={}", invoiceId, status);
|
||||
}
|
||||
} else {
|
||||
log.warn("[updatePaymentRefundStatusByInvoiceId] 未找到对应的支付记录,invoiceId={}", invoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Charge 中提取 invoiceId
|
||||
* Stripe SDK 32.0.0 (API 2026-03-25.dahlia):
|
||||
* - Charge 没有 invoice 字段(charge.getInvoice() 在新版本中不可用)
|
||||
* - Charge 有 payment_intent 字段(可展开)
|
||||
* 路径: Charge → payment_intent → InvoicePayment.list(payment.payment_intent=xxx) → invoice
|
||||
*
|
||||
* @param charge Stripe Charge
|
||||
* @return invoiceId 或 null
|
||||
*/
|
||||
private String extractInvoiceIdFromCharge(Charge charge) {
|
||||
if (charge == null) {
|
||||
return null;
|
||||
}
|
||||
// 方案1:从 charge.metadata 中获取(如果存储了相关信息)
|
||||
Map<String, String> metadata = charge.getMetadata();
|
||||
if (metadata != null && metadata.containsKey("invoiceId")) {
|
||||
return metadata.get("invoiceId");
|
||||
}
|
||||
|
||||
// 方案2:路径 Charge → payment_intent → InvoicePayment → invoice
|
||||
String paymentIntentId = charge.getPaymentIntent();
|
||||
if (!StringUtil.isNullOrEmpty(paymentIntentId)) {
|
||||
return extractInvoiceIdFromPaymentIntentById(paymentIntentId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 PaymentIntentId 查找关联的 invoiceId
|
||||
* 路径: PaymentIntent → InvoicePayment.list(payment.payment_intent=xxx) → invoice
|
||||
* SDK 32.0.0 InvoicePayment.list() 支持 payment.payment_intent 过滤参数
|
||||
*
|
||||
* @param paymentIntentId Stripe PaymentIntent ID
|
||||
* @return invoiceId 或 null
|
||||
*/
|
||||
private String extractInvoiceIdFromPaymentIntentById(String paymentIntentId) {
|
||||
if (StringUtil.isNullOrEmpty(paymentIntentId)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
InvoicePaymentListParams params = InvoicePaymentListParams.builder()
|
||||
.setPayment(
|
||||
InvoicePaymentListParams.Payment.builder()
|
||||
.setPaymentIntent(paymentIntentId)
|
||||
.setType(InvoicePaymentListParams.Payment.Type.PAYMENT_INTENT)
|
||||
.build()
|
||||
)
|
||||
.setLimit(1L)
|
||||
.build();
|
||||
InvoicePaymentCollection payments = InvoicePayment.list(params);
|
||||
if (payments != null && payments.getData() != null && !payments.getData().isEmpty()) {
|
||||
InvoicePayment payment = payments.getData().get(0);
|
||||
String invoiceId = payment.getInvoice();
|
||||
if (!StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
return invoiceId;
|
||||
}
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
log.warn("[extractInvoiceIdFromPaymentIntentById] 通过 InvoicePayment.list 查找 invoice 失败,paymentIntentId={}, error={}",
|
||||
paymentIntentId, e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice lines 中提取订单号
|
||||
* Stripe SDK 32.0.0: 兼容处理
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String extractOrderNoFromInvoiceLines(Invoice invoice) {
|
||||
try {
|
||||
List<InvoiceLineItem> lines = invoice.getLines().getData();
|
||||
if (lines != null && !lines.isEmpty()) {
|
||||
InvoiceLineItem firstLine = lines.getFirst();
|
||||
// 尝试从 line metadata 获取
|
||||
Map<String, String> lineMetadata = firstLine.getMetadata();
|
||||
if (lineMetadata != null && lineMetadata.containsKey("orderId")) {
|
||||
return lineMetadata.get("orderId");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[extractOrderNoFromInvoiceLines] 提取订单号失败,invoiceId={}, error={}",
|
||||
invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice metadata 中提取订单号
|
||||
* Stripe SDK 32.0.0: 兼容处理
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String extractOrderNoFromInvoiceMetadata(Invoice invoice) {
|
||||
Map<String, String> metadata = invoice.getMetadata();
|
||||
if (metadata != null && metadata.containsKey("orderId")) {
|
||||
return metadata.get("orderId");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice 中获取 subscriptionId
|
||||
* Stripe SDK 32.0.0: 使用 invoice.getParent().getSubscriptionDetails().getSubscription() 替代已移除的 invoice.getSubscription()
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return subscriptionId 或 null
|
||||
*/
|
||||
private String getSubscriptionByInvoice(Invoice invoice) {
|
||||
try {
|
||||
Invoice.Parent parent = invoice.getParent();
|
||||
if (parent != null && "subscription_details".equals(parent.getType())) {
|
||||
Invoice.Parent.SubscriptionDetails subscriptionDetails = parent.getSubscriptionDetails();
|
||||
if (subscriptionDetails != null) {
|
||||
return subscriptionDetails.getSubscription();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[getSubscriptionByInvoice] 从 invoice.getParent() 获取 subscriptionId 失败,invoiceId={}, error={}",
|
||||
invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice.getParent().getSubscriptionDetails().getMetadata() 直接获取 orderNo
|
||||
* Stripe SDK 32.0.0: 这是获取订阅关联的 orderNo 的推荐方式
|
||||
* 当通过 Checkout Session 创建订阅时,metadata 会自动传递到 Subscription,再传递到 Invoice
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String getOrderNoFromInvoiceParent(Invoice invoice) {
|
||||
try {
|
||||
Invoice.Parent parent = invoice.getParent();
|
||||
if (parent != null && "subscription_details".equals(parent.getType())) {
|
||||
Invoice.Parent.SubscriptionDetails subscriptionDetails = parent.getSubscriptionDetails();
|
||||
if (subscriptionDetails != null) {
|
||||
// SDK 32.0.0: subscriptionDetails.getMetadata() 可以直接获取 subscription 创建时设置的 metadata
|
||||
Map<String, String> subscriptionMetadata = subscriptionDetails.getMetadata();
|
||||
if (subscriptionMetadata != null && subscriptionMetadata.containsKey("orderId")) {
|
||||
return subscriptionMetadata.get("orderId");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[getOrderNoFromInvoiceParent] 从 invoice.getParent().getSubscriptionDetails().getMetadata() 获取 orderNo 失败,invoiceId={}, error={}",
|
||||
invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Subscription 中获取 orderNo
|
||||
* 优先从 subscription metadata 获取,其次从 description 中获取
|
||||
*
|
||||
* @param subscription Stripe Subscription
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String getOrderNoBySubscription(Subscription subscription) {
|
||||
if (subscription == null) {
|
||||
return null;
|
||||
}
|
||||
// 方案1:从 subscription metadata 获取(SDK 32.0.0 推荐方式)
|
||||
Map<String, String> metadata = subscription.getMetadata();
|
||||
if (metadata != null && metadata.containsKey("orderId")) {
|
||||
return metadata.get("orderId");
|
||||
}
|
||||
// 方案2:从 description 获取(旧方式,保持兼容)
|
||||
String description = subscription.getDescription();
|
||||
if (!StringUtil.isNullOrEmpty(description) && description.startsWith("AiDA - ")) {
|
||||
return description.replace("AiDA - ", "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ public class RabbitMQServiceImpl implements RabbitMQService {
|
||||
mqPublisher.sendGenerateMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMessageToGenerateResult(String message) {
|
||||
mqPublisher.sendGenerateResultMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMessageToSR(String message) {
|
||||
mqPublisher.sendSRMessage(message);
|
||||
|
||||
@@ -1,31 +1,59 @@
|
||||
package com.ai.da.service.impl;
|
||||
|
||||
import com.ai.da.common.enums.CreditsEventsEnum;
|
||||
import com.ai.da.common.enums.OrderStatusEnum;
|
||||
import com.ai.da.common.enums.ProductEnum;
|
||||
import com.ai.da.common.utils.OrderNoUtils;
|
||||
import com.ai.da.mapper.primary.AccountMapper;
|
||||
import com.ai.da.mapper.primary.RefundInfoMapper;
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.Account;
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.RefundInfo;
|
||||
import com.ai.da.service.OrderInfoService;
|
||||
import com.ai.da.service.RefundInfoService;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.google.gson.Gson;
|
||||
import com.stripe.Stripe;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Refund;
|
||||
import com.stripe.exception.StripeException;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
|
||||
|
||||
@Value("${stripe.private-key}")
|
||||
private String privateKey;
|
||||
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
@Resource
|
||||
private CreditsService creditsService;
|
||||
@Resource
|
||||
private AccountService accountService;
|
||||
@Resource
|
||||
private PaymentInfoService paymentInfoService;
|
||||
@Resource
|
||||
private AccountMapper accountMapper;
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
|
||||
/**
|
||||
* 根据订单号创建退款订单
|
||||
@@ -217,18 +245,275 @@ public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundI
|
||||
}
|
||||
|
||||
public RefundInfo updateRefundForStripe(Charge charge){
|
||||
List<RefundInfo> refundInfoList = getByChargeId(charge.getId());
|
||||
if (!refundInfoList.isEmpty()){
|
||||
RefundInfo refundInfo = refundInfoList.get(refundInfoList.size() - 1);
|
||||
if (StringUtil.isNullOrEmpty(refundInfo.getOrderNo())){
|
||||
String orderNo = charge.getDescription().replace("AiDA - ", "");
|
||||
refundInfo.setOrderNo(orderNo);
|
||||
refundInfo.setTotalFee(charge.getAmount() / 100f);
|
||||
refundInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(refundInfo);
|
||||
return refundInfo;
|
||||
String chargeId = charge.getId();
|
||||
List<RefundInfo> refundInfoList = getByChargeId(chargeId);
|
||||
if (refundInfoList.isEmpty()){
|
||||
return null;
|
||||
}
|
||||
RefundInfo refundInfo = refundInfoList.get(refundInfoList.size() - 1);
|
||||
if (StringUtil.isNullOrEmpty(refundInfo.getOrderNo())){
|
||||
String orderNo = charge.getDescription() != null ? charge.getDescription().replace("AiDA - ", "") : null;
|
||||
if (StringUtil.isNullOrEmpty(orderNo)){
|
||||
return null;
|
||||
}
|
||||
refundInfo.setOrderNo(orderNo);
|
||||
refundInfo.setTotalFee(charge.getAmount() / 100f);
|
||||
baseMapper.updateById(refundInfo);
|
||||
}
|
||||
|
||||
// 处理退款成功后的业务逻辑
|
||||
// 判断是否为全额退款(amount == amountRefunded)
|
||||
if (charge.getAmount() != null && charge.getAmountRefunded() != null
|
||||
&& charge.getAmount().equals(charge.getAmountRefunded())) {
|
||||
handleRefundSuccess(refundInfo);
|
||||
}
|
||||
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理全额退款成功后的业务逻辑
|
||||
* 根据订单类型执行不同操作:
|
||||
* - 积分购买订单:扣减 t_account.credits,并在 t_credits_detail 添加变动记录
|
||||
* - 订阅订单:扣减 t_account.credits,根据订阅类型在 t_credits_detail 添加变动记录,
|
||||
* 并将 t_account.valid_start_time 设置为 t_subscription_info.current_period_start
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void handleRefundSuccess(RefundInfo refundInfo) {
|
||||
String orderNo = refundInfo.getOrderNo();
|
||||
if (StringUtil.isNullOrEmpty(orderNo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (orderInfo == null) {
|
||||
log.warn("[handleFullRefundSuccess] 未找到订单,跳过,orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
String title = orderInfo.getTitle();
|
||||
Long accountId = orderInfo.getAccountId();
|
||||
Account account = accountMapper.selectById(accountId);
|
||||
|
||||
// 判断订单类型
|
||||
if (title != null && title.startsWith("积分购买")) {
|
||||
// 积分购买订单退款
|
||||
handleCreditsPurchaseRefund(orderNo, orderInfo, account);
|
||||
} else {
|
||||
// 订阅订单退款
|
||||
handleSubscriptionRefund(orderNo, orderInfo, account);
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS);
|
||||
log.info("[RefundInfoService] 退款成功,订单状态已更新,orderNo={}", orderNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理积分购买订单的退款
|
||||
* 扣减 t_account.credits,在 t_credits_detail 添加变动记录
|
||||
*/
|
||||
private void handleCreditsPurchaseRefund(String orderNo, OrderInfo orderInfo, Account account) {
|
||||
Long accountId = orderInfo.getAccountId();
|
||||
// 根据购买金额 / 单价计算积分数量
|
||||
float creditsToRefund = orderInfo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue());
|
||||
int creditsQty = (int) creditsToRefund;
|
||||
|
||||
BigDecimal existingCredits = account.getCredits();
|
||||
BigDecimal refundCredits = new BigDecimal(CreditsEventsEnum.BUY_CREDITS.getValue())
|
||||
.multiply(new BigDecimal(creditsQty));
|
||||
BigDecimal newCredits = existingCredits.subtract(refundCredits);
|
||||
if (newCredits.compareTo(BigDecimal.ZERO) < 0) {
|
||||
newCredits = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 更新 t_account.credits
|
||||
accountService.updateCreditsAndEndTime(account, newCredits.toString(), null, null);
|
||||
|
||||
// 更新 t_credits_detail
|
||||
creditsService.insertToCreditsDetail(
|
||||
accountId,
|
||||
CreditsEventsEnum.REFUND.getName() + "--Stripe",
|
||||
refundCredits.toString(),
|
||||
"negative",
|
||||
orderNo
|
||||
);
|
||||
|
||||
log.info("[handleCreditsPurchaseRefund] 积分购买退款完成,orderNo={},accountId={},creditsRefunded={}",
|
||||
orderNo, accountId, refundCredits);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理订阅订单的退款
|
||||
* 扣减 t_account.credits,根据订阅类型在 t_credits_detail 添加变动记录,
|
||||
* 并将 t_account.valid_start_time 设置为 t_subscription_info.current_period_start
|
||||
*/
|
||||
private void handleSubscriptionRefund(String orderNo, OrderInfo orderInfo, Account account) {
|
||||
Long accountId = orderInfo.getAccountId();
|
||||
String title = orderInfo.getTitle();
|
||||
|
||||
// 根据 orderInfo.title 在 ProductEnum 中匹配订阅类型
|
||||
ProductEnum productEnum = ProductEnum.getByName(title);
|
||||
if (productEnum == null) {
|
||||
log.warn("[handleSubscriptionRefund] 无法匹配订阅类型,跳过积分扣减,orderNo={},title={}", orderNo, title);
|
||||
return;
|
||||
}
|
||||
|
||||
// 扣减对应订阅类型的积分
|
||||
BigDecimal existingCredits = account.getCredits();
|
||||
BigDecimal refundCredits = new BigDecimal(productEnum.getCredits());
|
||||
BigDecimal newCredits = existingCredits.subtract(refundCredits);
|
||||
if (newCredits.compareTo(BigDecimal.ZERO) < 0) {
|
||||
newCredits = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 根据 orderNo 查询 t_subscription_info,将 t_account.valid_start_time 设置为 current_period_start
|
||||
List<SubscriptionInfo> subList = subscriptionInfoMapper.selectList(
|
||||
new QueryWrapper<SubscriptionInfo>().eq("order_no", orderNo)
|
||||
);
|
||||
if (!subList.isEmpty()) {
|
||||
SubscriptionInfo subscriptionInfo = subList.getFirst();
|
||||
|
||||
if (subscriptionInfo.getStatus().equals("active")) {
|
||||
stripeSubscriptionService.cancelSubscription(subscriptionInfo.getSubscriptionId(), "Refunded", accountId);
|
||||
}
|
||||
|
||||
Long periodStart = subscriptionInfo.getCurrentPeriodStart();
|
||||
if (periodStart != null) {
|
||||
account.setSystemUser(0);
|
||||
account.setValidEndTime(periodStart * 1000);
|
||||
account.setCredits(newCredits);
|
||||
account.setUpdateDate(new java.util.Date());
|
||||
accountMapper.updateById(account);
|
||||
log.info("[handleSubscriptionRefund] 已将 valid_start_time 设置为 current_period_start,orderNo={},periodStart={}",
|
||||
orderNo, periodStart);
|
||||
}
|
||||
} else {
|
||||
log.warn("[handleSubscriptionRefund] 未找到订阅记录,跳过 valid_start_time 更新,orderNo={}", orderNo);
|
||||
}
|
||||
|
||||
// 在 t_credits_detail 添加变动记录,systemUser 设为 0
|
||||
creditsService.insertToCreditsDetail(
|
||||
accountId,
|
||||
CreditsEventsEnum.REFUND.getName() + "--Stripe",
|
||||
refundCredits.toString(),
|
||||
"negative",
|
||||
orderNo
|
||||
);
|
||||
|
||||
log.info("[handleSubscriptionRefund] 订阅退款完成,orderNo={},accountId={},creditsRefunded={}",
|
||||
orderNo, accountId, refundCredits);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* refund.created 事件处理
|
||||
* 在 t_refund_info 表中创建退款记录
|
||||
*/
|
||||
@Override
|
||||
public RefundInfo handleRefundCreated(Refund refund) {
|
||||
String refundId = refund.getId();
|
||||
RefundInfo existing = getByRefundId(refundId);
|
||||
if (existing != null) {
|
||||
log.info("[handleRefundCreated] 退款记录已存在,跳过创建,refundId={}", refundId);
|
||||
return existing;
|
||||
}
|
||||
|
||||
RefundInfo refundInfo = new RefundInfo();
|
||||
refundInfo.setRefundId(refundId);
|
||||
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());
|
||||
refundInfo.setChargeId(refund.getCharge());
|
||||
refundInfo.setRefund(refund.getAmount() / 100f);
|
||||
refundInfo.setReason(refund.getReason());
|
||||
refundInfo.setRefundStatus("pending");
|
||||
refundInfo.setCreateTime(LocalDateTime.now());
|
||||
|
||||
baseMapper.insert(refundInfo);
|
||||
log.info("[handleRefundCreated] 退款记录已创建,refundId={},chargeId={}", refundId, refund.getCharge());
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* refund.updated (status=succeeded) 事件处理
|
||||
* 找到该笔退款对应的 invoice,从而修改 paymentInfo 表中 transactionId 为 invoiceId 的记录,将状态改为 refunded
|
||||
*/
|
||||
@Override
|
||||
public RefundInfo handleRefundSucceeded(Refund refund) {
|
||||
String refundId = refund.getId();
|
||||
RefundInfo refundInfo = getByRefundId(refundId);
|
||||
if (refundInfo == null) {
|
||||
log.warn("[handleRefundSucceeded] 未找到退款记录,先创建,refundId={}", refundId);
|
||||
refundInfo = handleRefundCreated(refund);
|
||||
}
|
||||
|
||||
// 获取 charge 并从中提取 orderNo
|
||||
String chargeId = refund.getCharge();
|
||||
Charge charge = null;
|
||||
if (!StringUtil.isNullOrEmpty(chargeId)) {
|
||||
Stripe.apiKey = privateKey;
|
||||
try {
|
||||
charge = Charge.retrieve(chargeId);
|
||||
String description = charge.getDescription();
|
||||
String orderNo = description != null ? description.replace("AiDA - ", "") : null;
|
||||
if (!StringUtil.isNullOrEmpty(orderNo) && !orderNo.equals(refundInfo.getOrderNo())) {
|
||||
if (!"succeeded".equals(refundInfo.getRefundStatus())) {
|
||||
refundInfo.setRefundStatus("succeeded");
|
||||
}
|
||||
refundInfo.setOrderNo(orderNo);
|
||||
refundInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(refundInfo);
|
||||
log.info("[handleRefundSucceeded] 从 charge 中提取并更新 orderNo,refundId={},orderNo={}", refundId, orderNo);
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
log.error("[handleRefundSucceeded] 获取 charge 失败,chargeId={},error={}", chargeId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
// 通过 charge 更新 paymentInfo 状态为 refunded
|
||||
if (charge != null) {
|
||||
paymentInfoService.updatePaymentRefundStatusByChargeId(charge, "Refunded");
|
||||
}
|
||||
|
||||
// 如果是全额退款,执行后续业务逻辑
|
||||
if (!StringUtil.isNullOrEmpty(refundInfo.getOrderNo())) {
|
||||
handleRefundSuccess(refundInfo);
|
||||
}
|
||||
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* refund.failed 事件处理
|
||||
* 修改 t_refund_info 表状态,同时邮件通知商家
|
||||
*/
|
||||
@Override
|
||||
public RefundInfo handleRefundFailed(Refund refund) {
|
||||
String refundId = refund.getId();
|
||||
RefundInfo refundInfo = getByRefundId(refundId);
|
||||
if (refundInfo == null) {
|
||||
log.warn("[handleRefundFailed] 未找到退款记录,先创建,refundId={}", refundId);
|
||||
refundInfo = handleRefundCreated(refund);
|
||||
}
|
||||
|
||||
if (!"failed".equals(refundInfo.getRefundStatus())) {
|
||||
refundInfo.setRefundStatus("failed");
|
||||
refundInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(refundInfo);
|
||||
log.info("[handleRefundFailed] 退款状态已更新为 failed,refundId={}", refundId);
|
||||
}
|
||||
|
||||
// 发送退款失败邮件通知商家
|
||||
try {
|
||||
String reason = refund.getFailureReason();
|
||||
String orderNo = refundInfo.getOrderNo() != null ? refundInfo.getOrderNo() : "";
|
||||
String amount = String.valueOf(refundInfo.getRefund());
|
||||
// SendEmailUtil.sendRefundFailedNotification(refundId, reason, orderNo, amount);
|
||||
log.info("[handleRefundFailed] 已发送退款失败通知邮件,refundId={}", refundId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleRefundFailed] 发送退款失败通知邮件异常,refundId={},error={}", refundId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user