Compare commits
238 Commits
4fa70a1c90
...
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 | |||
| 6cd42b799a | |||
| 6e1ed7f9b8 | |||
| b7be16738b | |||
| 6da5e91ec1 | |||
| a710fdd432 | |||
| d598f53d3c | |||
| 96170a9956 | |||
| 8205fb5290 | |||
| fcbe4762b3 | |||
| e750adcc94 |
@@ -99,6 +99,8 @@ jobs:
|
|||||||
volumes:
|
volumes:
|
||||||
# 数据挂载
|
# 数据挂载
|
||||||
- ./log:/log
|
- ./log:/log
|
||||||
|
- ./temp:/temp
|
||||||
|
- ./uploads:/temp/uploads
|
||||||
ports:
|
ports:
|
||||||
- '10090:5567'
|
- '10090:5567'
|
||||||
restart: always
|
restart: always
|
||||||
@@ -133,8 +135,6 @@ jobs:
|
|||||||
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
||||||
echo "停止旧容器..."
|
echo "停止旧容器..."
|
||||||
docker compose down || true
|
docker compose down || true
|
||||||
echo "清理Docker资源..."
|
|
||||||
docker system prune -f
|
|
||||||
echo "构建镜像..."
|
echo "构建镜像..."
|
||||||
docker compose build --no-cache
|
docker compose build --no-cache
|
||||||
echo "启动服务..."
|
echo "启动服务..."
|
||||||
|
|||||||
7
pom.xml
7
pom.xml
@@ -240,7 +240,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.stripe</groupId>
|
<groupId>com.stripe</groupId>
|
||||||
<artifactId>stripe-java</artifactId>
|
<artifactId>stripe-java</artifactId>
|
||||||
<version>26.2.0</version>
|
<version>32.0.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- aws s3 -->
|
<!-- aws s3 -->
|
||||||
@@ -427,6 +427,11 @@
|
|||||||
<artifactId>bcpkix-jdk18on</artifactId>
|
<artifactId>bcpkix-jdk18on</artifactId>
|
||||||
<version>1.78.1</version>
|
<version>1.78.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- AOP -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ public class MQPublisher {
|
|||||||
amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getSr(), mm);
|
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 含有的字段
|
* @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() {
|
public void sendTrialOrderExcelToManagements() {
|
||||||
// 获取前一天日期
|
// 获取前一天日期
|
||||||
LocalDate yesterday = LocalDate.now().minusDays(1);
|
LocalDate yesterday = LocalDate.now().minusDays(1);
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ public class CommonConstant {
|
|||||||
public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60;
|
public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60;
|
||||||
// 单位 秒 一天过期 in redis
|
// 单位 秒 一天过期 in redis
|
||||||
public static final Long GENERATE_RESULT_EXPIRE_TIME = 24 * 60 * 60L;
|
public static final Long GENERATE_RESULT_EXPIRE_TIME = 24 * 60 * 60L;
|
||||||
// 单位 秒 7天过期
|
// 单位 秒 7天过期 (todo 测试状态下 3小时过期)
|
||||||
public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L;
|
// 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 class Numbers{
|
||||||
public static final Integer NUMBER_10 = 10;
|
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 = "/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";
|
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 PRINTBOARD_ADVANCED_T2I = "qwen-image";
|
||||||
public static final String MOODBOARD_ADVANCED = "doubao-seedream-3-0-t2i-250415";
|
public static final String MOODBOARD_ADVANCED = "doubao-seedream-4-5-251128";
|
||||||
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-3-0-t2i-250415";
|
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-4-0-250828-high";
|
||||||
public static final String PRINTBOARD_HIGH_I2I = "doubao-seededit-3-0-i2i-250628";
|
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 PRINTBOARD_ADVANCED_I2I = "doubao-seedream-4-0-250828";
|
||||||
public static final String IMAGEN_MODEL = "imagen-4.0-generate-001";
|
public static final String IMAGEN_MODEL = "imagen-4.0-generate-001";
|
||||||
public static final String NANO_BANANA = "gemini-2.5-flash-image";
|
public static final String NANO_BANANA = "gemini-2.5-flash-image";
|
||||||
|
|||||||
@@ -33,7 +33,11 @@ public enum AuthenticationOperationTypeEnum {
|
|||||||
*/
|
*/
|
||||||
UPDATE_USERINFO,
|
UPDATE_USERINFO,
|
||||||
|
|
||||||
REGISTER;
|
REGISTER,
|
||||||
|
/**
|
||||||
|
* Global_Award 活动验证
|
||||||
|
*/
|
||||||
|
GLOBAL_AWARD;
|
||||||
|
|
||||||
public static AuthenticationOperationTypeEnum of(String name) {
|
public static AuthenticationOperationTypeEnum of(String name) {
|
||||||
return Stream.of(AuthenticationOperationTypeEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null);
|
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_QUARTERLY("init_quarterly", "12000"),
|
||||||
INIT_MONTHLY_EDU("init_monthly_edu", "3500"),
|
INIT_MONTHLY_EDU("init_monthly_edu", "3500"),
|
||||||
INIT_TRIAL("init_trial", "100"),
|
INIT_TRIAL("init_trial", "100"),
|
||||||
INIT_WEEKLY("init_weekly","6000"),
|
INIT_DAILY("init_daily","100"),
|
||||||
RESET_YEAR_CREDITS("reset_year_credits","6000"),
|
RESET_YEAR_CREDITS("reset_year_credits","6000"),
|
||||||
|
|
||||||
// SUPER_RESOLUTION("Super Resolution","30"),
|
// SUPER_RESOLUTION("Super Resolution","30"),
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ public enum OrderStatusEnum {
|
|||||||
* 已退款
|
* 已退款
|
||||||
*/
|
*/
|
||||||
REFUND_SUCCESS("已退款"),
|
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.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public enum ProductEnum {
|
public enum ProductEnum {
|
||||||
@@ -23,11 +25,27 @@ public enum ProductEnum {
|
|||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 类型
|
* 显示名称(用于与 orderInfo.title 匹配)
|
||||||
*/
|
*/
|
||||||
private final String name;
|
private final String name;
|
||||||
|
|
||||||
private final Long price;
|
private final Long price;
|
||||||
|
|
||||||
private final Long credits;
|
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.config.SecurityProperties;
|
||||||
import com.ai.da.common.security.jwt.JWTTokenHelper;
|
import com.ai.da.common.security.jwt.JWTTokenHelper;
|
||||||
import com.ai.da.common.utils.LocalCacheUtils;
|
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.MultiReadHttpServletRequest;
|
||||||
import com.ai.da.common.utils.MultiReadHttpServletResponse;
|
import com.ai.da.common.utils.MultiReadHttpServletResponse;
|
||||||
import com.ai.da.common.utils.RequestInfoUtil;
|
import com.ai.da.common.utils.RequestInfoUtil;
|
||||||
@@ -40,6 +41,8 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
|||||||
private JWTTokenHelper jwtTokenHelper;
|
private JWTTokenHelper jwtTokenHelper;
|
||||||
@Resource
|
@Resource
|
||||||
private SecurityProperties properties;
|
private SecurityProperties properties;
|
||||||
|
@Resource
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
private static final List<String> FILTER_URL =
|
private static final List<String> FILTER_URL =
|
||||||
Arrays.asList("/favicon.ico", "/doc.html", "/swagger-ui.html",
|
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",
|
"/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"
|
"/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/account/schoolLogin", "/api/account/enterpriseLogin", "/api/account/organizationNameSearch",
|
||||||
"/api/llm/stream"
|
"/api/llm/stream",
|
||||||
|
//GlobalAwardController
|
||||||
|
"/api/global-award"
|
||||||
);
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -132,12 +137,19 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
|||||||
UserContext.delete();
|
UserContext.delete();
|
||||||
//存取用户信息到缓存
|
//存取用户信息到缓存
|
||||||
UserContext.setUserHolder(principal);
|
UserContext.setUserHolder(principal);
|
||||||
//校验token
|
// 校验 token:先查本地缓存,再查 Redis,保证服务重启后仍然有效
|
||||||
String cacheToken = LocalCacheUtils.getTokenCache(String.valueOf(principal.getId()));
|
String userIdStr = String.valueOf(principal.getId());
|
||||||
|
String cacheToken = LocalCacheUtils.getTokenCache(userIdStr);
|
||||||
|
|
||||||
if(StringUtils.isEmpty(cacheToken)){
|
if (StringUtils.isEmpty(cacheToken)) {
|
||||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
// 本地缓存为空时,尝试从 Redis 读取
|
||||||
throw new TokenMissingOrExpiredException("TOKEN已过期,请重新登录!(local cache empty)");
|
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) ){
|
if(!cacheToken.equals(jwtToken) ){
|
||||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
||||||
|
|||||||
@@ -34,14 +34,14 @@ public class AccountTask {
|
|||||||
accountService.refreshCreditsMonthly();
|
accountService.refreshCreditsMonthly();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||||
public void getPaidUser() {
|
public void getPaidUser() {
|
||||||
// 获取code-create 表中 指定日期之后 订单状态为wc-processing的订单
|
// 获取code-create 表中 指定日期之后 订单状态为wc-processing的订单
|
||||||
accountService.extendValidityForCC();
|
accountService.extendValidityForCC();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 每天凌晨0点执行一次
|
// 每天凌晨0点执行一次 目前已没有角色类型为4的用户
|
||||||
@Scheduled(cron = "0 0 0 * * ?")
|
/*@Scheduled(cron = "0 0 0 * * ?")
|
||||||
public void cancelActivityBenefits() {
|
public void cancelActivityBenefits() {
|
||||||
// 1、查询当前所有参与了活动且过期的用户
|
// 1、查询当前所有参与了活动且过期的用户
|
||||||
List<Account> accountList = accountService.getExpiredUserBySystemUser(4);
|
List<Account> accountList = accountService.getExpiredUserBySystemUser(4);
|
||||||
@@ -51,7 +51,7 @@ public class AccountTask {
|
|||||||
log.info("参与活动的用户{} : {} 于 {} 账号有效期到期,置为游客", account.getId(), account.getUserEmail(), account.getValidEndTime());
|
log.info("参与活动的用户{} : {} 于 {} 账号有效期到期,置为游客", account.getId(), account.getUserEmail(), account.getValidEndTime());
|
||||||
accountService.toVisitor(account);
|
accountService.toVisitor(account);
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// 每天检测正式用户到期情况,每天凌晨0点执行
|
// 每天检测正式用户到期情况,每天凌晨0点执行
|
||||||
@Scheduled(cron = "0 0 0 * * ?")
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
@@ -92,7 +92,7 @@ public class AccountTask {
|
|||||||
|
|
||||||
@Scheduled(cron = "0 5 0 * * ?")
|
@Scheduled(cron = "0 5 0 * * ?")
|
||||||
public void activeSubscriptionPlan() {
|
public void activeSubscriptionPlan() {
|
||||||
subscriptionPlanService.activeSubscriptionPlan();
|
subscriptionPlanService.activeSubscriptionPlan(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ public class PaymentTask {
|
|||||||
@Resource
|
@Resource
|
||||||
private PayPalCheckoutService payPalCheckoutService;
|
private PayPalCheckoutService payPalCheckoutService;
|
||||||
|
|
||||||
// @Scheduled(cron = "0/30 * * * * ?")
|
// @Scheduled(cron = "0/30 * * * * ?")
|
||||||
public void orderConfirmForPaypal() throws SerializeException {
|
public void orderConfirmForPaypal() throws SerializeException {
|
||||||
|
|
||||||
// log.info("PayPal orderConfirm 被执行......");
|
// 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(){
|
public void updateAffiliateInfoWithPayment(){
|
||||||
// log.info("佣金计算定时器");
|
// log.info("佣金计算定时器");
|
||||||
affiliateService.updateAffiliateInfoWithPayment();
|
affiliateService.updateAffiliateInfoWithPayment();
|
||||||
@@ -109,7 +109,7 @@ public class PaymentTask {
|
|||||||
affiliateService.syncLinkViewCountToDB();
|
affiliateService.syncLinkViewCountToDB();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
||||||
public void commissionSummaryReminder(){
|
public void commissionSummaryReminder(){
|
||||||
// 每个月末的最后一天的早上八点执行
|
// 每个月末的最后一天的早上八点执行
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public class SubscriptionReminderTask {
|
|||||||
REMINDER_DAYS_CONFIG.put("year", 14);
|
REMINDER_DAYS_CONFIG.put("year", 14);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 0 9 * * ?")
|
// @Scheduled(cron = "0 0 9 * * ?")
|
||||||
public void subscriptionReminder() {
|
public void subscriptionReminder() {
|
||||||
// 获取所有需要通知的订阅
|
// 获取所有需要通知的订阅
|
||||||
List<SubscriptionInfo> subscriptionInfos = getDueSubscriptions();
|
List<SubscriptionInfo> subscriptionInfos = getDueSubscriptions();
|
||||||
@@ -97,7 +97,7 @@ public class SubscriptionReminderTask {
|
|||||||
return subscriptionInfoMapper.selectList(qw);
|
return subscriptionInfoMapper.selectList(qw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 0 9 * * ?")
|
// @Scheduled(cron = "0 0 9 * * ?")
|
||||||
public void trialReminder() {
|
public void trialReminder() {
|
||||||
// 今天的 00:00:00 和 23:59:59
|
// 今天的 00:00:00 和 23:59:59
|
||||||
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
|
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||||
|
|||||||
@@ -41,6 +41,13 @@ public class MinioUtil {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private MinioClient minioClient;
|
private MinioClient minioClient;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取MinIO客户端实例
|
||||||
|
*/
|
||||||
|
public MinioClient getMinioClient() {
|
||||||
|
return minioClient;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* description: 判断bucket是否存在,不存在则创建
|
* 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.SendEmailResponse;
|
||||||
import com.tencentcloudapi.ses.v20201002.models.Template;
|
import com.tencentcloudapi.ses.v20201002.models.Template;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 邮件发送类
|
* 邮件发送类
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
|
@Component
|
||||||
public class SendEmailUtil {
|
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
|
* 秘钥id
|
||||||
*/
|
*/
|
||||||
@@ -765,9 +786,7 @@ public class SendEmailUtil {
|
|||||||
|
|
||||||
public static boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
public static boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||||
try {
|
try {
|
||||||
String merchantEmail = "kimwong@code-create.com.hk";
|
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||||
String developer = "xupei3360@163.com";
|
|
||||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
|
||||||
Credential cred = new Credential(SECRET_ID, SECRET_KEy);
|
Credential cred = new Credential(SECRET_ID, SECRET_KEy);
|
||||||
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
||||||
HttpProfile httpProfile = new HttpProfile();
|
HttpProfile httpProfile = new HttpProfile();
|
||||||
@@ -966,9 +985,7 @@ public class SendEmailUtil {
|
|||||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||||
SendEmailRequest req = new SendEmailRequest();
|
SendEmailRequest req = new SendEmailRequest();
|
||||||
req.setFromEmailAddress(SEND_ADDRESS);
|
req.setFromEmailAddress(SEND_ADDRESS);
|
||||||
String merchantEmail = "kimwong@code-create.com.hk";
|
req.setDestination(buildMerchantReceiverEmail());
|
||||||
String developerEmail = "xupei@code-create.com.hk";
|
|
||||||
req.setDestination(new String[]{/*merchantEmail,*/ developerEmail});
|
|
||||||
Template template = new Template();
|
Template template = new Template();
|
||||||
req.setSubject("New Credit Purchase Order");
|
req.setSubject("New Credit Purchase Order");
|
||||||
template.setTemplateID(CREDITS_PURCHASE_MERCHANT);
|
template.setTemplateID(CREDITS_PURCHASE_MERCHANT);
|
||||||
@@ -1024,7 +1041,7 @@ public class SendEmailUtil {
|
|||||||
log.info("邮件发送结果res###{}", SendEmailResponse.toJsonString(resp));
|
log.info("邮件发送结果res###{}", SendEmailResponse.toJsonString(resp));
|
||||||
} catch (TencentCloudSDKException e) {
|
} catch (TencentCloudSDKException e) {
|
||||||
log.info("邮件发送失败###{}", e.toString());
|
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) {
|
public String sendFluxPost(String url, String requestBodyStr) {
|
||||||
// 尝试两个API key
|
// 尝试两个API key
|
||||||
String[] apiKeys = {"d447a0ac-2291-4f1c-9a36-f7614c385989",
|
String[] apiKeys = {"84e8f5d5-b0b3-49aa-b244-ab7ba27e7ae7",
|
||||||
"84e8f5d5-b0b3-49aa-b244-ab7ba27e7ae7"};
|
"d447a0ac-2291-4f1c-9a36-f7614c385989"};
|
||||||
boolean[] notified = {false, false}; // 记录是否已发送过不足提醒
|
boolean[] notified = {false, false}; // 记录是否已发送过不足提醒
|
||||||
|
|
||||||
for (int i = 0; i < apiKeys.length; i++) {
|
for (int i = 0; i < apiKeys.length; i++) {
|
||||||
@@ -140,8 +140,8 @@ public class SendRequestUtil {
|
|||||||
if (status == 402 || status == 403) {
|
if (status == 402 || status == 403) {
|
||||||
if (!notified[i]) {
|
if (!notified[i]) {
|
||||||
SendEmailUtil.commonExceptionReminder(
|
SendEmailUtil.commonExceptionReminder(
|
||||||
"Flux账户积分不足,flux生成任务失败",
|
"Flux账户积分不足,flux生成任务失败。(key:)" + apiKeys[i],
|
||||||
new String[]{"xupei3360@163.com, fangjianliao@aidlab.hk, investigation@aidlab.hk"}
|
new String[]{"xupei3360@163.com", "fangjianliao@aidlab.hk", "investigation@aidlab.hk"}
|
||||||
);
|
);
|
||||||
notified[i] = true;
|
notified[i] = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,16 @@ import com.ai.da.model.vo.PersonalHomepageVO;
|
|||||||
import com.ai.da.service.AccountService;
|
import com.ai.da.service.AccountService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
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.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -139,21 +139,21 @@ public class AccountController {
|
|||||||
@Operation(summary = "aws状态检测")
|
@Operation(summary = "aws状态检测")
|
||||||
@GetMapping("/healthy")
|
@GetMapping("/healthy")
|
||||||
@ResponseStatus(HttpStatus.OK)
|
@ResponseStatus(HttpStatus.OK)
|
||||||
public Response<Map<String,Integer>> checkStatus(){
|
public Response<Map<String, Integer>> checkStatus() {
|
||||||
Map<String,Integer> returnMap = new HashMap<>();
|
Map<String, Integer> returnMap = new HashMap<>();
|
||||||
returnMap.put("code",200);
|
returnMap.put("code", 200);
|
||||||
return Response.success(returnMap);
|
return Response.success(returnMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "查询账号到期时间")
|
@Operation(summary = "查询账号到期时间")
|
||||||
@PostMapping("/getExpiredTime")
|
@PostMapping("/getExpiredTime")
|
||||||
public Response<Long> getExpiredTime(){
|
public Response<Long> getExpiredTime() {
|
||||||
return Response.success(accountService.getExpiredTime());
|
return Response.success(accountService.getExpiredTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "免密登录")
|
@Operation(summary = "免密登录")
|
||||||
@PostMapping("/noLoginRequired")
|
@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));
|
return Response.success(accountService.noLoginRequired(noLoginRequiredDTO, request));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +191,7 @@ public class AccountController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 参与活动 获取福利
|
* 参与活动 获取福利
|
||||||
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
/* @Operation(summary = "参与活动 获取福利")
|
/* @Operation(summary = "参与活动 获取福利")
|
||||||
@@ -201,7 +202,7 @@ public class AccountController {
|
|||||||
|
|
||||||
@Operation(summary = "将用户账号过期时间设置为过期当天的23:59:59")
|
@Operation(summary = "将用户账号过期时间设置为过期当天的23:59:59")
|
||||||
@GetMapping("/setUserValidToDayEnd")
|
@GetMapping("/setUserValidToDayEnd")
|
||||||
public Response<List<Long>> setUserValidToDayEnd(){
|
public Response<List<Long>> setUserValidToDayEnd() {
|
||||||
return Response.success(accountService.setUserValidToDayEnd());
|
return Response.success(accountService.setUserValidToDayEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,19 +224,19 @@ public class AccountController {
|
|||||||
|
|
||||||
@Operation(summary = "获取个人主页信息")
|
@Operation(summary = "获取个人主页信息")
|
||||||
@GetMapping("/personalHomepage")
|
@GetMapping("/personalHomepage")
|
||||||
public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id){
|
public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id) {
|
||||||
return Response.success(accountService.getPersonalHomepage(id));
|
return Response.success(accountService.getPersonalHomepage(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "getUsernameModifyTimes")
|
@Operation(summary = "getUsernameModifyTimes")
|
||||||
@GetMapping("/getNicknameModifyTimes")
|
@GetMapping("/getNicknameModifyTimes")
|
||||||
public Response<Long> getNicknameModifyTimes(){
|
public Response<Long> getNicknameModifyTimes() {
|
||||||
return Response.success(accountService.getNicknameModifyTimes());
|
return Response.success(accountService.getNicknameModifyTimes());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "editUserName")
|
@Operation(summary = "editUserName")
|
||||||
@GetMapping("/editUserName")
|
@GetMapping("/editUserName")
|
||||||
public Response<String> editUserName(@RequestParam("newUserName") String newUserName){
|
public Response<String> editUserName(@RequestParam("newUserName") String newUserName) {
|
||||||
accountService.editUserName(newUserName);
|
accountService.editUserName(newUserName);
|
||||||
return Response.success("success");
|
return Response.success("success");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ public class ConvenientInquiryController {
|
|||||||
@GetMapping("/recentNewUserChart")
|
@GetMapping("/recentNewUserChart")
|
||||||
public Response<Map<String, Object>> recentNewUserChart(@Parameter(description = "startTime") @RequestParam @Nullable String startTime,
|
public Response<Map<String, Object>> recentNewUserChart(@Parameter(description = "startTime") @RequestParam @Nullable String startTime,
|
||||||
@Parameter(description = "endTime") @RequestParam @Nullable String endTime,
|
@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));
|
return Response.success(convenientInquiryService.recentNewUserChart(startTime, endTime, userType));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,8 +179,10 @@ public class ConvenientInquiryController {
|
|||||||
|
|
||||||
@Operation(summary = "获取所有用户id")
|
@Operation(summary = "获取所有用户id")
|
||||||
@GetMapping("/getAllUserId")
|
@GetMapping("/getAllUserId")
|
||||||
public Response<List<Map<String, Object>>> getAllUsrIdList() {
|
public Response<IPage<Map<String, Object>>> getAllUserIdList(@Parameter(description = "page") @RequestParam Integer page,
|
||||||
return Response.success(convenientInquiryService.getAllUserIdList());
|
@Parameter(description = "size") @RequestParam Integer size,
|
||||||
|
@Parameter(description = "email 模糊查询") @RequestParam(required = false) String email) {
|
||||||
|
return Response.success(convenientInquiryService.getAllUserIdList(page, size, email));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "获取所有交易信息")
|
@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.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@@ -119,4 +122,14 @@ public class PythonController {
|
|||||||
return Response.success(superResolutionService.prepareForSR(superResolutionDTO));
|
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;
|
package com.ai.da.controller;
|
||||||
|
|
||||||
|
import com.ai.da.common.context.UserContext;
|
||||||
import com.ai.da.common.response.Response;
|
import com.ai.da.common.response.Response;
|
||||||
import com.ai.da.common.utils.DateUtil;
|
import com.ai.da.common.utils.DateUtil;
|
||||||
import com.ai.da.common.utils.RedisUtil;
|
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.dto.QueryCouponsPageDTO;
|
||||||
import com.ai.da.model.vo.CheckCouponsVO;
|
import com.ai.da.model.vo.CheckCouponsVO;
|
||||||
import com.ai.da.service.StripeService;
|
import com.ai.da.service.StripeService;
|
||||||
|
import com.ai.da.service.StripeSubscriptionService;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.paypal.http.HttpResponse;
|
import com.paypal.http.HttpResponse;
|
||||||
import com.paypal.payments.Refund;
|
import com.paypal.payments.Refund;
|
||||||
@@ -40,6 +42,8 @@ public class StripeController {
|
|||||||
private StripeService stripeService;
|
private StripeService stripeService;
|
||||||
@Resource
|
@Resource
|
||||||
private RedisUtil redisUtil;
|
private RedisUtil redisUtil;
|
||||||
|
@Resource
|
||||||
|
private StripeSubscriptionService stripeSubscriptionService;
|
||||||
|
|
||||||
@Operation(summary = "创建支付链接")
|
@Operation(summary = "创建支付链接")
|
||||||
@PostMapping("/createOrder")
|
@PostMapping("/createOrder")
|
||||||
@@ -53,30 +57,29 @@ public class StripeController {
|
|||||||
@Operation(summary = "支付通知")
|
@Operation(summary = "支付通知")
|
||||||
@PostMapping("/trade/notify")
|
@PostMapping("/trade/notify")
|
||||||
public void callback(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
public void callback(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||||
try{
|
boolean result;
|
||||||
Boolean result = stripeService.notify(request);
|
try {
|
||||||
if (result){
|
result = stripeService.notify(request);
|
||||||
response.setStatus(HttpServletResponse.SC_OK);
|
} catch (Exception e) {
|
||||||
}else {
|
log.error("Stripe Controller层异常捕捉, {}", e.getMessage(), e);
|
||||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
}catch (Exception e){
|
|
||||||
log.error("Stripe Controller层异常捕捉, {}", e.getMessage());
|
|
||||||
e.printStackTrace();
|
|
||||||
String key_1 = RedisUtil.STRIPE_EXCEPTION_LOG + DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_HH);
|
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);
|
String stackTrace = stripeService.getStackTrace(e, 10);
|
||||||
redisUtil.addToString(key_2, stackTrace);
|
redisUtil.addToString(key_2, stackTrace);
|
||||||
Long size = redisUtil.getSize(key_1);
|
Long size = redisUtil.getSize(key_1);
|
||||||
// 给我发送邮件
|
if ("1".equals(webhookReminderFlag) && size == 3) {
|
||||||
if (webhookReminderFlag.equals("1") && size == 3){
|
|
||||||
SendEmailUtil.commonExceptionReminder("Stripe Webhook 回调处理出现异常", new String[]{"xupei3360@163.com"});
|
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);
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "申请退款")
|
/* @Operation(summary = "申请退款")
|
||||||
@GetMapping("/trade/refund/{orderNo}/{reason}")
|
@GetMapping("/trade/refund/{orderNo}/{reason}")
|
||||||
public Response<HttpResponse<Refund>> refund(@PathVariable String orderNo, @PathVariable String reason) throws IOException {
|
public Response<HttpResponse<Refund>> refund(@PathVariable String orderNo, @PathVariable String reason) throws IOException {
|
||||||
String response = stripeService.refund(null,orderNo,reason);
|
String response = stripeService.refund(null,orderNo,reason);
|
||||||
@@ -85,7 +88,7 @@ public class StripeController {
|
|||||||
}else {
|
}else {
|
||||||
return Response.fail("Request for refund failed.");
|
return Response.fail("Request for refund failed.");
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
@Operation(summary = "获取订阅")
|
@Operation(summary = "获取订阅")
|
||||||
@GetMapping("/getSubscription")
|
@GetMapping("/getSubscription")
|
||||||
@@ -100,7 +103,8 @@ public class StripeController {
|
|||||||
@Operation(summary = "取消订阅")
|
@Operation(summary = "取消订阅")
|
||||||
@GetMapping("/cancelSubscription")
|
@GetMapping("/cancelSubscription")
|
||||||
public Response<String> cancelSubscription(@RequestParam String subscriptionId, @RequestParam(required = false) String reason) {
|
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");
|
return Response.success("success");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ public class SubscriptionPlanController {
|
|||||||
@Operation(summary = "activeSubscriptionPlan")
|
@Operation(summary = "activeSubscriptionPlan")
|
||||||
@GetMapping("/activeSubscriptionPlan")
|
@GetMapping("/activeSubscriptionPlan")
|
||||||
public Response<String> activeSubscriptionPlan() {
|
public Response<String> activeSubscriptionPlan() {
|
||||||
subscriptionPlanService.activeSubscriptionPlan();
|
subscriptionPlanService.activeSubscriptionPlan(null);
|
||||||
return Response.success();
|
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);
|
Long insertDesign(Design design);
|
||||||
|
|
||||||
List<UserDesignStatisticDTO> getDesignStatistic(String startTime, String endTime, List<Long> ids, String email,
|
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();
|
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 com.ai.da.mapper.primary.entity.Notification;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -20,5 +21,5 @@ public interface NotificationMapper extends CommonMapper<Notification> {
|
|||||||
|
|
||||||
void setPersonalNotificationAllRead(String type, Long receiverId, LocalDateTime time);
|
void setPersonalNotificationAllRead(String type, Long receiverId, LocalDateTime time);
|
||||||
|
|
||||||
List<Long> getUnreadSysNotification(Long accountId);
|
List<Long> getUnreadSysNotification(Long accountId, Date createTime);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.common.config.mybatis.plus.CommonMapper;
|
||||||
import com.ai.da.mapper.primary.entity.WorkspaceRelStyle;
|
import com.ai.da.mapper.primary.entity.WorkspaceRelStyle;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapper 接口
|
* Mapper 接口
|
||||||
*
|
*
|
||||||
@@ -11,5 +13,11 @@ import com.ai.da.mapper.primary.entity.WorkspaceRelStyle;
|
|||||||
*/
|
*/
|
||||||
public interface WorkspaceRelStyleMapper extends CommonMapper<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 com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
import lombok.experimental.Accessors;
|
import lombok.experimental.Accessors;
|
||||||
|
|
||||||
import java.io.Serializable;
|
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;
|
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 Date updateDate;
|
||||||
|
|
||||||
private Integer isTrial;
|
private Integer isTrial;
|
||||||
@@ -142,4 +143,26 @@ public class Account implements Serializable {
|
|||||||
private String givenName;
|
private String givenName;
|
||||||
|
|
||||||
private Long subscriptionPlanId;
|
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;
|
private LocalDateTime updateDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象信息(JSON格式)
|
||||||
|
*/
|
||||||
|
private String object;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,6 @@ public class OrderInfo extends BaseEntity{
|
|||||||
|
|
||||||
private String note;
|
private String note;
|
||||||
|
|
||||||
private byte autoRenewal;
|
|
||||||
|
|
||||||
private String paymentType;//支付方式
|
private String paymentType;//支付方式
|
||||||
|
|
||||||
// 可用于标记用户订单是否首次订阅
|
// 可用于标记用户订单是否首次订阅
|
||||||
|
|||||||
@@ -67,6 +67,11 @@ public class SubscriptionPlan extends BaseEntity{
|
|||||||
*/
|
*/
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 国家或地区
|
||||||
|
*/
|
||||||
|
private String countryOrRegion;
|
||||||
|
|
||||||
// 在类内部定义的枚举
|
// 在类内部定义的枚举
|
||||||
@Getter
|
@Getter
|
||||||
public enum SubscriptionStatus {
|
public enum SubscriptionStatus {
|
||||||
|
|||||||
@@ -90,6 +90,19 @@ public class TDesignPythonOutfitDetail implements Serializable {
|
|||||||
*/
|
*/
|
||||||
@Schema(description = "图层优先级")
|
@Schema(description = "图层优先级")
|
||||||
private Integer priority;
|
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 Long accountId;
|
||||||
private String path;
|
private String path;
|
||||||
private LocalDateTime dataTime;
|
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")
|
@Schema(description = "项目id")
|
||||||
private Long projectId;
|
private Long projectId;
|
||||||
|
|
||||||
|
@NotBlank(message = "designType cannot be empty")
|
||||||
|
@Schema(description = "default -> 新增sketch || merge")
|
||||||
|
private String designType;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "DesignSingleIncludeLayersDTO{" +
|
return "DesignSingleIncludeLayersDTO{" +
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ai.da.model.dto;
|
package com.ai.da.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import com.ai.da.mapper.primary.entity.Gradient;
|
import com.ai.da.mapper.primary.entity.Gradient;
|
||||||
@@ -67,4 +68,18 @@ public class DesignSingleItemDTO implements Serializable {
|
|||||||
|
|
||||||
private PartialDesignDTO partialDesign;
|
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格式")
|
@Schema(description = "图片的base64格式")
|
||||||
private String partialDesignBase64;
|
private String partialDesignBase64;
|
||||||
|
|
||||||
@Schema(description = "图层信息")
|
/* @Schema(description = "图层信息")
|
||||||
private List<String> layers;
|
private List<String> layers;*/
|
||||||
|
|
||||||
public PartialDesignDTO(String partialDesignMinioPath) {
|
public PartialDesignDTO(String partialDesignMinioPath) {
|
||||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PartialDesignDTO(String partialDesignMinioPath, List<String> layers) {
|
/* public PartialDesignDTO(String partialDesignMinioPath, List<String> layers) {
|
||||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||||
this.layers = layers;
|
this.layers = layers;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
public PartialDesignDTO(String partialDesignMinioPath, String partialDesignPath) {
|
public PartialDesignDTO(String partialDesignMinioPath, String partialDesignPath) {
|
||||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.ai.da.model.dto;
|
package com.ai.da.model.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Hidden;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ public class ProductPurchaseDTO {
|
|||||||
@Schema(description = "EcoMonth || Month || Year")
|
@Schema(description = "EcoMonth || Month || Year")
|
||||||
private String subscribeType;
|
private String subscribeType;
|
||||||
|
|
||||||
|
@Hidden
|
||||||
@Schema(description = "是否自动续订 one_time || recurring")
|
@Schema(description = "是否自动续订 one_time || recurring")
|
||||||
private Boolean autoRenewal;
|
private Boolean autoRenewal;
|
||||||
|
|
||||||
|
|||||||
@@ -48,4 +48,7 @@ public class SubscriptionPlanDTO {
|
|||||||
@Schema(description = "订阅计划状态")
|
@Schema(description = "订阅计划状态")
|
||||||
private String status;
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "国家或地区")
|
||||||
|
private String CountryOrRegion;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,12 @@ public class SubscriptionPlanPageQuery extends QueryPageByTimeDTO {
|
|||||||
@Schema(description = "组织id")
|
@Schema(description = "组织id")
|
||||||
private Long organizationId;
|
private Long organizationId;
|
||||||
|
|
||||||
@Schema(description = "管理id")
|
@Schema(description = "管理员id")
|
||||||
private Long adminAccId;
|
private Long adminAccId;
|
||||||
|
|
||||||
@Schema(description = "状态 PENDING||ACTIVE||EXPIRED")
|
@Schema(description = "状态 PENDING||ACTIVE||EXPIRED")
|
||||||
private List<String> status;
|
private List<String> status;
|
||||||
|
|
||||||
|
@Schema(description = "国家或地区")
|
||||||
|
private String countryOrRegion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,4 +32,7 @@ public class UpdateSubscriptionPlanDTO {
|
|||||||
@Schema(description = "订阅重命名")
|
@Schema(description = "订阅重命名")
|
||||||
private String name;
|
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;
|
||||||
|
}
|
||||||
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
import com.ai.da.mapper.primary.entity.Gradient;
|
import com.ai.da.mapper.primary.entity.Gradient;
|
||||||
@@ -62,11 +62,11 @@ public class DesignItemClothesDetailVO {
|
|||||||
@Schema(description = "渐变色信息")
|
@Schema(description = "渐变色信息")
|
||||||
private Gradient gradient;
|
private Gradient gradient;
|
||||||
|
|
||||||
@Schema(description = "未分割的图层")
|
/* @Schema(description = "未分割的图层")
|
||||||
private String undividedLayer;
|
private String undividedLayer;
|
||||||
|
|
||||||
@Schema(description = "添加single印花的未分割的图层")
|
@Schema(description = "添加single印花的未分割的图层")
|
||||||
private String undividedLayerWithSinglePrint;
|
private String undividedLayerWithSinglePrint;*/
|
||||||
|
|
||||||
@Schema(description = "局部design")
|
@Schema(description = "局部design")
|
||||||
private PartialDesignDTO partialDesign;
|
private PartialDesignDTO partialDesign;
|
||||||
|
|||||||
@@ -59,4 +59,16 @@ public class DesignPythonOutfitVO {
|
|||||||
* 图层优先级 从10开始,优先级数字越大越靠近上层
|
* 图层优先级 从10开始,优先级数字越大越靠近上层
|
||||||
*/
|
*/
|
||||||
private Integer priority;
|
private Integer priority;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 镜像模式
|
||||||
|
*/
|
||||||
|
@Schema(description = "镜像模式")
|
||||||
|
private int[] transpose;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旋转角度
|
||||||
|
*/
|
||||||
|
@Schema(description = "旋转角度")
|
||||||
|
private Double rotate;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ public class DesignSinglePrint implements Serializable {
|
|||||||
@Schema(description = "印花优先级")
|
@Schema(description = "印花优先级")
|
||||||
private Integer priority;
|
private Integer priority;
|
||||||
|
|
||||||
|
private String object;
|
||||||
|
|
||||||
public DesignSinglePrint() {
|
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 Integer systemUser;
|
||||||
|
|
||||||
|
private Long subscriptionPlanId;
|
||||||
|
|
||||||
|
private Long organizationId;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,4 +45,7 @@ public class SubscriptionPlanVO {
|
|||||||
@Schema(description = "命名")
|
@Schema(description = "命名")
|
||||||
private String name;
|
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 single_overall;
|
||||||
|
|
||||||
private String preview_submit;
|
// private String preview_submit;
|
||||||
|
|
||||||
private String switch_category;
|
private String switch_category;
|
||||||
/**
|
/**
|
||||||
@@ -40,4 +40,7 @@ public class DesignPythonBasic {
|
|||||||
|
|
||||||
private Boolean layer_order = Boolean.FALSE;
|
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 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(),
|
public static List<String> OUTWEAR_DRESS_BLOUSE = Arrays.asList(CollectionLevel2TypeEnum.OUTWEAR.getRealName(),
|
||||||
CollectionLevel2TypeEnum.DRESS.getRealName(), CollectionLevel2TypeEnum.BLOUSE.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,
|
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,
|
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.type = type;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.color = color;
|
this.color = color;
|
||||||
@@ -157,6 +174,9 @@ public class DesignPythonItem {
|
|||||||
this.gradient = gradient;
|
this.gradient = gradient;
|
||||||
this.gradientString = gradientString;
|
this.gradientString = gradientString;
|
||||||
this.seg_mask_url = seg_mask_url;
|
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) {
|
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;
|
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 io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class DesignPythonItemPrint {
|
public class DesignPythonItemPrint {
|
||||||
@@ -53,7 +53,7 @@ public class DesignPythonItemPrint {
|
|||||||
if (ifDesign){
|
if (ifDesign){
|
||||||
this.print_path_list = print_path_list;
|
this.print_path_list = print_path_list;
|
||||||
this.location = Collections.singletonList(Arrays.asList(0.0f, 0.0f));
|
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);
|
this.print_angle_list = Arrays.asList(0.0, 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -246,4 +246,6 @@ public interface AccountService extends IService<Account> {
|
|||||||
void setEduAdminToExpire(Account adminAccount);
|
void setEduAdminToExpire(Account adminAccount);
|
||||||
|
|
||||||
String getOrganizationTypeByRole(Integer roleNum);
|
String getOrganizationTypeByRole(Integer roleNum);
|
||||||
|
|
||||||
|
void validateUserValidaExpire(Account account);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public interface ConvenientInquiryService extends IService<Questionnaire> {
|
|||||||
|
|
||||||
IPage<Account> getUserInfo(QueryUserConditionsVO queryUserConditionsVO);
|
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);
|
PageBaseResponse<PaymentInfoVO> queryTransactionRecords(QueryPaymentInfoDTO queryPaymentInfoDTO);
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ public interface DesignItemService extends IService<DesignItem> {
|
|||||||
|
|
||||||
DesignSingleVO designSingleIncludeLayers(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO);
|
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);
|
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 productId, String paymentType, HttpServletRequest request);
|
||||||
|
|
||||||
OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
||||||
HttpServletRequest request, byte autoRenewal);
|
HttpServletRequest request);
|
||||||
|
|
||||||
void saveCodeUrl(String orderNo, String codeUrl);
|
void saveCodeUrl(String orderNo, String codeUrl);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
|||||||
import com.paypal.orders.Order;
|
import com.paypal.orders.Order;
|
||||||
import com.stripe.model.Charge;
|
import com.stripe.model.Charge;
|
||||||
import com.stripe.model.Invoice;
|
import com.stripe.model.Invoice;
|
||||||
|
import com.stripe.model.PaymentMethod;
|
||||||
|
import com.stripe.model.checkout.Session;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -23,9 +25,15 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
|||||||
|
|
||||||
void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO, String type);
|
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);
|
List<PaymentInfo> getPaymentInfoByOrderNo(String orderId, String order);
|
||||||
|
|
||||||
@@ -35,5 +43,9 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
|||||||
|
|
||||||
List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode);
|
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 publishMessageToGenerate(String message);
|
||||||
|
|
||||||
|
void publishMessageToGenerateResult(String message);
|
||||||
|
|
||||||
void publishMessageToSR(String message);
|
void publishMessageToSR(String message);
|
||||||
|
|
||||||
Integer getMessageCount(String queueUrl);
|
Integer getMessageCount(String queueUrl);
|
||||||
|
|||||||
@@ -24,10 +24,18 @@ public interface RefundInfoService extends IService<RefundInfo> {
|
|||||||
|
|
||||||
List<RefundInfo> getByChargeId(String chargeId);
|
List<RefundInfo> getByChargeId(String chargeId);
|
||||||
|
|
||||||
|
RefundInfo getByRefundId(String refundId);
|
||||||
|
|
||||||
RefundInfo createRefundForStripe(Refund refund);
|
RefundInfo createRefundForStripe(Refund refund);
|
||||||
|
|
||||||
RefundInfo updateRefundStatusForStripe(Refund refund);
|
RefundInfo updateRefundStatusForStripe(Refund refund);
|
||||||
|
|
||||||
RefundInfo updateRefundForStripe(Charge charge);
|
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);
|
SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId);
|
||||||
|
|
||||||
String refund(String amount, String orderId, String reason);
|
|
||||||
|
|
||||||
void checkOrderStatus(String orderNo);
|
void checkOrderStatus(String orderNo);
|
||||||
|
|
||||||
List<String> getSubscriptionIds(String name, String userEmail) throws StripeException;
|
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);
|
void cancelSubscriptionTemp(String subscriptionId);
|
||||||
|
|
||||||
Map<String, String> getPaymentMethod(String paymentMethodId);
|
|
||||||
|
|
||||||
boolean sendEmail(String subscriptionId, String type, String orderNo);
|
boolean sendEmail(String subscriptionId, String type, String orderNo);
|
||||||
|
|
||||||
String getLanguage(String language, String country, String type);
|
String getLanguage(String language, String country, String type);
|
||||||
|
|
||||||
/*void updateSubscription(String subscriptionId);
|
|
||||||
|
|
||||||
void resume(String subscriptionId);*/
|
|
||||||
|
|
||||||
// void subscriptionReminder();
|
// void subscriptionReminder();
|
||||||
|
|
||||||
void checkSubscriptionExpiration();
|
|
||||||
|
|
||||||
String createSubscriptionTemp(String name, String email);
|
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);
|
// Map getIp(HttpServletRequest request);
|
||||||
|
|
||||||
String getStackTrace(Exception e, int maxLines);
|
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 switchSubAccSubscriptionPlan(Long subscriptionPlanId, Long subAccId);
|
||||||
|
|
||||||
void activeSubscriptionPlan();
|
void activeSubscriptionPlan(Long planId);
|
||||||
|
|
||||||
void expireSubscription();
|
void expireSubscription();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,9 +39,10 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.poi.ss.usermodel.*;
|
import org.apache.poi.ss.usermodel.*;
|
||||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
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.core.io.ClassPathResource;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
@@ -83,6 +84,9 @@ import java.util.stream.Collectors;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
|
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
|
||||||
|
@Resource
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private AccountMapper accountMapper;
|
private AccountMapper accountMapper;
|
||||||
|
|
||||||
@@ -132,6 +136,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
@Resource
|
@Resource
|
||||||
private RedisUtil redisUtil;
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private com.ai.da.common.security.config.SecurityProperties securityProperties;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private UserFollowService userFollowService;
|
private UserFollowService userFollowService;
|
||||||
|
|
||||||
@@ -237,12 +244,13 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
AccountLoginVO response = CopyUtil.copyObject(account, AccountLoginVO.class);
|
AccountLoginVO response = CopyUtil.copyObject(account, AccountLoginVO.class);
|
||||||
response.setEmail(account.getUserEmail());
|
response.setEmail(account.getUserEmail());
|
||||||
String token = LocalCacheUtils.getTokenCache(String.valueOf(account.getId()));
|
String token = LocalCacheUtils.getTokenCache(String.valueOf(account.getId()));
|
||||||
if (StringUtils.isNotBlank(token)) {
|
/*if (StringUtils.isNotBlank(token)) {
|
||||||
//用户已登入
|
//用户已登入
|
||||||
response.setToken(token);
|
response.setToken(token);
|
||||||
} else {
|
} else {
|
||||||
response.setToken(createAccountToken(account));
|
response.setToken(createAccountToken(account));
|
||||||
}
|
}*/
|
||||||
|
response.setToken(createAccountToken(account));
|
||||||
response.setUserId(account.getId());
|
response.setUserId(account.getId());
|
||||||
response.setSystemUser(account.getSystemUser());
|
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 static final Integer SYSTEM_USER_TYPE_EDU_ADMIN = 7;
|
||||||
|
|
||||||
private void validateUserValidaExpire(Account account) {
|
public void validateUserValidaExpire(Account account) {
|
||||||
Long currentTime = new Date().getTime();
|
Long currentTime = new Date().getTime();
|
||||||
if (account.getSystemUser().equals(0)) {
|
if (account.getSystemUser().equals(0)) {
|
||||||
return;
|
return;
|
||||||
@@ -294,7 +302,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
if (isEduAdmin) {
|
if (isEduAdmin) {
|
||||||
setEduAdminToExpire(account);
|
setEduAdminToExpire(account);
|
||||||
} else {
|
} else {
|
||||||
toVisitor(account);
|
// 这里调用代理的 toVisitor 方法
|
||||||
|
AccountService proxy = applicationContext.getBean(AccountService.class);
|
||||||
|
proxy.toVisitor(account);
|
||||||
// return;
|
// return;
|
||||||
throw new BusinessException("account.expired", ResultEnum.PROMPT.getCode());
|
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.setLanguage(account.getLanguage());
|
||||||
principal.setCountry(account.getCountry());
|
principal.setCountry(account.getCountry());
|
||||||
String token2 = jwtTokenHelper.createToken(principal);
|
String token2 = jwtTokenHelper.createToken(principal);
|
||||||
|
// 本地 JVM 缓存(适配旧逻辑)
|
||||||
LocalCacheUtils.setTokenCache(String.valueOf(account.getId()), token2);
|
LocalCacheUtils.setTokenCache(String.valueOf(account.getId()), token2);
|
||||||
|
// 同步写入 Redis,重启后仍然可用
|
||||||
|
long jwtExpiration = securityProperties.getJwtExpiration();
|
||||||
|
redisUtil.setLoginToken(account.getId(), token2, jwtExpiration);
|
||||||
return token2;
|
return token2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,21 +614,25 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean logout(AccountLogoutDTO accountLogoutDTO) {
|
public Boolean logout(AccountLogoutDTO accountLogoutDTO) {
|
||||||
//jwt本身失效比较难做 统一用缓存实现 删除缓存就失效
|
// jwt 本身失效比较难做,统一用缓存实现:删除缓存即失效
|
||||||
String token = LocalCacheUtils.getTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
|
String userIdStr = String.valueOf(accountLogoutDTO.getUserId());
|
||||||
if (StringUtils.isNotBlank(token)) {
|
LocalCacheUtils.delTokenCache(userIdStr);
|
||||||
LocalCacheUtils.delTokenCache(String.valueOf(accountLogoutDTO.getUserId()));
|
// 同时删除 Redis 中的 token,防止服务重启后仍然有效
|
||||||
}
|
redisUtil.deleteLoginToken(accountLogoutDTO.getUserId());
|
||||||
return Boolean.TRUE;
|
return Boolean.TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Boolean isLogin(AccountLogoutDTO accountLogoutDTO) {
|
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)) {
|
if (StringUtils.isNotBlank(token)) {
|
||||||
return Boolean.TRUE;
|
return Boolean.TRUE;
|
||||||
}
|
}
|
||||||
return Boolean.FALSE;
|
// 本地没有时,再查 Redis,保证服务重启后也能判断登录状态
|
||||||
|
String redisToken = redisUtil.getLoginToken(accountLogoutDTO.getUserId());
|
||||||
|
return StringUtils.isNotBlank(redisToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -812,6 +830,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
if (StringUtils.isNotBlank(token)) {
|
if (StringUtils.isNotBlank(token)) {
|
||||||
LocalCacheUtils.delTokenCache(String.valueOf(accountDelete.getId()));
|
LocalCacheUtils.delTokenCache(String.valueOf(accountDelete.getId()));
|
||||||
}
|
}
|
||||||
|
// 删除 Redis 中对应的登录 token
|
||||||
|
redisUtil.deleteLoginToken(accountDelete.getId());
|
||||||
if (!userName.equals(userToBeUpdate.getUserName())) {
|
if (!userName.equals(userToBeUpdate.getUserName())) {
|
||||||
// accountMapper.deleteById(accountDelete);
|
// accountMapper.deleteById(accountDelete);
|
||||||
log.info("排查用户被删除原因:deleteNoLoginRequired,true, 删除用户(改为降为游客)");
|
log.info("排查用户被删除原因:deleteNoLoginRequired,true, 删除用户(改为降为游客)");
|
||||||
@@ -1065,6 +1085,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
if (StringUtils.isNotBlank(token)) {
|
if (StringUtils.isNotBlank(token)) {
|
||||||
LocalCacheUtils.delTokenCache(String.valueOf(account.getId()));
|
LocalCacheUtils.delTokenCache(String.valueOf(account.getId()));
|
||||||
}
|
}
|
||||||
|
// 删除 Redis 中对应的登录 token
|
||||||
|
redisUtil.deleteLoginToken(account.getId());
|
||||||
// accountMapper.deleteById(account.getId());
|
// accountMapper.deleteById(account.getId());
|
||||||
log.info("排查用户被删除原因:deleteNoLoginRequiredNew,删除用户(改为降为游客)");
|
log.info("排查用户被删除原因:deleteNoLoginRequiredNew,删除用户(改为降为游客)");
|
||||||
accountMapper.toVisitor(account.getId());
|
accountMapper.toVisitor(account.getId());
|
||||||
@@ -1269,7 +1291,8 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
account.setSystemUser(3);
|
account.setSystemUser(3);
|
||||||
account.setIsTrial(1);
|
account.setIsTrial(1);
|
||||||
account.setCredits(BigDecimal.valueOf(50));
|
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.setIsBeginner(1);
|
||||||
account.setValidStartTime(Instant.now().toEpochMilli());
|
account.setValidStartTime(Instant.now().toEpochMilli());
|
||||||
account.setCreateDate(new Date());
|
account.setCreateDate(new Date());
|
||||||
@@ -1623,6 +1646,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
log.warn("当前用户 {} 在AiDA中没有账号", email);
|
log.warn("当前用户 {} 在AiDA中没有账号", email);
|
||||||
throw new BusinessException("user.has.no.account", ResultEnum.PROMPT.getCode());
|
throw new BusinessException("user.has.no.account", ResultEnum.PROMPT.getCode());
|
||||||
}
|
}
|
||||||
|
// 解决循环依赖问题
|
||||||
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
||||||
// 2、先判断当前用户是否已经填写过问卷
|
// 2、先判断当前用户是否已经填写过问卷
|
||||||
CreditsDetail record = creditsService.getByAccountIdAndChangeEvent(account.getId(), "Fill out the questionnaire", "+100");
|
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);
|
return baseMapper.selectList(queryWrapper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void toVisitor(Account account) {
|
public void toVisitor(Account account) {
|
||||||
accountMapper.toVisitor(account.getId());
|
accountMapper.toVisitor(account.getId());
|
||||||
}
|
}
|
||||||
@@ -2491,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());
|
subAccount.setValidEndTime(adminAcc.getValidEndTime());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3358,12 +3383,14 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
Account account = accountMapper.selectById(accountId);
|
Account account = accountMapper.selectById(accountId);
|
||||||
if (!Objects.isNull(account.getValidEndTime())
|
if (!Objects.isNull(account.getValidEndTime())
|
||||||
&& account.getValidEndTime().equals(currentPeriodEnd * 1000)) {
|
&& account.getValidEndTime().equals(currentPeriodEnd * 1000)) {
|
||||||
|
log.info("accountId:{}未更新账号有效期。current validEnd:{}, new validEnd:{}", accountId, account.getValidEndTime(), currentPeriodEnd);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
account.setValidEndTime(currentPeriodEnd * 1000);
|
account.setValidEndTime(currentPeriodEnd * 1000);
|
||||||
accountMapper.updateById(account);
|
accountMapper.updateById(account);
|
||||||
|
log.info("accountId:{} 将账号有效期更新到 {}", accountId, currentPeriodEnd);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -3376,34 +3403,36 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
|||||||
if (description.equals(ProductEnum.DailySubscription.getName())) {
|
if (description.equals(ProductEnum.DailySubscription.getName())) {
|
||||||
productCredits = ProductEnum.DailySubscription.getCredits();
|
productCredits = ProductEnum.DailySubscription.getCredits();
|
||||||
account.setSystemUser(3);
|
account.setSystemUser(3);
|
||||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_WEEKLY.getValue())));
|
|
||||||
} else if (description.equals(ProductEnum.MonthlySubscription.getName())) {
|
} else if (description.equals(ProductEnum.MonthlySubscription.getName())) {
|
||||||
productCredits = ProductEnum.MonthlySubscription.getCredits();
|
productCredits = ProductEnum.MonthlySubscription.getCredits();
|
||||||
account.setSystemUser(2);
|
account.setSystemUser(2);
|
||||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_MONTHLY.getValue())));
|
|
||||||
} else if (description.equals(ProductEnum.Eco_MonthlySubscription.getName())) {
|
} else if (description.equals(ProductEnum.Eco_MonthlySubscription.getName())) {
|
||||||
productCredits = ProductEnum.Eco_MonthlySubscription.getCredits();
|
productCredits = ProductEnum.Eco_MonthlySubscription.getCredits();
|
||||||
account.setSystemUser(2);
|
account.setSystemUser(2);
|
||||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_MONTHLY_ECO.getValue())));
|
|
||||||
} else if (description.equals(ProductEnum.AnnualSubscription.getName())) {
|
} else if (description.equals(ProductEnum.AnnualSubscription.getName())) {
|
||||||
productCredits = ProductEnum.AnnualSubscription.getCredits();
|
productCredits = ProductEnum.AnnualSubscription.getCredits();
|
||||||
account.setSystemUser(1);
|
account.setSystemUser(1);
|
||||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_YEARLY.getValue())));
|
|
||||||
} else {
|
} else {
|
||||||
log.error("未知订阅类型: {}", description);
|
log.error("未知订阅类型: {}", description);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
account.setCredits(BigDecimal.valueOf(productCredits));
|
||||||
accountMapper.updateById(account);
|
accountMapper.updateById(account);
|
||||||
|
log.info("accountId:{},更新用户角色为{},总积分为{}", accountId, account.getSystemUser(), productCredits);
|
||||||
|
|
||||||
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
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)) {
|
if (Objects.isNull(creditsDetail)) {
|
||||||
creditsService.insertToCreditsDetail(accountId,
|
creditsService.insertToCreditsDetail(accountId,
|
||||||
description + "--Stripe",
|
description + "--Stripe",
|
||||||
String.valueOf(productCredits),
|
String.valueOf(productCredits),
|
||||||
"positive", orderNo);
|
"positive", orderNo);
|
||||||
}
|
}*/
|
||||||
} else {
|
} else {
|
||||||
log.error("orderNo: {} 无法找到对应的记录", orderNo);
|
log.error("orderNo: {} 无法找到对应的记录", orderNo);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import jakarta.annotation.Resource;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
@@ -80,9 +79,7 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
|||||||
affiliate.setPromotionMethod(promotionMethod);
|
affiliate.setPromotionMethod(promotionMethod);
|
||||||
baseMapper.insert(affiliate);
|
baseMapper.insert(affiliate);
|
||||||
// 邮件通知审批者
|
// 邮件通知审批者
|
||||||
String merchantEmail = "kimwong@code-create.com.hk";
|
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||||
String developer = "xupei3360@163.com";
|
|
||||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
|
||||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||||
}else {
|
}else {
|
||||||
@@ -440,9 +437,7 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
|||||||
affiliateEmailParamsDTO.setUnpaidEarnings(String.valueOf(unpaidCommission));
|
affiliateEmailParamsDTO.setUnpaidEarnings(String.valueOf(unpaidCommission));
|
||||||
affiliateEmailParamsDTO.setPaidEarnings(String.valueOf(paidCommission));
|
affiliateEmailParamsDTO.setPaidEarnings(String.valueOf(paidCommission));
|
||||||
|
|
||||||
String merchantEmail = "kimwong@code-create.com.hk";
|
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||||
String developer = "xupei3360@163.com";
|
|
||||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
|
||||||
// 邮件通知
|
// 邮件通知
|
||||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary");
|
SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary");
|
||||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), affiliateEmailParamsDTO, "summary");
|
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), affiliateEmailParamsDTO, "summary");
|
||||||
@@ -607,4 +602,8 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
|||||||
coupon.setUnpaidCommission(unpaidCommission);
|
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()))
|
.filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName()))
|
||||||
.map(DesignCollectionPrintElementDTO::getId)
|
.map(DesignCollectionPrintElementDTO::getId)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
List<CollectionElement> printBoardElements = new ArrayList<>();
|
||||||
|
elementVO.setPrintBoardElements(printBoardElements);
|
||||||
if (!CollectionUtils.isEmpty(printBoardIds)) {
|
if (!CollectionUtils.isEmpty(printBoardIds)) {
|
||||||
// 从数据库批量查询printBoard元素
|
// 从数据库批量查询printBoard元素
|
||||||
List<CollectionElement> printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds);
|
printBoardElements.addAll(collectionElementMapper.selectBatchIds(printBoardIds));
|
||||||
// 验证查询结果的完整性
|
// 验证查询结果的完整性
|
||||||
if (CollectionUtil.isEmpty(printBoardElements) || printBoardElements.size() != printBoardIds.size()) {
|
if (CollectionUtil.isEmpty(printBoardElements) || printBoardElements.size() != printBoardIds.size()) {
|
||||||
throw new BusinessException("get.printBoards.data.is.mismatch");
|
throw new BusinessException("get.printBoards.data.is.mismatch");
|
||||||
}
|
}
|
||||||
elementVO.setPrintBoardElements(printBoardElements);
|
// elementVO.setPrintBoardElements(printBoardElements);
|
||||||
usedElementIds.addAll(printBoardIds); // 记录已使用的元素ID
|
usedElementIds.addAll(printBoardIds); // 记录已使用的元素ID
|
||||||
}
|
}
|
||||||
// 处理类型为LIBRARY的printBoard元素
|
// 处理类型为LIBRARY的printBoard元素
|
||||||
@@ -543,7 +545,8 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
|||||||
Map<Long, DesignCollectionPrintElementDTO> idToMap = designDTO.getPrintBoards()
|
Map<Long, DesignCollectionPrintElementDTO> idToMap = designDTO.getPrintBoards()
|
||||||
.stream()
|
.stream()
|
||||||
.collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v));
|
.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()
|
Map<Long, DesignCollectionPrintElementDTO> idToMap = designDTO.getPrintBoards()
|
||||||
.stream()
|
.stream()
|
||||||
.collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v));
|
.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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.poi.xssf.usermodel.*;
|
import org.apache.poi.xssf.usermodel.*;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -149,7 +148,17 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
|||||||
|| ADMIN_IDS.contains(account.getId())
|
|| ADMIN_IDS.contains(account.getId())
|
||||||
|| ADMIN_IDS_READ_ONLY.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)) {
|
if (StringUtil.isNullOrEmpty(endTime)) {
|
||||||
// yyyy-MM-dd HH:mm:ss "HH"表示24小时制 "hh"表示12小时制
|
// yyyy-MM-dd HH:mm:ss "HH"表示24小时制 "hh"表示12小时制
|
||||||
endTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
endTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||||
@@ -173,7 +182,7 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
|||||||
default:
|
default:
|
||||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
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 {
|
} else {
|
||||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
||||||
}
|
}
|
||||||
@@ -695,14 +704,19 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
|||||||
|
|
||||||
String username = UserContext.getUserHolder().getUsername();
|
String username = UserContext.getUserHolder().getUsername();
|
||||||
Account account = new Account();
|
Account account = new Account();
|
||||||
|
account.setId(accountId);
|
||||||
// 修改用户有效期截止日期、用户类型、积分
|
// 修改用户有效期截止日期、用户类型、积分
|
||||||
if (!Objects.isNull(validEndTime)) {
|
if (!Objects.isNull(validEndTime)) {
|
||||||
account.setValidEndTime(validEndTime);
|
account.setValidEndTime(validEndTime);
|
||||||
log.info("管理员:{},修改用户 {} 信息,将账号到期时间置为:{}", username, accountId, validEndTime);
|
log.info("管理员:{},修改用户 {} 信息,将账号到期时间置为:{}", username, accountId, validEndTime);
|
||||||
}
|
}
|
||||||
if (!Objects.isNull(systemUser)) {
|
if (!Objects.isNull(systemUser) && !systemUser.equals(0)) {
|
||||||
account.setSystemUser(systemUser);
|
account.setSystemUser(systemUser);
|
||||||
log.info("管理员:{},修改用户 {} 信息,将账号身份置为:{}", username, accountId, systemUser);
|
log.info("管理员:{},修改用户 {} 信息,将账号身份置为:{}", username, accountId, systemUser);
|
||||||
|
} else if (systemUser.equals(0)){
|
||||||
|
// 将用户身份设置为游客
|
||||||
|
accountService.toVisitor(account);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
/*if (!StringUtils.isNullOrEmpty(systemUser)) {
|
/*if (!StringUtils.isNullOrEmpty(systemUser)) {
|
||||||
int systemUser = 0;
|
int systemUser = 0;
|
||||||
@@ -728,7 +742,6 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// todo 如果修改管理员账号的积分上限或子账号数量,则其所有子账号的积分上限需要重新计算
|
// todo 如果修改管理员账号的积分上限或子账号数量,则其所有子账号的积分上限需要重新计算
|
||||||
account.setId(accountId);
|
|
||||||
account.setUpdateDate(new Date());
|
account.setUpdateDate(new Date());
|
||||||
return accountMapper.updateById(account) == 1;
|
return accountMapper.updateById(account) == 1;
|
||||||
// accountService.update(account,null);
|
// accountService.update(account,null);
|
||||||
@@ -774,6 +787,14 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
|||||||
queryWrapper.lt("create_date", queryUserConditionsVO.getEndTime());
|
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())) {
|
if (!StringUtils.isNullOrEmpty(queryUserConditionsVO.getOrder()) && !StringUtils.isNullOrEmpty(queryUserConditionsVO.getOrderBy())) {
|
||||||
String orderBy = "id";
|
String orderBy = "id";
|
||||||
@@ -798,27 +819,46 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
|||||||
return accountMapper.selectPage(new Page<>(queryUserConditionsVO.getPage(), queryUserConditionsVO.getSize()), queryWrapper);
|
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();
|
Long accountId = UserContext.getUserHolder().getId();
|
||||||
Account account = accountMapper.selectById(accountId);
|
Account account = accountMapper.selectById(accountId);
|
||||||
// 允许查看数据的用户id
|
|
||||||
if (Objects.nonNull(account.getSystemUser())
|
// 权限校验
|
||||||
&& (account.getSystemUser().equals(5)
|
if (Objects.isNull(account.getSystemUser())
|
||||||
|| account.getSystemUser().equals(7)
|
|| (!account.getSystemUser().equals(5)
|
||||||
|| ADMIN_IDS.contains(account.getId())
|
&& !account.getSystemUser().equals(7)
|
||||||
|| ADMIN_IDS_READ_ONLY.contains(account.getId())
|
&& !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 {
|
|
||||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
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")) {
|
if (!StringUtil.isNullOrEmpty(queryPaymentInfoDTO.getOrder()) && queryPaymentInfoDTO.getOrder().equals("ASC")) {
|
||||||
order = "ASC";
|
order = "ASC";
|
||||||
}
|
}
|
||||||
|
String status = StringUtil.isNullOrEmpty(queryPaymentInfoDTO.getStatus()) ? "Success" : queryPaymentInfoDTO.getStatus();
|
||||||
List<PaymentInfoVO> paymentInfoVOS = paymentInfoMapper.queryPaymentInfo(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
List<PaymentInfoVO> paymentInfoVOS = paymentInfoMapper.queryPaymentInfo(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
queryPaymentInfoDTO.getType(), status,
|
||||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(),
|
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(),
|
||||||
size, offset, order, queryPaymentInfoDTO.getPayer());
|
size, offset, order, queryPaymentInfoDTO.getPayer());
|
||||||
// 查询数据总量
|
// 查询数据总量
|
||||||
Long total = paymentInfoMapper.queryPaymentInfoCount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
Long total = paymentInfoMapper.queryPaymentInfoCount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
queryPaymentInfoDTO.getType(), status,
|
||||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
||||||
// 查询符合查询条件的总金额
|
// 查询符合查询条件的总金额
|
||||||
BigDecimal payerTotal = paymentInfoMapper.queryTotalPaymentAmount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
BigDecimal payerTotal = paymentInfoMapper.queryTotalPaymentAmount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
queryPaymentInfoDTO.getType(), status,
|
||||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
||||||
// 总页数
|
// 总页数
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
|||||||
*
|
*
|
||||||
* @param changeEvent 导致积分变更的事件
|
* @param changeEvent 导致积分变更的事件
|
||||||
* @param credits 变更的积分
|
* @param credits 变更的积分
|
||||||
* @param changeType 变更类型 : positive->增 negative->减
|
* @param changeType 变更类型 : positive->增 negative->减 set->重置
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo) {
|
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)) {
|
if ("positive".equals(changeType)) {
|
||||||
// finalCredits = account.getCredits().add(new BigDecimal(credits));
|
// finalCredits = account.getCredits().add(new BigDecimal(credits));
|
||||||
changeCredits = "+" + credits;
|
changeCredits = "+" + credits;
|
||||||
} else {
|
} else if ("negative".equals(changeType)) {
|
||||||
// finalCredits = account.getCredits().subtract(new BigDecimal(credits));
|
// finalCredits = account.getCredits().subtract(new BigDecimal(credits));
|
||||||
changeCredits = "-" + credits;
|
changeCredits = "-" + credits;
|
||||||
|
} else {
|
||||||
|
changeCredits = credits;
|
||||||
}
|
}
|
||||||
creditsDetail.setAccountId(accountId);
|
creditsDetail.setAccountId(accountId);
|
||||||
creditsDetail.setChangeEvent(changeEvent);
|
creditsDetail.setChangeEvent(changeEvent);
|
||||||
@@ -107,6 +109,7 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
|||||||
creditsDetail.setCreateTime(LocalDateTime.now());
|
creditsDetail.setCreateTime(LocalDateTime.now());
|
||||||
|
|
||||||
baseMapper.insert(creditsDetail);
|
baseMapper.insert(creditsDetail);
|
||||||
|
log.info("creditsDetail inserted");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -363,9 +363,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
public List<TDesignPythonOutfitDetail> saveDesignSingleItemDetailAndLayers(DesignPythonObjects pythonObjects
|
public List<TDesignPythonOutfitDetail> saveDesignSingleItemDetailAndLayers(DesignPythonObjects pythonObjects
|
||||||
, Long designId, Long designItemId, Long userId
|
, Long designId, Long designItemId, Long userId
|
||||||
, JSONObject outfit, String timeZone, List<DesignSingleItemDTO> designSingleItemDTOList
|
, JSONObject outfit, String timeZone, List<DesignSingleItemDTO> designSingleItemDTOList
|
||||||
, Map<String, List<String>> priorityAndUndividedLayer
|
/*, Map<String, List<String>> priorityAndUndividedLayer*/
|
||||||
, boolean changeModelFlag
|
, boolean changeModelFlag
|
||||||
, Long modelId, String modelType, boolean isSingleCollectionFlag) {
|
, Long modelId, String modelType, boolean isSingleCollectionFlag, String designType) {
|
||||||
|
|
||||||
DesignItem designItem = new DesignItem();
|
DesignItem designItem = new DesignItem();
|
||||||
// String url = pythonObjects.getObjects().get(0).getBasic().getSave_name();
|
// 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.setColor(detail.getColor());
|
||||||
designItemDetail.setIconPath(detail.getIcon());
|
designItemDetail.setIconPath(detail.getIcon());
|
||||||
// designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getType().toLowerCase()));
|
// designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getType().toLowerCase()));
|
||||||
|
/*// 取消存储UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||||
if (!detail.getType().equals("Body")) {
|
if (!detail.getType().equals("Body")) {
|
||||||
designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(0));
|
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(0))) {
|
||||||
designItemDetail.setUndividedLayerWithSinglePrint(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(1));
|
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表中 这里还要存吗?
|
// 印花存储在design_item_detail_print表中 这里还要存吗?
|
||||||
// DesignPythonItemPrint printObject = detail.getPrintToPython();
|
// DesignPythonItemPrint printObject = detail.getPrintToPython();
|
||||||
// designItemDetail.setPrintPath(Objects.isNull(printObject) ? "" : printObject.getPath());
|
// designItemDetail.setPrintPath(Objects.isNull(printObject) ? "" : printObject.getPath());
|
||||||
@@ -436,7 +440,18 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
// designItemDetail.setPrintJson(JSON.toJSONString(printObject));
|
// designItemDetail.setPrintJson(JSON.toJSONString(printObject));
|
||||||
|
|
||||||
designItemDetail.setPartialDesign(Objects.isNull(detail.getPrint()) ? null : detail.getPrint().getPartial());
|
designItemDetail.setPartialDesign(Objects.isNull(detail.getPrint()) ? null : detail.getPrint().getPartial());
|
||||||
|
designItemDetail.setGradientString(detail.getGradientString());
|
||||||
|
|
||||||
designItemDetails.add(designItemDetail);
|
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()
|
priorityOffset = designSingleItemDTOList.stream()
|
||||||
.collect(Collectors.toMap(DesignSingleItemDTO::getPriority, DesignSingleItemDTO::getOffset));
|
.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<>();
|
List<TDesignPythonOutfitDetail> list = new ArrayList<>();
|
||||||
for (int i = 0; i < layers.size(); i++) {
|
for (int i = 0; i < layers.size(); i++) {
|
||||||
JSONObject jsonObject = layers.getJSONObject(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.setOffset(String.valueOf(priorityOffset.get(Math.abs(priority))));
|
||||||
}
|
}
|
||||||
designPythonOutfitDetail.setPriority(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);
|
list.add(designPythonOutfitDetail);
|
||||||
}
|
}
|
||||||
@@ -683,6 +709,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public DesignSingleVO designSingleIncludeLayers(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO) {
|
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数据太长,所以这里去掉
|
// 记录入参 base64数据太长,所以这里去掉
|
||||||
DesignSingleIncludeLayersDTO clone = SerializationUtils.clone(designSingleIncludeLayersDTO);
|
DesignSingleIncludeLayersDTO clone = SerializationUtils.clone(designSingleIncludeLayersDTO);
|
||||||
clone.getDesignSingleItemDTOList().forEach(i -> {
|
clone.getDesignSingleItemDTOList().forEach(i -> {
|
||||||
@@ -706,6 +735,17 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
log.info("set partialDesignBase64为空,便于日志打印");
|
log.info("set partialDesignBase64为空,便于日志打印");
|
||||||
i.getPartialDesign().setPartialDesignBase64(null);
|
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));
|
log.info("designSingle request入参 ==> " + JSONObject.toJSONString(clone));
|
||||||
@@ -800,7 +840,7 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
maskBase64ToPath(designSingleIncludeLayersDTO, setNull);
|
maskBase64ToPath(designSingleIncludeLayersDTO, setNull);
|
||||||
// maskBase64ToPath(designSingleIncludeLayersDTO, Boolean.TRUE);
|
// maskBase64ToPath(designSingleIncludeLayersDTO, Boolean.TRUE);
|
||||||
|
|
||||||
partialDesignBase64ToImage(designSingleIncludeLayersDTO, userId, designSingleIncludeLayersDTO.getIsPreview());
|
partialDesignBase64ToImage(designSingleIncludeLayersDTO, userId, designSingleIncludeLayersDTO.getIsPreview(), designSingleIncludeLayersDTO.getDesignType());
|
||||||
|
|
||||||
// 组装入参
|
// 组装入参
|
||||||
DesignPythonObjects objects = pythonService.covertDesignSingleParam(
|
DesignPythonObjects objects = pythonService.covertDesignSingleParam(
|
||||||
@@ -818,13 +858,13 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
JSONObject outfit = data.getJSONObject("0");
|
JSONObject outfit = data.getJSONObject("0");
|
||||||
|
|
||||||
JSONArray layers = outfit.getJSONArray("layers");
|
JSONArray layers = outfit.getJSONArray("layers");
|
||||||
Map<String, List<String>> priorityAndUndividedLayer = setPriorityAndUndividedLayer(layers);
|
// Map<String, List<String>> priorityAndUndividedLayer = setPriorityAndUndividedLayer(layers, designSingleIncludeLayersDTO);
|
||||||
if (!designSingleIncludeLayersDTO.getIsPreview()) {
|
if (!designSingleIncludeLayersDTO.getIsPreview()) {
|
||||||
// 更新及保存图层信息
|
// 更新及保存图层信息
|
||||||
tDesignPythonOutfitDetails = saveDesignSingleItemDetailAndLayers(objects, design.getId(), designSingleIncludeLayersDTO.getDesignItemId()
|
tDesignPythonOutfitDetails = saveDesignSingleItemDetailAndLayers(objects, design.getId(), designSingleIncludeLayersDTO.getDesignItemId()
|
||||||
, userId, outfit, designSingleIncludeLayersDTO.getTimeZone()
|
, userId, outfit, designSingleIncludeLayersDTO.getTimeZone()
|
||||||
, designSingleIncludeLayersDTO.getDesignSingleItemDTOList()
|
, designSingleIncludeLayersDTO.getDesignSingleItemDTOList()
|
||||||
, priorityAndUndividedLayer, changeModelFlag, modelId, modelType, isSingleCollectionFlag);
|
/*, priorityAndUndividedLayer*/, changeModelFlag, modelId, modelType, isSingleCollectionFlag, designSingleIncludeLayersDTO.getDesignType());
|
||||||
|
|
||||||
saveCollectionElement(designSingleIncludeLayersDTO);
|
saveCollectionElement(designSingleIncludeLayersDTO);
|
||||||
} else {
|
} else {
|
||||||
@@ -858,8 +898,8 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
outfit.getString("synthesis_url"),
|
outfit.getString("synthesis_url"),
|
||||||
designSingleIncludeLayersDTO.getDesignSingleItemDTOList(),
|
designSingleIncludeLayersDTO.getDesignSingleItemDTOList(),
|
||||||
detailsVO,
|
detailsVO,
|
||||||
design.getSingleOverall(),
|
design.getSingleOverall()/*,
|
||||||
priorityAndUndividedLayer);
|
priorityAndUndividedLayer*/);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 方法1:仅查询(无事务)
|
// 方法1:仅查询(无事务)
|
||||||
@@ -919,12 +959,12 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
item.setMaskUrl(path);
|
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 -> {
|
designSingleIncludeLayersDTO.getDesignSingleItemDTOList().forEach(item -> {
|
||||||
PartialDesignDTO partialDesignDTO = item.getPartialDesign();
|
PartialDesignDTO partialDesignDTO = item.getPartialDesign();
|
||||||
if (!Objects.isNull(item.getPartialDesign())
|
if (!Objects.isNull(item.getPartialDesign())
|
||||||
@@ -946,21 +986,90 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
item.getPartialDesign().setPartialDesignMinioPath(newPath);
|
item.getPartialDesign().setPartialDesignMinioPath(newPath);
|
||||||
} else if (Objects.isNull(item.getPartialDesign())
|
} else if (Objects.isNull(item.getPartialDesign())
|
||||||
|| StringUtil.isNullOrEmpty(item.getPartialDesign().getPartialDesignMinioPath())) {
|
|| 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
|
@Override
|
||||||
public Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers) {
|
public Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers, DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO) {
|
||||||
HashMap<String, List<String>> priorityAndLayer = new HashMap<>();
|
String designType = "default";
|
||||||
for (int i = 0; i < layers.size(); i++) {
|
if (Objects.nonNull(designSingleIncludeLayersDTO)) {
|
||||||
JSONObject jsonObject = layers.getJSONObject(i);
|
designType = designSingleIncludeLayersDTO.getDesignType();
|
||||||
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")));
|
|
||||||
}
|
}
|
||||||
|
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;
|
return priorityAndLayer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1075,8 +1184,8 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
String currentFullBodyView,
|
String currentFullBodyView,
|
||||||
List<DesignSingleItemDTO> designSingleItemDTOList,
|
List<DesignSingleItemDTO> designSingleItemDTOList,
|
||||||
List<DesignPythonOutfitVO> layersObject,
|
List<DesignPythonOutfitVO> layersObject,
|
||||||
String singleOrOverall,
|
String singleOrOverall/*,
|
||||||
Map<String, List<String>> priorityAndUndividedLayer) {
|
Map<String, List<String>> priorityAndUndividedLayer*/) {
|
||||||
|
|
||||||
DesignSingleVO designSingleVO = new DesignSingleVO();
|
DesignSingleVO designSingleVO = new DesignSingleVO();
|
||||||
ArrayList<DesignItemClothesDetailVO> clothes = new ArrayList<>();
|
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);
|
String preSignedUrl = StringUtil.isNullOrEmpty(partialDesignMinioPath) ? null : minioUtil.getPreSignedUrl(partialDesignMinioPath, CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true);
|
||||||
designItemClothesDetailVO.setPartialDesign(new PartialDesignDTO(partialDesignMinioPath, preSignedUrl));
|
designItemClothesDetailVO.setPartialDesign(new PartialDesignDTO(partialDesignMinioPath, preSignedUrl));
|
||||||
|
|
||||||
|
/*// 取消存储/返回UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||||
if (priorityAndUndividedLayer.containsKey(singleItem.getPriority().toString())) {
|
if (priorityAndUndividedLayer.containsKey(singleItem.getPriority().toString())) {
|
||||||
designItemClothesDetailVO.setUndividedLayer(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(0), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(0))) {
|
||||||
designItemClothesDetailVO.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(1), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
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()));
|
body.setLayersObject(layersObject.stream().filter(layers -> layers.getImageCategory().equals("body")).collect(Collectors.toList()));
|
||||||
|
|
||||||
clothes.add(designItemClothesDetailVO);
|
clothes.add(designItemClothesDetailVO);
|
||||||
@@ -1201,6 +1315,7 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
|||||||
designItemDetailPrint.setPosition(print.getLocation().toString());
|
designItemDetailPrint.setPosition(print.getLocation().toString());
|
||||||
designItemDetailPrint.setAngle(print.getAngle());
|
designItemDetailPrint.setAngle(print.getAngle());
|
||||||
designItemDetailPrint.setPriority(priority);
|
designItemDetailPrint.setPriority(priority);
|
||||||
|
designItemDetailPrint.setObject(print.getObject());
|
||||||
|
|
||||||
designItemDetailPrints.add(designItemDetailPrint);
|
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
|
// 添加print到library
|
||||||
designSingleItem.getPrintObject().getPrints().forEach(print -> {
|
designSingleItem.getPrintObject().getPrints().forEach(print -> {
|
||||||
if (!StringUtil.isNullOrEmpty(print.getDesignType()) && print.getDesignType().equals("Collection")) {
|
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.JSONException;
|
||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
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.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
@@ -45,6 +46,7 @@ import java.math.BigDecimal;
|
|||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
@@ -100,8 +102,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
private final UserLikeGroupService userLikeGroupService;
|
private final UserLikeGroupService userLikeGroupService;
|
||||||
private final UserLikeService userLikeService;
|
private final UserLikeService userLikeService;
|
||||||
private final UserBehaviorMapper userBehaviorMapper;
|
private final UserBehaviorMapper userBehaviorMapper;
|
||||||
private final UserPreferenceLogMapper userPreferenceLogMapper;
|
private final UserPreferenceMapper userPreferenceMapper;
|
||||||
private final WorkspaceService workspaceService;
|
private final WorkspaceService workspaceService;
|
||||||
|
private final WorkspaceRelStyleMapper workspaceRelStyleMapper;
|
||||||
|
|
||||||
@Value("${minio.endpoint}")
|
@Value("${minio.endpoint}")
|
||||||
private String endpoint;
|
private String endpoint;
|
||||||
@@ -713,7 +716,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
(existing, replacement) -> replacement));
|
(existing, replacement) -> replacement));
|
||||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||||
log.info("all typeLayers Map:{}", typeAndUndividedLayer);
|
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()) {
|
for (DesignPythonItem detail : item.getItems()) {
|
||||||
if (null == detail) {
|
if (null == detail) {
|
||||||
continue;
|
continue;
|
||||||
@@ -753,7 +756,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
print.setPosition("[0.0,0.0]");
|
print.setPosition("[0.0,0.0]");
|
||||||
// print.setScale(1d);
|
// print.setScale(1d);
|
||||||
// todo mark 将print默认scale置为0.3
|
// 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.setAngle(0.0);
|
||||||
print.setPriority(1);
|
print.setPriority(1);
|
||||||
QueryWrapper<CollectionElement> getPrintboardLevel2TypeQw = new QueryWrapper<>();
|
QueryWrapper<CollectionElement> getPrintboardLevel2TypeQw = new QueryWrapper<>();
|
||||||
@@ -761,7 +764,20 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
getPrintboardLevel2TypeQw.lambda().orderByDesc(CollectionElement::getCreateDate);
|
getPrintboardLevel2TypeQw.lambda().orderByDesc(CollectionElement::getCreateDate);
|
||||||
getPrintboardLevel2TypeQw.last("limit 1");
|
getPrintboardLevel2TypeQw.last("limit 1");
|
||||||
CollectionElement one = collectionElementService.getOne(getPrintboardLevel2TypeQw);
|
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());
|
print.setCreateDate(LocalDateTime.now());
|
||||||
designItemDetailPrintService.save(print);
|
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],
|
Map<String, Integer> typePriority = list.stream().collect(Collectors.toMap(d -> d.getImageCategory().split("_")[0],
|
||||||
d -> Math.abs(d.getPriority()),
|
d -> Math.abs(d.getPriority()),
|
||||||
(existing, replacement) -> replacement));
|
(existing, replacement) -> replacement));
|
||||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
// Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||||
for (DesignPythonItem detail : item.getItems()) {
|
for (DesignPythonItem detail : item.getItems()) {
|
||||||
if (null == detail) {
|
if (null == detail) {
|
||||||
continue;
|
continue;
|
||||||
@@ -862,9 +878,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
designItemDetail.setDesignItemId(designItemId);
|
designItemDetail.setDesignItemId(designItemId);
|
||||||
designItemDetail.setCollectionElementId(detail.getElementId());
|
designItemDetail.setCollectionElementId(detail.getElementId());
|
||||||
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
||||||
if (!detail.getType().equals("Body")) {
|
/*if (!detail.getType().equals("Body")) {
|
||||||
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
||||||
designItemDetail.setPath(detail.getBody_path());
|
designItemDetail.setPath(detail.getBody_path());
|
||||||
@@ -1108,18 +1124,20 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
//修改designItem为like状态
|
//修改designItem为like状态
|
||||||
designItemService.updateLikeStatus(designLikeDTO.getDesignItemId(), (byte) 1);
|
designItemService.updateLikeStatus(designLikeDTO.getDesignItemId(), (byte) 1);
|
||||||
// 记录喜欢的系统sketch
|
// 记录喜欢的系统sketch
|
||||||
addSystemLikeSketch(designItem);
|
addSystemLikeSketch(designItem, designLikeDTO.getProjectId());
|
||||||
// 更新项目更新时间
|
// 更新项目更新时间
|
||||||
projectService.modifyProjectUpdateTime(designLikeDTO.getProjectId());
|
projectService.modifyProjectUpdateTime(designLikeDTO.getProjectId());
|
||||||
return new DesignLikeVO(userLikeSortId, userGroupId, groupDetailId, pictureName, userLike.getId(), userLikeSort.getSort());
|
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();
|
AuthPrincipalVo userHolder = UserContext.getUserHolder();
|
||||||
QueryWrapper<DesignItemDetail> qw = new QueryWrapper<>();
|
QueryWrapper<DesignItemDetail> qw = new QueryWrapper<>();
|
||||||
qw.lambda().eq(DesignItemDetail::getDesignItemId, designItem.getId());
|
qw.lambda().eq(DesignItemDetail::getDesignItemId, designItem.getId());
|
||||||
qw.lambda().ne(DesignItemDetail::getType, "Body");
|
qw.lambda().ne(DesignItemDetail::getType, "Body");
|
||||||
List<DesignItemDetail> designItemDetails = designItemDetailMapper.selectList(qw);
|
List<DesignItemDetail> designItemDetails = designItemDetailMapper.selectList(qw);
|
||||||
|
List<WorkspaceRelStyle> workspaceRelStyles = workspaceRelStyleMapper.selectByProjectId(projectId);
|
||||||
|
|
||||||
for (DesignItemDetail designItemDetail : designItemDetails) {
|
for (DesignItemDetail designItemDetail : designItemDetails) {
|
||||||
if (designItemDetail.getPath().startsWith("aida-sys-image")) {
|
if (designItemDetail.getPath().startsWith("aida-sys-image")) {
|
||||||
String[] split = designItemDetail.getPath().split("/");
|
String[] split = designItemDetail.getPath().split("/");
|
||||||
@@ -1132,16 +1150,34 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
userBehavior.setCreateTime(LocalDateTime.now());
|
userBehavior.setCreateTime(LocalDateTime.now());
|
||||||
userBehaviorMapper.insert(userBehavior);
|
userBehaviorMapper.insert(userBehavior);
|
||||||
|
|
||||||
UserPreferenceLogTest userPreferenceLogTest = new UserPreferenceLogTest();
|
UserPreference userPreference = new UserPreference();
|
||||||
userPreferenceLogTest.setPath(designItemDetail.getPath());
|
userPreference.setPath(designItemDetail.getPath());
|
||||||
userPreferenceLogTest.setAccountId(userHolder.getId());
|
SysFile sysFile = sysFileService.getOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUrl, designItemDetail.getPath()));
|
||||||
// userPreferenceLogTest.setUserLikeGroupId(userLike.getUserLikeGroupId());
|
userPreference.setAccountId(userHolder.getId());
|
||||||
userPreferenceLogTest.setDataTime(designItemDetail.getCreateDate().toInstant()
|
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())
|
.atZone(ZoneId.systemDefault())
|
||||||
.toLocalDateTime());
|
.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) {
|
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.setScope(o.getPath().startsWith("aida-sys-image") ? "sys" : "user");
|
||||||
d.setLevel1Type(converTypeToLevel1(o.getType()));
|
d.setLevel1Type(converTypeToLevel1(o.getType()));
|
||||||
d.setGradient(JSONObject.parseObject(o.getGradientString(), Gradient.class));
|
d.setGradient(JSONObject.parseObject(o.getGradientString(), Gradient.class));
|
||||||
|
/*// 取消存储/返回UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||||
if (!StringUtil.isNullOrEmpty(o.getUndividedLayer())) {
|
if (!StringUtil.isNullOrEmpty(o.getUndividedLayer())) {
|
||||||
d.setUndividedLayer(minioUtil.getPreSignedUrl(o.getUndividedLayer(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
d.setUndividedLayer(minioUtil.getPreSignedUrl(o.getUndividedLayer(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
||||||
}
|
}
|
||||||
if (!StringUtil.isNullOrEmpty(o.getUndividedLayerWithSinglePrint())) {
|
if (!StringUtil.isNullOrEmpty(o.getUndividedLayerWithSinglePrint())) {
|
||||||
d.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(o.getUndividedLayerWithSinglePrint(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
d.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(o.getUndividedLayerWithSinglePrint(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
||||||
}
|
}*/
|
||||||
// 根据designItemDetailId获取印花
|
// 根据designItemDetailId获取印花
|
||||||
List<DesignItemDetailPrint> prints = designItemDetailPrintService.getByDesignItemDetailId(o.getId(), "print");
|
List<DesignItemDetailPrint> prints = designItemDetailPrintService.getByDesignItemDetailId(o.getId(), "print");
|
||||||
prints.removeIf(print -> !minioUtil.doesObjectExist(print.getPath()));
|
prints.removeIf(print -> !minioUtil.doesObjectExist(print.getPath()));
|
||||||
@@ -1563,6 +1600,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
designSinglePrint.setScale(scales);
|
designSinglePrint.setScale(scales);
|
||||||
|
designSinglePrint.setObject(detailPrint.getObject());
|
||||||
prints.add(designSinglePrint);
|
prints.add(designSinglePrint);
|
||||||
} else {
|
} else {
|
||||||
// 多个印花
|
// 多个印花
|
||||||
@@ -1583,7 +1621,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
scales = Arrays.asList(scale, scale);
|
scales = Arrays.asList(scale, scale);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
prints.add(new DesignSinglePrint(
|
DesignSinglePrint designSinglePrint = new DesignSinglePrint(
|
||||||
print.getLevel2Type(),
|
print.getLevel2Type(),
|
||||||
minioUtil.getPreSignedUrl(print.getPath(), 24 * 60),
|
minioUtil.getPreSignedUrl(print.getPath(), 24 * 60),
|
||||||
print.getPath(),
|
print.getPath(),
|
||||||
@@ -1591,7 +1629,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
scales,
|
scales,
|
||||||
print.getAngle(),
|
print.getAngle(),
|
||||||
print.getPriority(),
|
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)
|
.lt("create_date", endTime)
|
||||||
.select("count(id) as count");
|
.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()) {
|
if (!Objects.isNull(accountIds) && !accountIds.isEmpty()) {
|
||||||
queryWrapper.in("account_id", accountIds);
|
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);
|
List<Map<String, Object>> result = baseMapper.selectMaps(queryWrapper);
|
||||||
if (result != null && !result.isEmpty()) {
|
if (result != null && !result.isEmpty()) {
|
||||||
Object countObj = result.get(0).get("count");
|
Object countObj = result.get(0).get("count");
|
||||||
return (Long) countObj;
|
return countObj != null ? ((Number) countObj).longValue() : 0L;
|
||||||
} else {
|
} else {
|
||||||
return 0L;
|
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],
|
Map<String, Integer> typePriority = list.stream().collect(Collectors.toMap(d -> d.getImageCategory().split("_")[0],
|
||||||
d -> Math.abs(d.getPriority()),
|
d -> Math.abs(d.getPriority()),
|
||||||
(existing, replacement) -> replacement));
|
(existing, replacement) -> replacement));
|
||||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
// Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||||
for (DesignPythonItem detail : item.getItems()) {
|
for (DesignPythonItem detail : item.getItems()) {
|
||||||
if (null == detail) {
|
if (null == detail) {
|
||||||
continue;
|
continue;
|
||||||
@@ -2530,9 +2588,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
|||||||
designItemDetail.setDesignItemId(designItemId);
|
designItemDetail.setDesignItemId(designItemId);
|
||||||
designItemDetail.setCollectionElementId(detail.getElementId());
|
designItemDetail.setCollectionElementId(detail.getElementId());
|
||||||
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
||||||
if (!detail.getType().equals("Body")) {
|
/*if (!detail.getType().equals("Body")) {
|
||||||
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
||||||
}
|
}*/
|
||||||
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
||||||
designItemDetail.setPath(detail.getBody_path());
|
designItemDetail.setPath(detail.getBody_path());
|
||||||
//BODY不关联businessId
|
//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.DateUtil;
|
||||||
import com.ai.da.common.utils.MailUtil;
|
import com.ai.da.common.utils.MailUtil;
|
||||||
import com.ai.da.common.utils.RedisUtil;
|
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.AccountMapper;
|
||||||
import com.ai.da.mapper.primary.EmailLogMapper;
|
import com.ai.da.mapper.primary.EmailLogMapper;
|
||||||
import com.ai.da.mapper.primary.EmailTemplateMapper;
|
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) {
|
public boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||||
try {
|
try {
|
||||||
String merchantEmail = "kimwong@code-create.com.hk";
|
List<String> merchantReceiver = buildMerchantReceiverList();
|
||||||
String developer = "xupei3360@163.com";
|
|
||||||
List<String> merchantReceiver = Arrays.asList(/*merchantEmail,*/ developer);
|
|
||||||
|
|
||||||
String merchantSubject = null;
|
String merchantSubject = null;
|
||||||
String merchantTemplate = null;
|
String merchantTemplate = null;
|
||||||
@@ -723,15 +722,13 @@ public class EmailServiceImpl implements EmailService {
|
|||||||
|
|
||||||
private final static String CREDITS_PURCHASE_MERCHANT = "133275_AiDA 积分购买通知-merchant.html";
|
private final static String CREDITS_PURCHASE_MERCHANT = "133275_AiDA 积分购买通知-merchant.html";
|
||||||
public void creditsPurchaseReminder(String username, String quantity, String amount) {
|
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 jsonObject = new JSONObject();
|
||||||
// 设置试用订单相关数据
|
// 设置试用订单相关数据
|
||||||
jsonObject.put("username", username);
|
jsonObject.put("username", username);
|
||||||
jsonObject.put("quantity", quantity);
|
jsonObject.put("quantity", quantity);
|
||||||
jsonObject.put("totalFee", amount);
|
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";
|
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);
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import okhttp3.*;
|
import okhttp3.*;
|
||||||
import org.apache.commons.io.IOUtils;
|
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.HttpHost;
|
||||||
import org.apache.http.client.config.RequestConfig;
|
import org.apache.http.client.config.RequestConfig;
|
||||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.dao.DuplicateKeyException;
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
import javax.imageio.ImageIO;
|
import javax.imageio.ImageIO;
|
||||||
import java.awt.image.BufferedImage;
|
import java.awt.image.BufferedImage;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
@@ -194,10 +198,13 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
generate.setText(text);
|
generate.setText(text);
|
||||||
Long elementId = generateThroughImageTextDTO.getCollectionElementId();
|
Long elementId = generateThroughImageTextDTO.getCollectionElementId();
|
||||||
// validateGeneraType(generate, text, elementId);
|
// validateGeneraType(generate, text, elementId);
|
||||||
if (!StringUtil.isNullOrEmpty(text)) {
|
if (!(generateThroughImageTextDTO.getLevel1Type().equals(MOOD_BOARD.getRealName())&&generateThroughImageTextDTO.getModelName().equals("high"))){
|
||||||
text = modifyPrompt(text, generate, generateThroughImageTextDTO.getLevel1Type(), generateThroughImageTextDTO.getAgeGroup());
|
if (!StringUtil.isNullOrEmpty(text)) {
|
||||||
|
text = modifyPrompt(text, generate, generateThroughImageTextDTO.getLevel1Type(), generateThroughImageTextDTO.getAgeGroup());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// todo 这一步现在还是有必要的吗?
|
// todo 这一步现在还是有必要的吗?
|
||||||
// 2.1 sketch或print在t_collection_element表/t_library表中的信息是否需要更新 如 level2Type
|
// 2.1 sketch或print在t_collection_element表/t_library表中的信息是否需要更新 如 level2Type
|
||||||
CollectionElement collectionElement = collectionElementService.editLevel2Type(elementId, generateThroughImageTextDTO.getLevel2Type(), generateThroughImageTextDTO.getDesignType());
|
CollectionElement collectionElement = collectionElementService.editLevel2Type(elementId, generateThroughImageTextDTO.getLevel2Type(), generateThroughImageTextDTO.getDesignType());
|
||||||
@@ -218,6 +225,8 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
version = "fast";
|
version = "fast";
|
||||||
params.put("version", "fast");
|
params.put("version", "fast");
|
||||||
}
|
}
|
||||||
|
// 4、将请求信息落库,将本次generate的请求信息添加到t_generate表中
|
||||||
|
saveGenerateImmediately(generate);
|
||||||
// 3.1 确定不同类型的印花分别调哪个接口
|
// 3.1 确定不同类型的印花分别调哪个接口
|
||||||
if (generateThroughImageTextDTO.getLevel1Type().equals(PRINT_BOARD.getRealName())) {
|
if (generateThroughImageTextDTO.getLevel1Type().equals(PRINT_BOARD.getRealName())) {
|
||||||
switch (generateThroughImageTextDTO.getLevel2Type()) {
|
switch (generateThroughImageTextDTO.getLevel2Type()) {
|
||||||
@@ -243,15 +252,28 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
GenerateToPythonDTO generateToPythonDTO = new GenerateToPythonDTO(generateThroughImageTextDTO.getUniqueId(), text, Objects.isNull(collectionElement) ? "" : collectionElement.getUrl(),
|
if (Objects.equals(version, "fast")) {
|
||||||
mode, category, generateThroughImageTextDTO.getGender(), version);
|
GenerateToPythonDTO generateToPythonDTO = new GenerateToPythonDTO(generateThroughImageTextDTO.getUniqueId(), text, Objects.isNull(collectionElement) ? "" : collectionElement.getUrl(),
|
||||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
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
|
// 5、将本次请求存入redis
|
||||||
String key = generateResultKey + ":" + generateThroughImageTextDTO.getUniqueId();
|
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) {
|
public GenerateModeEnum getMode(GenerateThroughImageTextDTO generateThroughImageTextDTO) {
|
||||||
if (!StringUtil.isNullOrEmpty(generateThroughImageTextDTO.getText())) {
|
if (!StringUtil.isNullOrEmpty(generateThroughImageTextDTO.getText())) {
|
||||||
if (Objects.nonNull(generateThroughImageTextDTO.getCollectionElementId())) {
|
if (Objects.nonNull(generateThroughImageTextDTO.getCollectionElementId())) {
|
||||||
@@ -284,11 +340,16 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void processGenerateResult(String taskId, String url, String category) {
|
public void processGenerateResult(String taskId, String url, String category) {
|
||||||
|
log.info("============ProcessGenerateResult listening==========");
|
||||||
|
log.debug("taskId: " + taskId);
|
||||||
|
String status = null;
|
||||||
// 1、处理模型返回的数据
|
// 1、处理模型返回的数据
|
||||||
GenerateDetail generateDetail = new GenerateDetail();
|
GenerateDetail generateDetail = new GenerateDetail();
|
||||||
GenerateCollectionItemVO generateCollectionItemVO = new GenerateCollectionItemVO();
|
GenerateCollectionItemVO generateCollectionItemVO = new GenerateCollectionItemVO();
|
||||||
Generate generate;
|
Generate generate;
|
||||||
try {
|
try {
|
||||||
|
// 等待 HTTP 线程写入完成后再查库
|
||||||
|
waitForSaveLock(taskId);
|
||||||
generate = selectByUniqueId(taskId);
|
generate = selectByUniqueId(taskId);
|
||||||
} catch (MybatisPlusException e) {
|
} catch (MybatisPlusException e) {
|
||||||
log.error(e.getMessage());
|
log.error(e.getMessage());
|
||||||
@@ -311,14 +372,15 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
generateDetail.setUrl(url);
|
generateDetail.setUrl(url);
|
||||||
generateDetail.setGenerateId(generate.getId());
|
generateDetail.setGenerateId(generate.getId());
|
||||||
generateDetail.setCreateDate(LocalDateTime.now());
|
generateDetail.setCreateDate(LocalDateTime.now());
|
||||||
generateDetail.setMd5(md5);
|
generateDetail.setMd5("");
|
||||||
// 将相应的url保存到数据库
|
// 将相应的url保存到数据库
|
||||||
generateDetailMapper.insert(generateDetail);
|
generateDetailMapper.insert(generateDetail);
|
||||||
|
log.debug("generateDetail: " + generateDetail.toString());
|
||||||
|
|
||||||
// String uuid = taskId.substring(0, taskId.substring(0, taskId.lastIndexOf("-")).lastIndexOf("-"));
|
// String uuid = taskId.substring(0, taskId.substring(0, taskId.lastIndexOf("-")).lastIndexOf("-"));
|
||||||
String key = generateResultKey + ":" + taskId;
|
String key = generateResultKey + ":" + taskId;
|
||||||
String imageName = url.substring(url.lastIndexOf("/") + 1);
|
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)) {
|
if (StringUtil.isNullOrEmpty(category)) {
|
||||||
Generate generateRecord = selectByUniqueId(taskId);
|
Generate generateRecord = selectByUniqueId(taskId);
|
||||||
category = generateRecord.getLevel2Type();
|
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);
|
GenerateResultVO generateResultVO = new GenerateResultVO(taskId, generateDetail.getId(), url, status, category);
|
||||||
// 更新redis
|
// 更新redis
|
||||||
redisUtil.addToString(key, new Gson().toJson(generateResultVO), CommonConstant.GENERATE_RESULT_EXPIRE_TIME);
|
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();
|
long requestEndTime = System.currentTimeMillis();
|
||||||
log.info("HTTP请求完成 - 响应状态: {}, 耗时: {}ms, taskId: {}",
|
log.info("HTTP请求完成 - 响应状态: {}, 耗时: {}ms, taskId: {}",
|
||||||
response.code(), (requestEndTime - requestStartTime), taskId);
|
response.code(), (requestEndTime - requestStartTime), taskId);
|
||||||
|
String result = response.body().string();
|
||||||
if (!response.isSuccessful()) {
|
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) {
|
if (attempt < maxRetries) {
|
||||||
Thread.sleep(retryDelay * attempt); // 递增延迟
|
Thread.sleep(retryDelay * attempt); // 递增延迟
|
||||||
continue;
|
continue;
|
||||||
@@ -795,7 +860,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String result = response.body().string();
|
|
||||||
// log.info("Google 响应结果:{}", result);
|
// log.info("Google 响应结果:{}", result);
|
||||||
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(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();
|
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);
|
// log.info("Google 响应结果:{}", result);
|
||||||
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(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
|
* @param modelName advanced high normal
|
||||||
*/
|
*/
|
||||||
private HashMap<String, String> chooseModelAndPrompt(GenerateThroughImageTextDTO generateDTO, String modelName) {
|
private HashMap<String, String> chooseModelAndPrompt(GenerateThroughImageTextDTO generateDTO, String modelName) {
|
||||||
|
if (StringUtil.isNullOrEmpty(modelName)) {
|
||||||
|
throw new BusinessException("system error");
|
||||||
|
}
|
||||||
HashMap<String, String> modelAndPromptMap = new HashMap<>();
|
HashMap<String, String> modelAndPromptMap = new HashMap<>();
|
||||||
boolean isUseImage;
|
boolean isUseImage;
|
||||||
if (Objects.isNull(generateDTO.getCollectionElementId())
|
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 style = generateDTO.getText().substring(0, firstCommaIndex).trim();
|
||||||
|
|
||||||
String prompt = generateDTO.getText().substring(firstCommaIndex + 1).trim();
|
String prompt = generateDTO.getText().substring(firstCommaIndex + 1).trim();
|
||||||
prompt = getPrintboardPrompt(style, prompt);
|
prompt = getPrintboardPrompt(style, prompt, modelName, isUseImage);
|
||||||
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||||
|
|
||||||
|
|
||||||
@@ -1239,7 +1313,14 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if (ModelConstants.MOODBOARD.equals(generateDTO.getLevel1Type())) {
|
} 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);
|
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||||
|
|
||||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
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);
|
modelAndPromptMap.put(ModelConstants.USE_MODEL, ModelConstants.LOCAL_MODEL);
|
||||||
}
|
}
|
||||||
} else if (ModelConstants.SKETCHBOARD.equals(generateDTO.getLevel1Type())) {
|
} 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.";
|
" 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);
|
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||||
if (isUseImage) {
|
if (isUseImage) {
|
||||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||||
@@ -1449,6 +1553,13 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
if (imagePath != null) {
|
if (imagePath != null) {
|
||||||
requestBuilder.image(finalImagePath1);
|
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(
|
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)) {
|
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" +
|
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||||
"2.Core Theme: " + prompt + "\n" +
|
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.";
|
||||||
"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.";
|
} 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)) {
|
} 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" +
|
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||||
"2.Core Theme: " + prompt + "\n" +
|
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. ";
|
||||||
"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.";
|
} 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)) {
|
} 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" +
|
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||||
"2.Core Theme: " + prompt + "\n" +
|
systemPrompt = "Tileable seamless pattern, even color field background, photorealistic style pattern, high-quality digital print, zero perspective depth, harmonious visual balance, consistent color tone. ";
|
||||||
"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.";
|
} 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;
|
return prompt;
|
||||||
}
|
}
|
||||||
@@ -1970,7 +2106,9 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
public Generate selectByUniqueId(String uniqueId) {
|
public Generate selectByUniqueId(String uniqueId) {
|
||||||
QueryWrapper<Generate> qw = new QueryWrapper<>();
|
QueryWrapper<Generate> qw = new QueryWrapper<>();
|
||||||
qw.eq("unique_id", uniqueId);
|
qw.eq("unique_id", uniqueId);
|
||||||
|
log.debug("selectByUniqueId: " + uniqueId);
|
||||||
|
Generate one = getOne(qw);
|
||||||
|
log.debug("Generate: " + one);
|
||||||
return getOne(qw);
|
return getOne(qw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3796,11 +3934,48 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
}
|
}
|
||||||
|
|
||||||
public byte[] downloadVideoOrImage(String url) {
|
public byte[] downloadVideoOrImage(String url) {
|
||||||
try (CloseableHttpClient client = HttpClients.createDefault();
|
int maxRetries = 3;
|
||||||
InputStream in = client.execute(new HttpGet(url)).getEntity().getContent()) {
|
int retryDelayMs = 1000;
|
||||||
return IOUtils.toByteArray(in);
|
IOException lastException = null;
|
||||||
} catch (IOException e) {
|
|
||||||
throw new RuntimeException(e);
|
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) {
|
switch (statusEnum) {
|
||||||
case TASK_NOT_FOUND:
|
case TASK_NOT_FOUND:
|
||||||
// 审核没过
|
// 审核没过
|
||||||
case REQUEST_MODERATED:
|
case REQUEST_MODERATED:
|
||||||
// 审核没过
|
// 审核没过
|
||||||
case CONTENT_MODERATED:
|
case CONTENT_MODERATED:
|
||||||
// 出错
|
// 出错
|
||||||
case ERROR:
|
case ERROR:
|
||||||
return "Fail";
|
return "Fail";
|
||||||
case PENDING_F:
|
case PENDING_F:
|
||||||
@@ -4264,7 +4439,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
|||||||
MotionModeEnum motionModeEnum = MotionModeEnum.of(poseTransformDTO.getMode());
|
MotionModeEnum motionModeEnum = MotionModeEnum.of(poseTransformDTO.getMode());
|
||||||
switch (motionModeEnum) {
|
switch (motionModeEnum) {
|
||||||
case POSE_TO_VIDEO:
|
case POSE_TO_VIDEO:
|
||||||
params.put("pose_id", poseTransformDTO.getPoseId());
|
params.put("pose_id", poseTransformDTO.getPoseId().toString());
|
||||||
params.put("image_url", poseTransformDTO.getProductImage());
|
params.put("image_url", poseTransformDTO.getProductImage());
|
||||||
break;
|
break;
|
||||||
case PROMPT_TO_VIDEO:
|
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.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
@@ -53,7 +54,11 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
|||||||
private final PythonTAllInfoService pythonTAllInfoService;
|
private final PythonTAllInfoService pythonTAllInfoService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public LibraryModelPointVO saveOrEditTemplatePoint(LibraryModelPointDTO libraryModelPointDTO) {
|
public LibraryModelPointVO saveOrEditTemplatePoint(LibraryModelPointDTO libraryModelPointDTO) {
|
||||||
|
// 参数校验
|
||||||
|
validateInputParams(libraryModelPointDTO);
|
||||||
|
|
||||||
LibraryModelPointVO libraryModelPointVO = CopyUtil.copyObject(libraryModelPointDTO, LibraryModelPointVO.class);
|
LibraryModelPointVO libraryModelPointVO = CopyUtil.copyObject(libraryModelPointDTO, LibraryModelPointVO.class);
|
||||||
|
|
||||||
// 不管是保存还是另存为,都需要传模特的libraryId
|
// 不管是保存还是另存为,都需要传模特的libraryId
|
||||||
@@ -71,7 +76,8 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
|||||||
// 更新模特图片
|
// 更新模特图片
|
||||||
if (flag) {
|
if (flag) {
|
||||||
libModel.setUrl(url);
|
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);
|
List<Integer> imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(url);
|
||||||
libModel.setWidth(imagesWidthAndHeight.get(0));
|
libModel.setWidth(imagesWidthAndHeight.get(0));
|
||||||
libModel.setHigh(imagesWidthAndHeight.get(1));
|
libModel.setHigh(imagesWidthAndHeight.get(1));
|
||||||
@@ -104,25 +110,10 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
|||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 不覆盖,即另存为
|
// 不覆盖,即另存为
|
||||||
// 新增模特library信息
|
Library saveAsModel = createNewLibraryCopy(libModel, libraryModelPointDTO);
|
||||||
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());
|
|
||||||
|
|
||||||
// 新增模特点位信息
|
// 新增模特点位信息
|
||||||
|
libraryModelPointDTO.setLibraryId(saveAsModel.getId()); // 更新libraryId为新创建的模型ID
|
||||||
LibraryModelPoint libraryModelPoint = resolvePoint(libraryModelPointDTO);
|
LibraryModelPoint libraryModelPoint = resolvePoint(libraryModelPointDTO);
|
||||||
libraryModelPoint.setModelType("Library");
|
libraryModelPoint.setModelType("Library");
|
||||||
libraryModelPoint.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
libraryModelPoint.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
||||||
@@ -130,22 +121,50 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
|||||||
libraryModelPointVO.setTemplateId(libraryModelPoint.getId());
|
libraryModelPointVO.setTemplateId(libraryModelPoint.getId());
|
||||||
libraryModelPointVO.setRelationId(libraryModelPoint.getRelationId());
|
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;
|
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
|
@Override
|
||||||
public LibraryModelPointVO saveOrEditTemplatePointOld(LibraryModelPointDTO libraryModelPointDTO) {
|
public LibraryModelPointVO saveOrEditTemplatePointOld(LibraryModelPointDTO libraryModelPointDTO) {
|
||||||
// Library library = libraryService.getById(libraryModelPointDTO.getLibraryId());
|
// Library library = libraryService.getById(libraryModelPointDTO.getLibraryId());
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
throw new BusinessException("type.cannot.be.empty");
|
throw new BusinessException("type.cannot.be.empty");
|
||||||
}
|
}
|
||||||
Long accountId = UserContext.getUserHolder().getId();
|
Long accountId = UserContext.getUserHolder().getId();
|
||||||
|
Account account = accountService.getById(accountId);
|
||||||
// 查动态
|
// 查动态
|
||||||
if (!StringUtils.isNullOrEmpty(getNotificationDTO.getType()) && getNotificationDTO.getType().equals("newPosted")) {
|
if (!StringUtils.isNullOrEmpty(getNotificationDTO.getType()) && getNotificationDTO.getType().equals("newPosted")) {
|
||||||
return getNewPosted(accountId, getNotificationDTO.getPage(), getNotificationDTO.getSize());
|
return getNewPosted(accountId, getNotificationDTO.getPage(), getNotificationDTO.getSize());
|
||||||
@@ -92,6 +93,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
|
|
||||||
if (getNotificationDTO.getType().equals("system")) {
|
if (getNotificationDTO.getType().equals("system")) {
|
||||||
queryWrapper.lambda().eq(Notification::getType, "system")
|
queryWrapper.lambda().eq(Notification::getType, "system")
|
||||||
|
.gt(Notification::getCreateTime, account.getCreateDate())
|
||||||
.and(wrapper -> wrapper
|
.and(wrapper -> wrapper
|
||||||
.isNull(Notification::getReceiverId)
|
.isNull(Notification::getReceiverId)
|
||||||
.or()
|
.or()
|
||||||
@@ -103,7 +105,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
|
|
||||||
Page<Notification> notificationPage = baseMapper.selectPage(new Page<>(getNotificationDTO.getPage(), getNotificationDTO.getSize()), queryWrapper);
|
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 -> {
|
IPage<NotificationVO> convert = notificationPage.convert(o -> {
|
||||||
NotificationVO notificationVO = CopyUtil.copyObject(o, NotificationVO.class);
|
NotificationVO notificationVO = CopyUtil.copyObject(o, NotificationVO.class);
|
||||||
Account senderAccount = accountService.getById(notificationVO.getSenderId());
|
Account senderAccount = accountService.getById(notificationVO.getSenderId());
|
||||||
@@ -192,6 +194,8 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
if (!type.equals("system")) {
|
if (!type.equals("system")) {
|
||||||
// 个人未读消息
|
// 个人未读消息
|
||||||
count = getUnreadCountByType(type, receiverId);
|
count = getUnreadCountByType(type, receiverId);
|
||||||
|
} else if (Objects.isNull(receiverId)) {
|
||||||
|
count = 1L;
|
||||||
} else {
|
} else {
|
||||||
// 系统未读消息
|
// 系统未读消息
|
||||||
count = getUnreadSystemNotification(receiverId);
|
count = getUnreadSystemNotification(receiverId);
|
||||||
@@ -247,6 +251,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
|
|
||||||
private Long getUnreadSystemNotification(Long receiverId) {
|
private Long getUnreadSystemNotification(Long receiverId) {
|
||||||
// Long accountId = UserContext.getUserHolder().getId();
|
// Long accountId = UserContext.getUserHolder().getId();
|
||||||
|
Account account = accountService.getById(receiverId);
|
||||||
// 计算总的系统通知数量
|
// 计算总的系统通知数量
|
||||||
QueryWrapper<Notification> queryWrapper = new QueryWrapper<>();
|
QueryWrapper<Notification> queryWrapper = new QueryWrapper<>();
|
||||||
queryWrapper.lambda().eq(Notification::getType, "system")
|
queryWrapper.lambda().eq(Notification::getType, "system")
|
||||||
@@ -255,6 +260,9 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
.or()
|
.or()
|
||||||
.eq(Notification::getReceiverId, receiverId)
|
.eq(Notification::getReceiverId, receiverId)
|
||||||
);
|
);
|
||||||
|
if (Objects.nonNull(account)) {
|
||||||
|
queryWrapper.lambda().gt(Notification::getCreateTime, account.getCreateDate());
|
||||||
|
}
|
||||||
Long totalSysCount = baseMapper.selectCount(queryWrapper);
|
Long totalSysCount = baseMapper.selectCount(queryWrapper);
|
||||||
|
|
||||||
// 计算单个用户读了多少条系统数据
|
// 计算单个用户读了多少条系统数据
|
||||||
@@ -302,6 +310,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
// 一键已读
|
// 一键已读
|
||||||
public void setReadAll(String type) {
|
public void setReadAll(String type) {
|
||||||
Long accountId = UserContext.getUserHolder().getId();
|
Long accountId = UserContext.getUserHolder().getId();
|
||||||
|
Account account = accountService.getById(accountId);
|
||||||
// 指定某个用户的某种类型的数据,将未读数据全部已读
|
// 指定某个用户的某种类型的数据,将未读数据全部已读
|
||||||
if (!type.equals("system")) {
|
if (!type.equals("system")) {
|
||||||
// 个人消息已读
|
// 个人消息已读
|
||||||
@@ -309,7 +318,7 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
|||||||
} else {
|
} else {
|
||||||
// 系统消息已读
|
// 系统消息已读
|
||||||
// 1、先确定当前用户未读的系统消息有哪些
|
// 1、先确定当前用户未读的系统消息有哪些
|
||||||
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId);
|
List<Long> unreadSysNotificationIds = baseMapper.getUnreadSysNotification(accountId, account.getCreateDate());
|
||||||
// 2、将未读的设为已读
|
// 2、将未读的设为已读
|
||||||
if (!unreadSysNotificationIds.isEmpty()) setReadStatusSystem(unreadSysNotificationIds);
|
if (!unreadSysNotificationIds.isEmpty()) setReadStatusSystem(unreadSysNotificationIds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import java.time.Duration;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -90,7 +91,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
public OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
public OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
||||||
HttpServletRequest request, byte autoRenewal) {
|
HttpServletRequest request) {
|
||||||
|
|
||||||
//获取商品信息
|
//获取商品信息
|
||||||
// Product product = productMapper.selectById(amount);
|
// Product product = productMapper.selectById(amount);
|
||||||
@@ -276,10 +277,11 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
|||||||
|
|
||||||
public void updateTotalFeeByOrderNo(String orderNo) {
|
public void updateTotalFeeByOrderNo(String orderNo) {
|
||||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
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);
|
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qw);
|
||||||
Float sum = paymentInfos.stream()
|
Float sum = paymentInfos.stream()
|
||||||
.map(PaymentInfo::getPayerTotal)
|
.map(PaymentInfo::getPayerTotal)
|
||||||
|
.filter(Objects::nonNull)
|
||||||
.reduce(0f, Float::sum);
|
.reduce(0f, Float::sum);
|
||||||
|
|
||||||
baseMapper.update(
|
baseMapper.update(
|
||||||
|
|||||||
@@ -258,6 +258,11 @@ public class PanToneServiceImpl extends ServiceImpl<PanToneMapper, PanTone> impl
|
|||||||
d.setH(getRgbByHsvBatchDTO.getH());
|
d.setH(getRgbByHsvBatchDTO.getH());
|
||||||
d.setS(getRgbByHsvBatchDTO.getS());
|
d.setS(getRgbByHsvBatchDTO.getS());
|
||||||
d.setV(getRgbByHsvBatchDTO.getV());
|
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(
|
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.context.UserContext;
|
||||||
import com.ai.da.common.enums.PayTypeEnum;
|
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.response.PageBaseResponse;
|
||||||
import com.ai.da.common.utils.SpringUtils;
|
import com.ai.da.common.utils.SpringUtils;
|
||||||
import com.ai.da.mapper.primary.PaymentInfoMapper;
|
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.OrderInfo;
|
||||||
import com.ai.da.mapper.primary.entity.PaymentInfo;
|
import com.ai.da.mapper.primary.entity.PaymentInfo;
|
||||||
import com.ai.da.mapper.primary.entity.ProductCoupons;
|
import com.ai.da.mapper.primary.entity.ProductCoupons;
|
||||||
@@ -20,13 +22,17 @@ import com.google.gson.Gson;
|
|||||||
import com.paypal.orders.Order;
|
import com.paypal.orders.Order;
|
||||||
import com.stripe.Stripe;
|
import com.stripe.Stripe;
|
||||||
import com.stripe.exception.StripeException;
|
import com.stripe.exception.StripeException;
|
||||||
import com.stripe.model.Charge;
|
import com.stripe.model.*;
|
||||||
import com.stripe.model.Invoice;
|
|
||||||
import com.stripe.model.Subscription;
|
|
||||||
import com.stripe.model.checkout.Session;
|
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 io.netty.util.internal.StringUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -49,6 +55,9 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
|||||||
@Resource
|
@Resource
|
||||||
private OrderInfoService orderInfoService;
|
private OrderInfoService orderInfoService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ProductCouponsMapper productCouponsMapper;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录支付日志:微信支付
|
* 记录支付日志:微信支付
|
||||||
* @param plainText
|
* @param plainText
|
||||||
@@ -194,38 +203,199 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
|||||||
baseMapper.insert(paymentInfo);
|
baseMapper.insert(paymentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void createOrUpdatePaymentInfoForStripe(Session session){
|
/**
|
||||||
String orderId = session.getMetadata().get("orderId");
|
* 为 Stripe Checkout Session 创建支付记录
|
||||||
String status = session.getStatus();
|
*
|
||||||
// 获取transactionId,从sessionId更改为invoiceId
|
* 策略:根据 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 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();
|
Long amountTotal = session.getAmountTotal();
|
||||||
// stripe 的支付金额单位是分
|
|
||||||
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||||
|
|
||||||
PaymentInfo paymentInfo = new PaymentInfo();
|
PaymentInfo paymentInfo = new PaymentInfo();
|
||||||
paymentInfo.setOrderNo(orderId);
|
paymentInfo.setOrderNo(orderNo);
|
||||||
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
||||||
paymentInfo.setTransactionId(sessionId);
|
paymentInfo.setTransactionId(invoiceId);
|
||||||
paymentInfo.setTradeState(status);
|
paymentInfo.setTradeState(status);
|
||||||
paymentInfo.setPayerTotal(divide);
|
paymentInfo.setPayerTotal(divide);
|
||||||
Gson gson = new Gson();
|
Gson gson = new Gson();
|
||||||
String json = gson.toJson(session);
|
paymentInfo.setContent(gson.toJson(session));
|
||||||
paymentInfo.setContent(json);
|
paymentInfo.setType(type);
|
||||||
// 获取订单信息
|
paymentInfo.setNotified(0);
|
||||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId);
|
paymentInfo.setPaymentMethod(paymentMethodInfo.getOrDefault("paymentMethod", "N/A"));
|
||||||
if (!Objects.isNull(orderByOrderNo)){
|
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.setCountry(orderByOrderNo.getCountry());
|
||||||
paymentInfo.setCity(orderByOrderNo.getCity());
|
paymentInfo.setCity(orderByOrderNo.getCity());
|
||||||
paymentInfo.setIpAddress(orderByOrderNo.getIpAddress());
|
paymentInfo.setIpAddress(orderByOrderNo.getIpAddress());
|
||||||
}
|
}
|
||||||
|
|
||||||
baseMapper.insert(paymentInfo);
|
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}")
|
@Value("${stripe.private-key}")
|
||||||
private String privateKey;
|
private String privateKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为 Stripe Invoice 创建或更新支付记录
|
||||||
|
*
|
||||||
|
* @param invoice Stripe Invoice
|
||||||
|
* @param paymentMethodInfo 外部传入的支付方式信息(如从 Session 传入),优先使用,为空时内部重新获取
|
||||||
|
* @return PaymentInfo 支付记录
|
||||||
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@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;
|
Stripe.apiKey = privateKey;
|
||||||
StripeService stripeService = SpringUtils.getBean(StripeService.class);
|
StripeService stripeService = SpringUtils.getBean(StripeService.class);
|
||||||
// 获取transactionId,从sessionId更改为invoiceId
|
// 获取transactionId,从sessionId更改为invoiceId
|
||||||
@@ -235,29 +405,61 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
|||||||
qw.eq("transaction_id", invoiceId);
|
qw.eq("transaction_id", invoiceId);
|
||||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||||
String status = invoice.getStatus();
|
String status = invoice.getStatus();
|
||||||
// 判断是否有优惠码
|
// 判断是否有优惠码 续订不会使用优惠码,故取消这部分代码
|
||||||
|
// Stripe SDK 32.0.0: invoice.getDiscount() 已移除,使用 invoice.getDiscountObject().get() 替代
|
||||||
String promotionCode = null;
|
String promotionCode = null;
|
||||||
if (Objects.nonNull(invoice.getDiscount()) && !StringUtil.isNullOrEmpty(invoice.getDiscount().getPromotionCode())){
|
if (!CollectionUtils.isEmpty(discounts)) {
|
||||||
ProductCoupons productCoupon = stripeService.getProductCoupon(null, invoice.getDiscount().getPromotionCode());
|
promotionCode = getPromotionCodeByPromotionCodeId(discounts.getFirst().getPromotionCode());
|
||||||
promotionCode = productCoupon.getPromotionCode();
|
|
||||||
}
|
}
|
||||||
// 判断当前支付是否已经被记录,确保同一个支付不会被重复记录
|
// 判断当前支付是否已经被记录,确保同一个支付不会被重复记录
|
||||||
if (Objects.isNull(paymentInfo)){
|
if (Objects.isNull(paymentInfo)){
|
||||||
String orderNo;
|
String orderNo = null;
|
||||||
|
String billingReason = invoice.getBillingReason();
|
||||||
|
String paymentIntentIdForCharge = null;
|
||||||
try {
|
try {
|
||||||
if (invoice.getBillingReason().equals("manual")){
|
if ("manual".equals(billingReason)){
|
||||||
// 手动创建的发票,针对one-time支付
|
// 手动创建的发票,针对one-time支付
|
||||||
// orderNo = invoice.getLines().getData().get(0).getPrice().getMetadata().get("orderId");
|
// 获取 PaymentIntent 用于后续获取 chargeId 和支付方式
|
||||||
// 当支付失败时,chargeId为空
|
paymentIntentIdForCharge = getPaymentIntentByInvoice(invoice);
|
||||||
String chargeId = invoice.getCharge();
|
if (!StringUtil.isNullOrEmpty(paymentIntentIdForCharge)) {
|
||||||
orderNo = Charge.retrieve(chargeId).getDescription().replace("AiDA - ", "");
|
PaymentIntent paymentIntent = PaymentIntent.retrieve(paymentIntentIdForCharge);
|
||||||
}else {
|
String chargeId = paymentIntent.getLatestCharge();
|
||||||
String subscriptionId = invoice.getSubscription();
|
if (!StringUtil.isNullOrEmpty(chargeId)) {
|
||||||
// 从subscription中获取orderNo
|
Charge charge = Charge.retrieve(chargeId);
|
||||||
orderNo = Subscription.retrieve(subscriptionId).getDescription().replace("AiDA - ", "");
|
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) {
|
} 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;
|
Long amountTotal;
|
||||||
if (status.equals("paid")){
|
if (status.equals("paid")){
|
||||||
@@ -268,11 +470,9 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
|||||||
|
|
||||||
// stripe 的支付金额单位是分,在我们数据库中金额单位为 元
|
// stripe 的支付金额单位是分,在我们数据库中金额单位为 元
|
||||||
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||||
String type = invoice.getBillingReason().equals("subscription_create") ? "new" :
|
String type = invoice.getBillingReason().equals("subscription_create") ? PaymentInfoType.NEW.getType() :
|
||||||
invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason();
|
invoice.getBillingReason().equals("subscription_cycle") ? PaymentInfoType.RENEWAL.getType() : invoice.getBillingReason();
|
||||||
|
|
||||||
// 获取支付方式
|
|
||||||
Map<String, String> paymentMethod = stripeService.getPaymentMethodByInvoiceId(invoiceId);
|
|
||||||
// 获取订单信息
|
// 获取订单信息
|
||||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||||
|
|
||||||
@@ -288,8 +488,8 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
|||||||
paymentInfo.setContent(json);
|
paymentInfo.setContent(json);
|
||||||
paymentInfo.setType(type);
|
paymentInfo.setType(type);
|
||||||
paymentInfo.setNotified(0);
|
paymentInfo.setNotified(0);
|
||||||
paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod"));
|
paymentInfo.setPaymentMethod(paymentMethodInfo.get("paymentMethod"));
|
||||||
paymentInfo.setLast4(paymentMethod.get("last4"));
|
paymentInfo.setLast4(paymentMethodInfo.get("last4"));
|
||||||
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
|
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
|
||||||
paymentInfo.setPromotionCode(promotionCode);
|
paymentInfo.setPromotionCode(promotionCode);
|
||||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||||
@@ -300,87 +500,85 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
|||||||
}
|
}
|
||||||
int row = baseMapper.insertIgnore(paymentInfo);
|
int row = baseMapper.insertIgnore(paymentInfo);
|
||||||
log.info("Payment Info insert affect rows:{}", row);
|
log.info("Payment Info insert affect rows:{}", row);
|
||||||
}else {
|
orderInfoService.updateTotalFeeByOrderNo(orderNo);
|
||||||
|
} else {
|
||||||
paymentInfo.setTradeState(status);
|
paymentInfo.setTradeState(status);
|
||||||
paymentInfo.setPromotionCode(promotionCode);
|
paymentInfo.setPromotionCode(promotionCode);
|
||||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||||
baseMapper.updateById(paymentInfo);
|
baseMapper.updateById(paymentInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
return paymentInfo;
|
return paymentInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge){
|
private String getPaymentIntentByInvoice(Invoice invoice) {
|
||||||
Stripe.apiKey = privateKey;
|
// 从 invoice.getPayments() 获取(适用于已支付完成的 Invoice)
|
||||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
// SDK 32.0.0: invoice.getPayments() 可能为 null,需逐层判空
|
||||||
// todo 首次支付失败,没有invoiceId,所以如果这个order之后成功支付后,会有多条paymentInfo 是否需要优化??
|
try {
|
||||||
qw.eq("transaction_id", charge.getInvoice());
|
InvoicePaymentCollection payments = invoice.getPayments();
|
||||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
if (payments != null) {
|
||||||
Charge.PaymentMethodDetails paymentMethodDetails = charge.getPaymentMethodDetails();
|
List<InvoicePayment> invoicePayments = payments.getData();
|
||||||
String paymentMethod;
|
if (invoicePayments != null && !invoicePayments.isEmpty()) {
|
||||||
String last4 = "N/A";
|
InvoicePayment firstPayment = invoicePayments.getFirst();
|
||||||
switch (paymentMethodDetails.getType()){
|
if (firstPayment != null) {
|
||||||
case "alipay":
|
InvoicePayment.Payment payment = firstPayment.getPayment();
|
||||||
paymentMethod = "Alipay";
|
if (payment != null) {
|
||||||
break;
|
PaymentIntent paymentIntent = payment.getPaymentIntentObject();
|
||||||
case "bancontact":
|
if (paymentIntent != null) {
|
||||||
paymentMethod = "BanContact";
|
return paymentIntent.getId();
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
int row = baseMapper.insertIgnore(paymentInfo);
|
} catch (Exception e) {
|
||||||
log.info("Payment Info insert affect rows:{}", row);
|
log.warn("[getPaymentIntentByInvoice] 获取 PaymentIntent 失败,invoiceId={},error={}", invoice.getId(), e.getMessage());
|
||||||
}else {
|
}
|
||||||
paymentInfo.setTradeState(charge.getStatus());
|
return null;
|
||||||
paymentInfo.setPaymentMethod(paymentMethod);
|
}
|
||||||
paymentInfo.setLast4(last4);
|
|
||||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
private String getPromotionCodeByInvoice(Invoice invoice) throws StripeException {
|
||||||
baseMapper.updateById(paymentInfo);
|
// 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);
|
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<>();
|
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||||
qw.eq("transaction_id", charge.getInvoice());
|
qw.eq("transaction_id", invoiceId);
|
||||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||||
if (Objects.nonNull(paymentInfo)){
|
if (Objects.nonNull(paymentInfo)) {
|
||||||
String status ;
|
if (!paymentInfo.getTradeState().equals(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)){
|
|
||||||
paymentInfo.setTradeState(status);
|
paymentInfo.setTradeState(status);
|
||||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||||
baseMapper.updateById(paymentInfo);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ public class RabbitMQServiceImpl implements RabbitMQService {
|
|||||||
mqPublisher.sendGenerateMessage(message);
|
mqPublisher.sendGenerateMessage(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void publishMessageToGenerateResult(String message) {
|
||||||
|
mqPublisher.sendGenerateResultMessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void publishMessageToSR(String message) {
|
public void publishMessageToSR(String message) {
|
||||||
mqPublisher.sendSRMessage(message);
|
mqPublisher.sendSRMessage(message);
|
||||||
|
|||||||
@@ -1,31 +1,59 @@
|
|||||||
package com.ai.da.service.impl;
|
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.common.utils.OrderNoUtils;
|
||||||
|
import com.ai.da.mapper.primary.AccountMapper;
|
||||||
import com.ai.da.mapper.primary.RefundInfoMapper;
|
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.OrderInfo;
|
||||||
import com.ai.da.mapper.primary.entity.RefundInfo;
|
import com.ai.da.mapper.primary.entity.RefundInfo;
|
||||||
import com.ai.da.service.OrderInfoService;
|
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||||
import com.ai.da.service.RefundInfoService;
|
import com.ai.da.service.*;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
|
import com.stripe.Stripe;
|
||||||
import com.stripe.model.Charge;
|
import com.stripe.model.Charge;
|
||||||
import com.stripe.model.Refund;
|
import com.stripe.model.Refund;
|
||||||
|
import com.stripe.exception.StripeException;
|
||||||
import io.netty.util.internal.StringUtil;
|
import io.netty.util.internal.StringUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
|
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
|
||||||
|
|
||||||
|
@Value("${stripe.private-key}")
|
||||||
|
private String privateKey;
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private OrderInfoService orderInfoService;
|
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){
|
public RefundInfo updateRefundForStripe(Charge charge){
|
||||||
List<RefundInfo> refundInfoList = getByChargeId(charge.getId());
|
String chargeId = charge.getId();
|
||||||
if (!refundInfoList.isEmpty()){
|
List<RefundInfo> refundInfoList = getByChargeId(chargeId);
|
||||||
RefundInfo refundInfo = refundInfoList.get(refundInfoList.size() - 1);
|
if (refundInfoList.isEmpty()){
|
||||||
if (StringUtil.isNullOrEmpty(refundInfo.getOrderNo())){
|
return null;
|
||||||
String orderNo = charge.getDescription().replace("AiDA - ", "");
|
}
|
||||||
refundInfo.setOrderNo(orderNo);
|
RefundInfo refundInfo = refundInfoList.get(refundInfoList.size() - 1);
|
||||||
refundInfo.setTotalFee(charge.getAmount() / 100f);
|
if (StringUtil.isNullOrEmpty(refundInfo.getOrderNo())){
|
||||||
refundInfo.setUpdateTime(LocalDateTime.now());
|
String orderNo = charge.getDescription() != null ? charge.getDescription().replace("AiDA - ", "") : null;
|
||||||
baseMapper.updateById(refundInfo);
|
if (StringUtil.isNullOrEmpty(orderNo)){
|
||||||
return refundInfo;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,22 +23,18 @@ import com.alibaba.fastjson.JSON;
|
|||||||
import com.alibaba.fastjson.JSONObject;
|
import com.alibaba.fastjson.JSONObject;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.stripe.Stripe;
|
import com.stripe.Stripe;
|
||||||
import com.stripe.exception.InvalidRequestException;
|
import com.stripe.exception.InvalidRequestException;
|
||||||
import com.stripe.exception.SignatureVerificationException;
|
|
||||||
import com.stripe.exception.StripeException;
|
import com.stripe.exception.StripeException;
|
||||||
import com.stripe.model.*;
|
import com.stripe.model.*;
|
||||||
import com.stripe.model.Product;
|
import com.stripe.model.Product;
|
||||||
import com.stripe.model.checkout.Session;
|
import com.stripe.model.checkout.Session;
|
||||||
import com.stripe.net.Webhook;
|
|
||||||
import com.stripe.param.*;
|
import com.stripe.param.*;
|
||||||
import com.stripe.param.checkout.SessionCreateParams;
|
import com.stripe.param.checkout.SessionCreateParams;
|
||||||
import io.netty.util.internal.StringUtil;
|
import io.netty.util.internal.StringUtil;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Lazy;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -46,14 +42,16 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import jakarta.annotation.Resource;
|
import jakarta.annotation.Resource;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneOffset;
|
|
||||||
import java.time.ZonedDateTime;
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stripe 核心服务实现
|
||||||
|
*
|
||||||
|
* Stripe SDK 32.0.0 版本差异说明:
|
||||||
|
* - Subscription.getCurrentPeriodStart/End() 已移除,改用 subscription.getItems().getData().get(0).getCurrentPeriodStart/End()
|
||||||
|
*/
|
||||||
@SuppressWarnings("LoggingSimilarMessage")
|
@SuppressWarnings("LoggingSimilarMessage")
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -62,14 +60,10 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
@Resource
|
@Resource
|
||||||
private OrderInfoService orderInfoService;
|
private OrderInfoService orderInfoService;
|
||||||
@Resource
|
@Resource
|
||||||
private PayPalCheckoutService payPalCheckoutService;
|
|
||||||
@Resource
|
|
||||||
private PaymentInfoService paymentInfoService;
|
private PaymentInfoService paymentInfoService;
|
||||||
@Resource
|
@Resource
|
||||||
private CreditsService creditsService;
|
private CreditsService creditsService;
|
||||||
@Resource
|
@Resource
|
||||||
private RefundInfoService refundInfoService;
|
|
||||||
@Resource
|
|
||||||
private AccountService accountService;
|
private AccountService accountService;
|
||||||
@Resource
|
@Resource
|
||||||
private AccountMapper accountMapper;
|
private AccountMapper accountMapper;
|
||||||
@@ -81,6 +75,8 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
private ProductCouponsMapper productCouponsMapper;
|
private ProductCouponsMapper productCouponsMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private RedisUtil redisUtil;
|
private RedisUtil redisUtil;
|
||||||
|
@Resource
|
||||||
|
private StripeWebhookService stripeWebhookService;
|
||||||
|
|
||||||
@Value("${stripe.private-key}")
|
@Value("${stripe.private-key}")
|
||||||
private String privateKey;
|
private String privateKey;
|
||||||
@@ -109,6 +105,7 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
productPurchaseDTO.setAutoRenewal(false);
|
productPurchaseDTO.setAutoRenewal(false);
|
||||||
break;
|
break;
|
||||||
case "Subscription":
|
case "Subscription":
|
||||||
|
productPurchaseDTO.setAutoRenewal(true);
|
||||||
switch (productPurchaseDTO.getSubscribeType()){
|
switch (productPurchaseDTO.getSubscribeType()){
|
||||||
case "Month":
|
case "Month":
|
||||||
productEnum = ProductEnum.MonthlySubscription;
|
productEnum = ProductEnum.MonthlySubscription;
|
||||||
@@ -144,16 +141,13 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
}
|
}
|
||||||
log.info("生成订单");
|
log.info("生成订单");
|
||||||
String payType;
|
String payType;
|
||||||
byte autoRenewal;
|
|
||||||
if (productPurchaseDTO.getAutoRenewal()){
|
if (productPurchaseDTO.getAutoRenewal()){
|
||||||
payType = "recurring";
|
payType = "recurring";
|
||||||
autoRenewal = 1;
|
|
||||||
}else {
|
}else {
|
||||||
payType = "one_time";
|
payType = "one_time";
|
||||||
autoRenewal = 0;
|
|
||||||
}
|
}
|
||||||
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(),
|
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(),
|
||||||
PayTypeEnum.STRIPE.getType(), productEnum, request, autoRenewal);
|
PayTypeEnum.STRIPE.getType(), productEnum, request);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Long id = UserContext.getUserHolder().getId();
|
Long id = UserContext.getUserHolder().getId();
|
||||||
@@ -172,10 +166,20 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
// Alipay - Not supported when using Checkout in subscription mode or setup mode.
|
// Alipay - Not supported when using Checkout in subscription mode or setup mode.
|
||||||
if (payType.equals("recurring")){
|
if (payType.equals("recurring")){
|
||||||
sessionBuilder.setMode(SessionCreateParams.Mode.SUBSCRIPTION);
|
sessionBuilder.setMode(SessionCreateParams.Mode.SUBSCRIPTION);
|
||||||
sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setDescription("AiDA - " + orderId).build());
|
// Stripe SDK 32.0.0: 使用 SubscriptionData.setMetadata() 将 orderId 传递到 Subscription
|
||||||
|
// Stripe 会将该 metadata 自动传递给 Subscription 及其生成的 Invoice
|
||||||
|
sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder()
|
||||||
|
.setDescription("AiDA - " + orderId)
|
||||||
|
.putMetadata("orderId", orderId)
|
||||||
|
.build());
|
||||||
}else {
|
}else {
|
||||||
sessionBuilder.setMode(SessionCreateParams.Mode.PAYMENT);
|
sessionBuilder.setMode(SessionCreateParams.Mode.PAYMENT);
|
||||||
sessionBuilder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder().setDescription("AiDA - " + orderId).build());
|
// Stripe SDK 32.0.0: 使用 PaymentIntentData.setMetadata() 将 orderId 传递到 PaymentIntent
|
||||||
|
// 对于手动创建的 invoice,metadata 需要在 invoice 创建时单独设置
|
||||||
|
sessionBuilder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder()
|
||||||
|
.setDescription("AiDA - " + orderId)
|
||||||
|
.putMetadata("orderId", orderId)
|
||||||
|
.build());
|
||||||
// one-time 手动创建发票;订阅会自动创建invoice
|
// one-time 手动创建发票;订阅会自动创建invoice
|
||||||
sessionBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(Boolean.TRUE).build());
|
sessionBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(Boolean.TRUE).build());
|
||||||
}
|
}
|
||||||
@@ -189,7 +193,8 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
.setQuantity((long) productPurchaseDTO.getQuantity())
|
.setQuantity((long) productPurchaseDTO.getQuantity())
|
||||||
.setPrice(priceId)
|
.setPrice(priceId)
|
||||||
.build());
|
.build());
|
||||||
sessionBuilder.putMetadata("orderId", orderId); //通过订单号关联用于检索支付信息(可选)
|
// 将 orderId 写入 metadata,Stripe Checkout 会自动传递给关联的 PaymentIntent/Subscription
|
||||||
|
sessionBuilder.putMetadata("orderId", orderId);
|
||||||
|
|
||||||
Session session = Session.create(sessionBuilder.build());
|
Session session = Session.create(sessionBuilder.build());
|
||||||
List<String> paymentMethodTypes = session.getPaymentMethodTypes();
|
List<String> paymentMethodTypes = session.getPaymentMethodTypes();
|
||||||
@@ -276,422 +281,9 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
return Price.create(priceCreateParams.build());
|
return Price.create(priceCreateParams.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Resource
|
|
||||||
private EmailService emailService;
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public Boolean notify(HttpServletRequest request) {
|
public Boolean notify(HttpServletRequest request) {
|
||||||
log.info("stripe异步通知进行中");
|
return stripeWebhookService.notify(request);
|
||||||
String payload = null;
|
|
||||||
String sigHeader = null;
|
|
||||||
String endpointSecret = signSecret;
|
|
||||||
try {
|
|
||||||
sigHeader = request.getHeader("Stripe-Signature");
|
|
||||||
payload = payPalCheckoutService.getBody(request);
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info("stripe 支付回调参数解析异常:errorMsg {}", e.getMessage());
|
|
||||||
log.info("request sigHeader = {}", sigHeader);
|
|
||||||
log.info("request body = {}", JSON.toJSONString(payload));
|
|
||||||
e.printStackTrace();
|
|
||||||
return Boolean.FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
Event event;
|
|
||||||
try {
|
|
||||||
assert sigHeader != null;
|
|
||||||
event = Webhook.constructEvent(payload, sigHeader, endpointSecret);
|
|
||||||
} catch (SignatureVerificationException e) {
|
|
||||||
log.info("stripe 验签,获取事件异常, errorMsg={}", e.getMessage());
|
|
||||||
log.info("request sigHeader = {}", sigHeader);
|
|
||||||
log.info("request body = {}", JSON.toJSONString(payload));
|
|
||||||
e.printStackTrace();
|
|
||||||
return Boolean.FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
//获取自定义参数
|
|
||||||
// Deserialize the nested object inside the event
|
|
||||||
assert event != null;
|
|
||||||
EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer();
|
|
||||||
StripeObject stripeObject ;
|
|
||||||
if (dataObjectDeserializer.getObject().isPresent()) {
|
|
||||||
stripeObject = dataObjectDeserializer.getObject().get();
|
|
||||||
} else {
|
|
||||||
log.info("stripe 验签失败!");
|
|
||||||
log.info("request sigHeader = {}", sigHeader);
|
|
||||||
log.info("request body = {}", JSON.toJSONString(payload));
|
|
||||||
return Boolean.FALSE;
|
|
||||||
}
|
|
||||||
log.info("stripe验签成功");
|
|
||||||
boolean response = Boolean.TRUE;
|
|
||||||
|
|
||||||
log.info("回调事件 {}", event.getType());
|
|
||||||
if (stripeObject instanceof Session){
|
|
||||||
Session session = (Session) stripeObject;
|
|
||||||
if (event.getType().equals("checkout.session.completed")) {
|
|
||||||
response = processOrder(session);
|
|
||||||
}else if (event.getType().equals("checkout.session.expired")){
|
|
||||||
String orderNo = session.getMetadata().get("orderId");
|
|
||||||
// 会话过期 未支付 且之后没有支付成功的订单
|
|
||||||
response = processExpiredOrder(orderNo);
|
|
||||||
}
|
|
||||||
} else if (stripeObject instanceof Subscription){
|
|
||||||
Subscription subscription = (Subscription) stripeObject;
|
|
||||||
if (event.getType().equals("customer.subscription.created")){
|
|
||||||
// 添加数据到t_subscription_info表 需记录订阅id。需要判断订阅的状态是否active吗 ??
|
|
||||||
createSubscription(subscription);
|
|
||||||
log.info("创建连续订阅");
|
|
||||||
} else if (event.getType().equals("customer.subscription.updated")){
|
|
||||||
// 更新订阅信息
|
|
||||||
SubscriptionInfo subscriptionInfo = updateSubscription(subscription);
|
|
||||||
log.info("订阅更新");
|
|
||||||
if (subscription.getStatus().equals("active")){
|
|
||||||
response = sendEmail(subscription.getId(), null, null);
|
|
||||||
}
|
|
||||||
// 续订支付失败,邮件通知用户
|
|
||||||
if (subscription.getStatus().equals("past_due")){
|
|
||||||
// 发送续订失败邮件
|
|
||||||
response = sendRenewalFailEmail(null, subscription.getId(), subscriptionInfo.getOrderNo());
|
|
||||||
|
|
||||||
}
|
|
||||||
} else if (event.getType().equals("customer.subscription.deleted")){
|
|
||||||
SubscriptionInfo subscriptionInfo = updateSubscription(subscription);
|
|
||||||
if (Objects.isNull(subscriptionInfo)){
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
log.info("用户 {} 取消连续订阅 {}", subscriptionInfo.getAccountId(), subscription.getId());
|
|
||||||
if (subscriptionInfo.getCancelNotified() == (byte)0){
|
|
||||||
log.info("取消订阅 邮件通知商家");
|
|
||||||
response = sendEmail(subscription.getId(), "cancel", null);
|
|
||||||
if (response){
|
|
||||||
subscriptionInfo.setCancelNotified((byte)1);
|
|
||||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
|
||||||
// 更新订单信息
|
|
||||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo());
|
|
||||||
orderInfo.setAutoRenewal((byte)0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}/* else if (event.getType().equals("customer.subscription.paused")){
|
|
||||||
updateSubscription(subscription);
|
|
||||||
} else if (event.getType().equals("customer.subscription.resumed")){
|
|
||||||
updateSubscription(subscription);
|
|
||||||
log.info("用户订阅恢复");
|
|
||||||
}*/
|
|
||||||
} else if (stripeObject instanceof Invoice) {
|
|
||||||
Invoice invoice = (Invoice) stripeObject;
|
|
||||||
if (event.getType().equals("invoice.paid")) {
|
|
||||||
// 新增支付成功的信息,返回orderNo,表示,该回调第一次被记录
|
|
||||||
PaymentInfo paymentInfo = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice);
|
|
||||||
|
|
||||||
/* 在sendEmail方法中有做判断,这里的判断取消
|
|
||||||
// 当前支付没有被通知时才需要发送通知邮件
|
|
||||||
if (paymentInfo.getNotified().equals(0)) {
|
|
||||||
|
|
||||||
}*/
|
|
||||||
// 更新t_order_info中的total_fee,记录该订单的累计付款金额
|
|
||||||
orderInfoService.updateTotalFeeByOrderNo(paymentInfo.getOrderNo());
|
|
||||||
// 邮件通知商家和用户
|
|
||||||
String billingReason = invoice.getBillingReason();
|
|
||||||
switch (billingReason) {
|
|
||||||
case "subscription_create":
|
|
||||||
response = sendEmail(invoice.getSubscription(), "new", null);
|
|
||||||
break;
|
|
||||||
case "subscription_cycle":
|
|
||||||
response = sendEmail(invoice.getSubscription(), "renewal", null);
|
|
||||||
break;
|
|
||||||
case "manual":
|
|
||||||
boolean b = invoice.getLines().getData().get(0).getDescription().endsWith("Subscription");
|
|
||||||
if (b) {
|
|
||||||
// 非自动续订式订阅,Stripe不会创建Subscription,所以invoice中不会有subscriptionId
|
|
||||||
response = sendEmail(null, "new", paymentInfo.getOrderNo());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if (event.getType().equals("invoice.payment_failed")) {
|
|
||||||
// 更新支付信息
|
|
||||||
QueryWrapper<PaymentInfo> queryWrapper = new QueryWrapper<>();
|
|
||||||
queryWrapper.eq("transaction_id", invoice.getId());
|
|
||||||
PaymentInfo paymentInfo = paymentInfoService.getBaseMapper().selectOne(queryWrapper);
|
|
||||||
if (!Objects.isNull(paymentInfo)){
|
|
||||||
String type = invoice.getBillingReason().equals("subscription_create") ? "new" :
|
|
||||||
invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason();
|
|
||||||
Gson gson = new Gson();
|
|
||||||
String json = gson.toJson(invoice);
|
|
||||||
paymentInfo.setContent(json);
|
|
||||||
paymentInfo.setType(type);
|
|
||||||
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
|
|
||||||
paymentInfoService.updateById(paymentInfo);
|
|
||||||
|
|
||||||
// 发送续订失败邮件
|
|
||||||
response = sendRenewalFailEmail(invoice.getId(), null, paymentInfo.getOrderNo());
|
|
||||||
}else {
|
|
||||||
// 新增支付信息
|
|
||||||
PaymentInfo paymentInfoFail = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice);
|
|
||||||
// 发送新订阅失败邮件
|
|
||||||
response = sendEmail(paymentInfoFail.getOrderNo());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else if (stripeObject instanceof Charge) {
|
|
||||||
Charge charge = (Charge) stripeObject;
|
|
||||||
String orderNo = charge.getDescription().replace("AiDA - ", "");
|
|
||||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
|
||||||
if (Objects.isNull(orderInfo)){
|
|
||||||
// 说明该回调不是从AiDA订阅获得
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (event.getType().equals("charge.failed")){
|
|
||||||
// 添加支付信息 && 更新支付信息
|
|
||||||
// 支付失败时,无法通过invoice_id获取支付方式,所以使用charge.failed回调添加支付信息
|
|
||||||
paymentInfoService.createOrUpdatePaymentInfoForStripe(charge);
|
|
||||||
|
|
||||||
orderInfo.setOrderStatus(OrderStatusEnum.FAILURE.getType());
|
|
||||||
orderInfo.setNote(charge.getFailureMessage());
|
|
||||||
orderInfoService.updateById(orderInfo);
|
|
||||||
}else if (event.getType().equals("charge.succeeded")){
|
|
||||||
orderInfo.setOrderStatus(OrderStatusEnum.SUCCESS.getType());
|
|
||||||
orderInfo.setNote("");
|
|
||||||
orderInfoService.updateById(orderInfo);
|
|
||||||
}else if (event.getType().equals("charge.refunded")){
|
|
||||||
// 更新退款信息
|
|
||||||
RefundInfo refundInfo = refundInfoService.updateRefundForStripe(charge);
|
|
||||||
// 更新 t_payment_info的支付状态
|
|
||||||
if (Objects.nonNull(refundInfo)){
|
|
||||||
paymentInfoService.updatePaymentRefundStatus(charge);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}else if (stripeObject instanceof Refund){
|
|
||||||
Refund refund = (Refund) stripeObject;
|
|
||||||
if (event.getType().equals("refund.created")){
|
|
||||||
// 新增退款信息
|
|
||||||
refundInfoService.createRefundForStripe(refund);
|
|
||||||
}else if (event.getType().equals("refund.updated")){
|
|
||||||
// 根据***id更新退款记录信息
|
|
||||||
RefundInfo refundInfo = refundInfoService.updateRefundStatusForStripe(refund);
|
|
||||||
if (Objects.isNull(refundInfo)){
|
|
||||||
// 等事件先创建,再更新。回调事件的顺序随机
|
|
||||||
response = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log.info("回调事件 {} 处理完成", event.getType());
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean processOrder(Session session) {
|
|
||||||
Stripe.apiKey = privateKey;
|
|
||||||
String orderNo = session.getMetadata().get("orderId");
|
|
||||||
float totalAmount = new BigDecimal(session.getAmountTotal()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
|
||||||
|
|
||||||
boolean resp = true;
|
|
||||||
try {
|
|
||||||
//处理重复通知
|
|
||||||
//接口调用的幂等性:无论接口被调用多少次,以下业务执行一次
|
|
||||||
// String orderStatus = orderInfoService.getOrderStatus(orderNo);
|
|
||||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
|
||||||
String orderStatus = orderByOrderNo.getOrderStatus();
|
|
||||||
// 当订单状态处于未支付或超时已关闭时,更新订单状态,其他状态均不更新订单状态
|
|
||||||
if (!OrderStatusEnum.NOT_PAY.getType().equals(orderStatus) && !OrderStatusEnum.TIMEOUT_CLOSED.getType().equals(orderStatus)) {
|
|
||||||
log.info("订单状态 : {}", orderStatus);
|
|
||||||
}else {
|
|
||||||
//更新订单状态
|
|
||||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS);
|
|
||||||
log.info("Stripe 订单:{} 状态更新成功", orderNo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orderByOrderNo.getTitle().startsWith("积分购买")){
|
|
||||||
// 查询当前订单的积分是否已添加
|
|
||||||
CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo);
|
|
||||||
if (Objects.isNull(creditsDetail)){
|
|
||||||
float quantity = totalAmount / ProductEnum.CreditsProduct.getPrice();
|
|
||||||
// 更新积分
|
|
||||||
creditsService.buyCredits(orderByOrderNo.getAccountId(), quantity);
|
|
||||||
// 添加积分变更记录
|
|
||||||
creditsService.insertToCreditsDetail(orderByOrderNo.getAccountId(),
|
|
||||||
CreditsEventsEnum.BUY_CREDITS.getName() + "--Stripe",
|
|
||||||
String.valueOf((Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * quantity)),
|
|
||||||
"positive", orderNo);
|
|
||||||
log.info("用户:{} 积分信息更新成功", orderByOrderNo.getAccountId());
|
|
||||||
}
|
|
||||||
}else if (orderByOrderNo.getTitle().endsWith("Subscription") && orderByOrderNo.getAutoRenewal() == (byte)0){
|
|
||||||
String invoiceId = session.getInvoice();
|
|
||||||
Invoice invoice = Invoice.retrieve(invoiceId);
|
|
||||||
InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0);
|
|
||||||
String description = invoiceLineItem.getDescription();
|
|
||||||
Long amount = invoiceLineItem.getAmount();
|
|
||||||
log.info("单次订阅 description : {}, amount: {} 分", description, amount);
|
|
||||||
boolean b = createSubscriptionAndUpdateAccount(orderNo, orderByOrderNo.getAccountId(), description, amount);
|
|
||||||
// 邮件通知用户
|
|
||||||
if (b){
|
|
||||||
resp = sendEmail(null, "new", orderNo);
|
|
||||||
}
|
|
||||||
log.info("单次订阅订单:{} 处理完成", orderNo);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.info(e.getMessage());
|
|
||||||
resp = false;
|
|
||||||
}
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean processExpiredOrder(String orderNo) {
|
|
||||||
// 支付失败 通知商家的条件 1、会话过期 2、支付失败 3、这个用户在这个支付失败后再无支付成功的订阅
|
|
||||||
// 1、获取当前订单的支付状态
|
|
||||||
// String orderNo = session.getMetadata().get("orderId");
|
|
||||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
|
||||||
// 2、确认订单状态为支付失败
|
|
||||||
boolean resp = true;
|
|
||||||
if (!Objects.isNull(orderByOrderNo) && orderByOrderNo.getOrderStatus().equals(OrderStatusEnum.FAILURE.getType())) {
|
|
||||||
// 3、判断失败订单之后再无成功的订单
|
|
||||||
QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();
|
|
||||||
queryWrapper.eq("account_id", orderByOrderNo.getAccountId());
|
|
||||||
queryWrapper.gt("create_time", orderByOrderNo.getCreateTime());
|
|
||||||
queryWrapper.eq("order_status", OrderStatusEnum.SUCCESS.getType());
|
|
||||||
queryWrapper.likeLeft("title", "Subscription");
|
|
||||||
List<OrderInfo> orderInfos = orderInfoService.getBaseMapper().selectList(queryWrapper);
|
|
||||||
if (orderInfos.isEmpty()) {
|
|
||||||
// 4、判断当前订单有没有订阅信息
|
|
||||||
QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
|
||||||
qw.eq("order_no", orderNo);
|
|
||||||
SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw);
|
|
||||||
// 发送邮件通知商家用户支付失败
|
|
||||||
if (Objects.isNull(subscriptionInfo)
|
|
||||||
|| subscriptionInfo.getStatus().equals("incomplete")
|
|
||||||
|| subscriptionInfo.getStatus().equals("incomplete_expired")) {
|
|
||||||
resp = sendEmail(orderNo);
|
|
||||||
}else {
|
|
||||||
// todo 续订失败 应该不会走这里
|
|
||||||
resp = sendEmail(subscriptionInfo.getSubscriptionId(), "fail_renewal", null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return resp;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public SubscriptionInfo createSubscription(Subscription subscription){
|
|
||||||
// 确认当前subscription是否已经记录
|
|
||||||
SubscriptionInfo subscriptionInfo = getSubscriptionInfoBySubId(subscription.getId());
|
|
||||||
// SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw);
|
|
||||||
if (Objects.isNull(subscriptionInfo)) {
|
|
||||||
String description = subscription.getDescription();
|
|
||||||
String orderNo = description.replace("AiDA - ", "");
|
|
||||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
|
||||||
if (Objects.isNull(orderInfo)){
|
|
||||||
log.warn("未知订阅:{}", subscription.getId());
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从回调信息中获取recurring type
|
|
||||||
SubscriptionItem subscriptionItem = subscription.getItems().getData().get(0);
|
|
||||||
String interval = subscriptionItem.getPrice().getRecurring().getInterval();
|
|
||||||
|
|
||||||
subscriptionInfo = new SubscriptionInfo();
|
|
||||||
subscriptionInfo.setAccountId(orderInfo.getAccountId());
|
|
||||||
subscriptionInfo.setOrderNo(orderNo);
|
|
||||||
subscriptionInfo.setSubscriptionId(subscription.getId());
|
|
||||||
subscriptionInfo.setType(interval);
|
|
||||||
subscriptionInfo.setStatus(subscription.getStatus());
|
|
||||||
subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
|
||||||
subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart());
|
|
||||||
subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd());
|
|
||||||
subscriptionInfo.setCreateTime(LocalDateTime.now());
|
|
||||||
|
|
||||||
int rows = subscriptionInfoMapper.insertIgnore(subscriptionInfo);
|
|
||||||
log.info("Subscription info insert affect rows : {}", rows);
|
|
||||||
|
|
||||||
if (subscriptionInfo.getStatus().equals("active")){
|
|
||||||
log.info("创建订阅更新账号信息");
|
|
||||||
// 更新账号到期时间
|
|
||||||
boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd());
|
|
||||||
// 更新账号身份和积分
|
|
||||||
if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), orderNo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subscriptionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 非自动续订订阅
|
|
||||||
* Stripe不会自动创建Subscription,所以没有subscription相关的回调,无法触发订阅相关的处理代码
|
|
||||||
*/
|
|
||||||
public boolean createSubscriptionAndUpdateAccount(String orderNo, Long accountId, String description, Long amount){
|
|
||||||
QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
|
||||||
qw.eq("order_no", orderNo);
|
|
||||||
SubscriptionInfo subscriptionInfo = subscriptionInfoMapper.selectOne(qw);
|
|
||||||
if (Objects.isNull(subscriptionInfo)) {
|
|
||||||
String interval;
|
|
||||||
// 获取当前时间戳(秒级)
|
|
||||||
long currentPeriodStart = Instant.now().getEpochSecond();;
|
|
||||||
long currentPeriodEnd;
|
|
||||||
// InvoiceLineItem invoiceLineItem = invoice.getLines().getData().get(0);
|
|
||||||
if (description.equals(ProductEnum.DailySubscription.getName())
|
|
||||||
&& amount.equals(ProductEnum.DailySubscription.getPrice() * 100)){
|
|
||||||
interval = "day";
|
|
||||||
// 获取一天后的时间戳(秒级)
|
|
||||||
ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
currentPeriodEnd = now.plusDays(1).toEpochSecond();
|
|
||||||
}else if (description.equals(ProductEnum.MonthlySubscription.getName())
|
|
||||||
&& amount.equals(ProductEnum.MonthlySubscription.getPrice() * 100)){
|
|
||||||
interval = "month";
|
|
||||||
// 获取一天后的时间戳(秒级)
|
|
||||||
ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
currentPeriodEnd = now.plusMonths(1).toEpochSecond();
|
|
||||||
} else if (description.equals(ProductEnum.Eco_MonthlySubscription.getName())
|
|
||||||
&& amount.equals(ProductEnum.Eco_MonthlySubscription.getPrice() * 100)){
|
|
||||||
interval = "month";
|
|
||||||
// 获取一天后的时间戳(秒级)
|
|
||||||
ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
currentPeriodEnd = now.plusMonths(1).toEpochSecond();
|
|
||||||
} else if (description.equals(ProductEnum.AnnualSubscription.getName())
|
|
||||||
&& amount.equals(ProductEnum.AnnualSubscription.getPrice() * 100)){
|
|
||||||
interval = "year";
|
|
||||||
// 获取一天后的时间戳(秒级)
|
|
||||||
ZonedDateTime now = ZonedDateTime.now();
|
|
||||||
currentPeriodEnd = now.plusYears(1).toEpochSecond();
|
|
||||||
}else {
|
|
||||||
log.error("未知订阅类型");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
subscriptionInfo = new SubscriptionInfo();
|
|
||||||
subscriptionInfo.setAccountId(accountId);
|
|
||||||
subscriptionInfo.setOrderNo(orderNo);
|
|
||||||
subscriptionInfo.setType(interval);
|
|
||||||
subscriptionInfo.setStatus("canceled");
|
|
||||||
subscriptionInfo.setCurrentPeriodStart(currentPeriodStart);
|
|
||||||
subscriptionInfo.setCurrentPeriodEnd(currentPeriodEnd);
|
|
||||||
subscriptionInfo.setCreateTime(LocalDateTime.now());
|
|
||||||
|
|
||||||
subscriptionInfoMapper.insertIgnore(subscriptionInfo);
|
|
||||||
|
|
||||||
log.info("创建订阅, 更新账号信息");
|
|
||||||
// 更新账号到期时间
|
|
||||||
boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd());
|
|
||||||
// 更新账号身份和积分
|
|
||||||
if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), orderNo);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public SubscriptionInfo getSubscriptionInfoBySubId(String subId){
|
|
||||||
QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
|
||||||
qw.eq("subscription_id", subId);
|
|
||||||
|
|
||||||
List<SubscriptionInfo> subscriptionInfos = subscriptionInfoMapper.selectList(qw);
|
|
||||||
if (subscriptionInfos.size() == 1){
|
|
||||||
return subscriptionInfos.get(0);
|
|
||||||
}else if (subscriptionInfos.size() > 1) {
|
|
||||||
// 如果新建了多个订阅,则筛选出状态为active的订单
|
|
||||||
Optional<SubscriptionInfo> activeSubscriptionInfo = subscriptionInfos.stream()
|
|
||||||
.filter(sub -> sub.getStatus().equals("active"))
|
|
||||||
.findFirst();
|
|
||||||
|
|
||||||
return activeSubscriptionInfo.orElseGet(() -> subscriptionInfos.get(0));
|
|
||||||
}else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId){
|
public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId){
|
||||||
@@ -705,67 +297,6 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(rollbackFor = Exception.class)
|
|
||||||
public SubscriptionInfo updateSubscription(Subscription subscription){
|
|
||||||
// 获取当前是否有已经记录的subscriptionInfo
|
|
||||||
SubscriptionInfo subscriptionInfo = createSubscription(subscription);
|
|
||||||
// 用于标志数据有没有变动,避免在没有改动的情况下频繁的更新数据库
|
|
||||||
boolean flag = false;
|
|
||||||
if (!subscriptionInfo.getStatus().equals(subscription.getStatus())){
|
|
||||||
subscriptionInfo.setStatus(subscription.getStatus());
|
|
||||||
flag = true;
|
|
||||||
}
|
|
||||||
if (!subscriptionInfo.getCurrentPeriodStart().equals(subscription.getCurrentPeriodStart())){
|
|
||||||
subscriptionInfo.setCurrentPeriodStart(subscription.getCurrentPeriodStart());
|
|
||||||
flag = true;
|
|
||||||
}
|
|
||||||
if (!subscriptionInfo.getCurrentPeriodEnd().equals(subscription.getCurrentPeriodEnd())){
|
|
||||||
subscriptionInfo.setCurrentPeriodEnd(subscription.getCurrentPeriodEnd());
|
|
||||||
subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(subscription.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
|
||||||
log.info("更新订阅更新账号信息");
|
|
||||||
// 更新账号到期时间
|
|
||||||
accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd());
|
|
||||||
// 更新账号身份和积分
|
|
||||||
accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo());
|
|
||||||
log.info("更新 {} 账号到期时间为:{}", subscriptionInfo.getAccountId(), DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
|
||||||
flag = true;
|
|
||||||
}
|
|
||||||
if (subscriptionInfo.getStatus().equals("active")){
|
|
||||||
// 更新账号到期时间
|
|
||||||
boolean b = accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd());
|
|
||||||
// 更新账号身份和积分
|
|
||||||
if (b) accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo());
|
|
||||||
}
|
|
||||||
if (flag){
|
|
||||||
subscriptionInfo.setUpdateTime(LocalDateTime.now());
|
|
||||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
|
||||||
}
|
|
||||||
return subscriptionInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消连续订阅 将订阅从pause状态转为cancel状态(使用定时器,定期检索DB中,过期且不续订的订阅)
|
|
||||||
public void cancelSubscription(String subscriptionId, String cancelReason) {
|
|
||||||
Stripe.apiKey = privateKey;
|
|
||||||
log.info("cancel subscription");
|
|
||||||
Long accountId = UserContext.getUserHolder().getId();
|
|
||||||
com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(accountId);
|
|
||||||
List<Subscription> subscriptions = getSubscription(account.getUserName(), account.getUserEmail());
|
|
||||||
// 获取status = active的订阅
|
|
||||||
subscriptions.forEach(subscription -> {
|
|
||||||
if (subscription.getId().equals(subscriptionId)) {
|
|
||||||
try {
|
|
||||||
Subscription cancel = subscription.cancel();
|
|
||||||
cancel.getStatus();
|
|
||||||
log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId);
|
|
||||||
// 更新数据库
|
|
||||||
updateCancelReason(subscriptionId, cancelReason);
|
|
||||||
} catch (StripeException e) {
|
|
||||||
log.error("订阅 {} 取消失败, error message : {}", subscription.getId(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelSubscriptionTemp(String subscriptionId) {
|
public void cancelSubscriptionTemp(String subscriptionId) {
|
||||||
Stripe.apiKey = privateKey;
|
Stripe.apiKey = privateKey;
|
||||||
try {
|
try {
|
||||||
@@ -779,101 +310,39 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String refund(String amount, String orderNo, String reason) {
|
|
||||||
Refund refund;
|
|
||||||
RefundInfo refundByOrderNo = refundInfoService.createRefundByOrderNo(orderNo, reason);
|
|
||||||
|
|
||||||
try {
|
|
||||||
Stripe.apiKey = privateKey;
|
|
||||||
// todo transactionId不再是sessionId而是invoiceId,所以这里需要更新
|
|
||||||
// 根据orderId找到对应的sessionId
|
|
||||||
String sessionId = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0).getTransactionId();
|
|
||||||
|
|
||||||
if (StringUtils.isNotEmpty(sessionId)) { //根据会话编号退款
|
|
||||||
Session session = Session.retrieve(sessionId);
|
|
||||||
RefundCreateParams params;
|
|
||||||
if (amount != null && !amount.equals("0")) { //指定退款金额
|
|
||||||
BigDecimal actualAmount = new BigDecimal(amount).multiply(BigDecimal.valueOf(100)); //api默认单位分
|
|
||||||
params = RefundCreateParams.builder()
|
|
||||||
.setPaymentIntent(session.getPaymentIntent())
|
|
||||||
.setAmount(actualAmount.longValue())
|
|
||||||
.build();
|
|
||||||
} else { //全额退款
|
|
||||||
params = RefundCreateParams.builder()
|
|
||||||
.setPaymentIntent(session.getPaymentIntent())
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
refund = Refund.create(params);
|
|
||||||
log.info("根据会话编号退款成功");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
log.error("当前订单不存在");
|
|
||||||
return "退款异常";
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
//e.getMessage.contain("charge_already_refunded") 已退款
|
|
||||||
//e.getMessage.contain("resource_missing") 退款编号错误
|
|
||||||
//e.getMessage.contain("amount on charge ($n)") 金额应小于n
|
|
||||||
log.error("退款异常:", e);
|
|
||||||
return "退款异常";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ("succeeded".equals(refund.getStatus())) {
|
|
||||||
//进行数据库操作,修改状态为已退款(配合回调和退款查询确定退款成功)
|
|
||||||
//更新订单状态
|
|
||||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS);
|
|
||||||
|
|
||||||
refundInfoService.updateRefundForPayPal(
|
|
||||||
refundByOrderNo.getId(),
|
|
||||||
refund.getId(),
|
|
||||||
new Gson().toJson(refund),
|
|
||||||
AliPayTradeStateEnum.REFUND_SUCCESS.getType()); //退款成功
|
|
||||||
|
|
||||||
// 更新积分状态
|
|
||||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
|
||||||
creditsService.creditsRefund(orderByOrderNo.getAccountId(), (int) (orderByOrderNo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue())), orderNo);
|
|
||||||
} else {
|
|
||||||
//更新订单状态
|
|
||||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_ABNORMAL);
|
|
||||||
|
|
||||||
//更新退款单
|
|
||||||
refundInfoService.updateRefundForPayPal(
|
|
||||||
refundByOrderNo.getId(),
|
|
||||||
refund.getId(),
|
|
||||||
new Gson().toJson(refund),
|
|
||||||
AliPayTradeStateEnum.REFUND_ERROR.getType()); //退款失败
|
|
||||||
}
|
|
||||||
log.info("记录退款订单");
|
|
||||||
return "退款成功";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkOrderStatus(String orderNo) {
|
public void checkOrderStatus(String orderNo) {
|
||||||
Stripe.apiKey = privateKey;
|
Stripe.apiKey = privateKey;
|
||||||
// 1、通过orderNo 查询sessionId
|
List<PaymentInfo> paymentInfos = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC");
|
||||||
// todo transactionId不再是sessionId而是invoiceId,所以这里需要更新
|
if (paymentInfos == null || paymentInfos.isEmpty()) {
|
||||||
PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0);
|
log.warn("核实订单未找到 ===> {}", orderNo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PaymentInfo paymentInfo = paymentInfos.get(0);
|
||||||
|
String transactionId = paymentInfo.getTransactionId();
|
||||||
|
if (transactionId == null) {
|
||||||
|
log.warn("核实订单 transactionId 为空 ===> {}", orderNo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Session session = Session.retrieve(paymentInfo.getTransactionId());
|
Session session = Session.retrieve(transactionId);
|
||||||
if (Objects.isNull(session)) {
|
String status = session.getStatus();
|
||||||
log.warn("核实订单未创建 ===> {}", orderNo);
|
if ("open".equals(status) || "expired".equals(status)) {
|
||||||
return;
|
|
||||||
} else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) {
|
|
||||||
// 订单未支付 || 订单过期 ---> 均设置为超时未支付
|
|
||||||
log.info("订单超时未支付 ===> {}", orderNo);
|
log.info("订单超时未支付 ===> {}", orderNo);
|
||||||
//更新本地订单状态
|
|
||||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED);
|
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED);
|
||||||
paymentInfoService.updatePaymentStatusById(paymentInfo.getId(),
|
paymentInfoService.updatePaymentStatusById(paymentInfo.getId(),
|
||||||
session.getStatus(),
|
status, new Gson().toJson(session));
|
||||||
new Gson().toJson(session));
|
} else if ("complete".equals(status)) {
|
||||||
} else if (session.getStatus().equals("complete")) {
|
// 订单已完成,通过 Checkout 事件处理(积分/订阅)已在 checkout.session.completed 中处理
|
||||||
// 订单已完成
|
// 此处仅确保本地订单状态一致
|
||||||
processOrder(session);
|
String currentStatus = orderInfoService.getOrderByOrderNo(orderNo).getOrderStatus();
|
||||||
|
if (!OrderStatusEnum.SUCCESS.getType().equals(currentStatus)) {
|
||||||
|
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (StripeException e) {
|
} catch (StripeException e) {
|
||||||
log.error("根据sessionId获取Stripe Session失败");
|
// transactionId 可能是 invoiceId(Payment Mode),此时无法用 sessionId 查询
|
||||||
throw new RuntimeException(e);
|
log.warn("根据 transactionId={} 查询 Stripe Session 失败,可能为 invoiceId,error={}", transactionId, e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Subscription> getSubscription(String username, String userEmail) {
|
public List<Subscription> getSubscription(String username, String userEmail) {
|
||||||
@@ -933,84 +402,6 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
return customer.getId();
|
return customer.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用连续订阅的订单,回调中没有paymentIntentId,所以通过invoiceId间接获取
|
|
||||||
* @param invoiceId 发票Id
|
|
||||||
*/
|
|
||||||
public Map<String, String> getPaymentMethodByInvoiceId(String invoiceId) {
|
|
||||||
try {
|
|
||||||
Stripe.apiKey = privateKey;
|
|
||||||
Invoice invoice = Invoice.retrieve(invoiceId);
|
|
||||||
if (!StringUtil.isNullOrEmpty(invoice.getPaymentIntent())){
|
|
||||||
PaymentIntent paymentIntent = PaymentIntent.retrieve(invoice.getPaymentIntent());
|
|
||||||
if (!StringUtil.isNullOrEmpty(paymentIntent.getPaymentMethod())){
|
|
||||||
PaymentMethod paymentMethod = PaymentMethod.retrieve(paymentIntent.getPaymentMethod());
|
|
||||||
return getPaymentMethod(paymentMethod.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HashMap<String, String> resp = new HashMap<>();
|
|
||||||
resp.put("paymentMethod", "N/A");
|
|
||||||
resp.put("last4", "N/A");
|
|
||||||
return resp;
|
|
||||||
} catch (StripeException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<String, String> getPaymentMethod(String paymentMethodId){
|
|
||||||
Stripe.apiKey = privateKey;
|
|
||||||
String paymentMethod = null;
|
|
||||||
String last4 = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
PaymentMethod retrieve = PaymentMethod.retrieve(paymentMethodId);
|
|
||||||
switch (retrieve.getType()){
|
|
||||||
case "alipay":
|
|
||||||
paymentMethod = "Alipay";
|
|
||||||
last4 = "N/A";
|
|
||||||
break;
|
|
||||||
case "bancontact":
|
|
||||||
paymentMethod = "BanContact";
|
|
||||||
break;
|
|
||||||
case "card":
|
|
||||||
PaymentMethod.Card card = retrieve.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":
|
|
||||||
PaymentMethod.Eps eps = retrieve.getEps();
|
|
||||||
paymentMethod = eps.getBank();
|
|
||||||
last4 = "N/A";
|
|
||||||
break;
|
|
||||||
case "giropay":
|
|
||||||
paymentMethod = "GiroPay";
|
|
||||||
last4 = "N/A";
|
|
||||||
break;
|
|
||||||
case "ideal":
|
|
||||||
PaymentMethod.Ideal ideal = retrieve.getIdeal();
|
|
||||||
paymentMethod = ideal.getBank();
|
|
||||||
last4 = "N/A";
|
|
||||||
break;
|
|
||||||
case "link":
|
|
||||||
paymentMethod = "Link";
|
|
||||||
last4 = "N/A";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
paymentMethod = "N/A";
|
|
||||||
last4 = "N/A";
|
|
||||||
}
|
|
||||||
HashMap<String, String> resp = new HashMap<>();
|
|
||||||
resp.put("paymentMethod", paymentMethod);
|
|
||||||
resp.put("last4", last4);
|
|
||||||
return resp;
|
|
||||||
} catch (StripeException e) {
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
// return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean sendEmail(String subscriptionId, String type, String orderNo) {
|
public boolean sendEmail(String subscriptionId, String type, String orderNo) {
|
||||||
SubscriptionInfo subscriptionInfo;
|
SubscriptionInfo subscriptionInfo;
|
||||||
long secondsTimestamp = System.currentTimeMillis() / 1000;
|
long secondsTimestamp = System.currentTimeMillis() / 1000;
|
||||||
@@ -1060,11 +451,12 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
String periodEnd = DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_yyyy_MM_dd_HH_mm_ss);
|
String periodEnd = DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_yyyy_MM_dd_HH_mm_ss);
|
||||||
|
|
||||||
qwPI.lambda().eq(PaymentInfo::getOrderNo, subscriptionInfo.getOrderNo())
|
qwPI.lambda().eq(PaymentInfo::getOrderNo, subscriptionInfo.getOrderNo())
|
||||||
|
.eq(PaymentInfo::getTradeState, "paid")
|
||||||
.between(PaymentInfo::getCreateTime, periodStart, periodEnd)
|
.between(PaymentInfo::getCreateTime, periodStart, periodEnd)
|
||||||
.orderByDesc(PaymentInfo::getId);
|
.orderByDesc(PaymentInfo::getId);
|
||||||
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qwPI);
|
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qwPI);
|
||||||
if (paymentInfos.isEmpty()) {
|
if (paymentInfos.isEmpty()) {
|
||||||
log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo);
|
log.info("不发送邮件,原因:【根据order_no:{},查询到的成功的paymentInfos为空】", orderNo);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
PaymentInfo paymentInfo = paymentInfos.get(0);
|
PaymentInfo paymentInfo = paymentInfos.get(0);
|
||||||
@@ -1094,7 +486,8 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
emailParamsDTO.setEmail(account.getUserEmail());
|
emailParamsDTO.setEmail(account.getUserEmail());
|
||||||
emailParamsDTO.setCountry(paymentInfo.getCountry());
|
emailParamsDTO.setCountry(paymentInfo.getCountry());
|
||||||
emailParamsDTO.setOrderId(paymentInfo.getId().toString());
|
emailParamsDTO.setOrderId(paymentInfo.getId().toString());
|
||||||
emailParamsDTO.setOrderRef("\"" + orderListLink + paymentInfo.getId().toString() + "\"");
|
// emailParamsDTO.setOrderRef("\"" + orderListLink + paymentInfo.getId().toString() + "\"");
|
||||||
|
emailParamsDTO.setOrderRef("\"" + paymentInfo.getHostedInvoiceUrl() + "\"");
|
||||||
emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " "));
|
emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " "));
|
||||||
emailParamsDTO.setQuantity(String.valueOf(1));
|
emailParamsDTO.setQuantity(String.valueOf(1));
|
||||||
emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString());
|
emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString());
|
||||||
@@ -1172,7 +565,7 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){
|
/*public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){
|
||||||
// 1、确认当前订单最后一笔支付为fail
|
// 1、确认当前订单最后一笔支付为fail
|
||||||
// 更新支付信息
|
// 更新支付信息
|
||||||
PaymentInfo paymentInfo;
|
PaymentInfo paymentInfo;
|
||||||
@@ -1232,7 +625,7 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
payment.setUpdateTime(LocalDateTime.now());
|
payment.setUpdateTime(LocalDateTime.now());
|
||||||
paymentInfoMapper.updateById(payment);
|
paymentInfoMapper.updateById(payment);
|
||||||
return true;
|
return true;
|
||||||
}
|
}*/
|
||||||
|
|
||||||
private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo,
|
private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo,
|
||||||
SubscriptionEmailParamsDTO emailParamsDTO, String language) {
|
SubscriptionEmailParamsDTO emailParamsDTO, String language) {
|
||||||
@@ -1302,7 +695,7 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
public void checkSubscriptionExpiration(){
|
/*public void checkSubscriptionExpiration(){
|
||||||
long epochSecond = Instant.now().getEpochSecond();
|
long epochSecond = Instant.now().getEpochSecond();
|
||||||
QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
||||||
qw.lt("current_period_end", epochSecond);
|
qw.lt("current_period_end", epochSecond);
|
||||||
@@ -1315,13 +708,13 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
subscriptionInfoMapper.updateById(subscriptionInfo);
|
||||||
log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo());
|
log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo());
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
|
|
||||||
// 新建一个订阅 使用不会成功的付款方式(仅供测试使用)
|
// 新建一个订阅 使用不会成功的付款方式(仅供测试使用)
|
||||||
public String createSubscriptionTemp(String name, String email){
|
public String createSubscriptionTemp(String name, String email){
|
||||||
Stripe.apiKey = privateKey;
|
Stripe.apiKey = privateKey;
|
||||||
try {
|
try {
|
||||||
OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null, (byte)0);
|
OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null);
|
||||||
|
|
||||||
// String customerId = getCustomer(name, email);
|
// String customerId = getCustomer(name, email);
|
||||||
String paymentMethodCode = "pm_card_mastercard";
|
String paymentMethodCode = "pm_card_mastercard";
|
||||||
@@ -1527,9 +920,17 @@ public class StripeServiceImpl implements StripeService {
|
|||||||
|
|
||||||
public PromotionCode createPromotionCode(String couponId, Long maxRedemption){
|
public PromotionCode createPromotionCode(String couponId, Long maxRedemption){
|
||||||
Stripe.apiKey = privateKey;
|
Stripe.apiKey = privateKey;
|
||||||
|
|
||||||
|
// 1. 构建 Promotion 对象,设置 type 为 "coupon" 并传入 couponId
|
||||||
|
PromotionCodeCreateParams.Promotion promotion = PromotionCodeCreateParams.Promotion.builder()
|
||||||
|
.setCoupon(couponId)// 设置关联的优惠券ID
|
||||||
|
.setType(PromotionCodeCreateParams.Promotion.Type.COUPON)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 2. 构建主参数,通过 setPromotion 传入
|
||||||
PromotionCodeCreateParams.Builder promotionCodeParams = PromotionCodeCreateParams.builder()
|
PromotionCodeCreateParams.Builder promotionCodeParams = PromotionCodeCreateParams.builder()
|
||||||
.setCoupon(couponId)
|
.setPromotion(promotion); // 使用 setPromotion 而不是 setCoupon
|
||||||
.setRestrictions(PromotionCodeCreateParams.Restrictions.builder().build());
|
|
||||||
if (Objects.nonNull(maxRedemption)){
|
if (Objects.nonNull(maxRedemption)){
|
||||||
promotionCodeParams.setMaxRedemptions(maxRedemption);
|
promotionCodeParams.setMaxRedemptions(maxRedemption);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,607 @@
|
|||||||
|
package com.ai.da.service.impl;
|
||||||
|
|
||||||
|
import com.ai.da.common.config.exception.BusinessException;
|
||||||
|
import com.ai.da.common.constant.CommonConstant;
|
||||||
|
import com.ai.da.common.enums.ProductEnum;
|
||||||
|
import com.ai.da.common.utils.DateUtil;
|
||||||
|
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.PaymentInfoMapper;
|
||||||
|
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.PaymentInfo;
|
||||||
|
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||||
|
import com.ai.da.model.dto.SubscriptionEmailParamsDTO;
|
||||||
|
import com.ai.da.model.enums.Language;
|
||||||
|
import com.ai.da.service.*;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.stripe.Stripe;
|
||||||
|
import com.stripe.exception.StripeException;
|
||||||
|
import com.stripe.model.Customer;
|
||||||
|
import com.stripe.model.CustomerCollection;
|
||||||
|
import com.stripe.model.Subscription;
|
||||||
|
import com.stripe.param.CustomerCreateParams;
|
||||||
|
import com.stripe.param.CustomerListParams;
|
||||||
|
import io.netty.util.internal.StringUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stripe 订阅服务实现
|
||||||
|
*
|
||||||
|
* 本类负责订阅相关的业务辅助方法,供其他组件调用。
|
||||||
|
* 订阅事件处理已迁移至策略处理器:
|
||||||
|
* - InvoicePaidHandler:处理 invoice.paid
|
||||||
|
* - CheckoutSessionCompletedHandler:处理 checkout.session.completed (subscription)
|
||||||
|
* - SubscriptionDeletedHandler:处理 customer.subscription.deleted
|
||||||
|
* - SubscriptionUpdatedHandler:处理 customer.subscription.updated
|
||||||
|
*
|
||||||
|
* Stripe SDK 32.0.0 版本差异说明:
|
||||||
|
* - SubscriptionItem.getPrice().getRecurring().getInterval() 访问方式保持一致
|
||||||
|
* - Subscription.getItems().getData() 访问方式保持一致
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StripeSubscriptionServiceImpl implements StripeSubscriptionService {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private AccountService accountService;
|
||||||
|
@Resource
|
||||||
|
private AccountMapper accountMapper;
|
||||||
|
@Resource
|
||||||
|
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||||
|
@Resource
|
||||||
|
private OrderInfoService orderInfoService;
|
||||||
|
@Resource
|
||||||
|
private PaymentInfoMapper paymentInfoMapper;
|
||||||
|
@Resource
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
@Value("${stripe.private-key}")
|
||||||
|
private String privateKey;
|
||||||
|
|
||||||
|
@Value("${orderList.link}")
|
||||||
|
private String orderListLink;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送订阅相关邮件
|
||||||
|
* @param subscription Stripe Subscription object (may be null)
|
||||||
|
* @param type 邮件类型
|
||||||
|
* @param orderNo 订单号
|
||||||
|
* @param passedSubscriptionInfo 本地订阅记录 (用于避免事务未提交时重新查询,可为空)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean sendSubscriptionEmail(Subscription subscription, String type, String orderNo,
|
||||||
|
SubscriptionInfo passedSubscriptionInfo) {
|
||||||
|
SubscriptionInfo subscriptionInfo = resolveSubscriptionInfo(subscription, type, orderNo, passedSubscriptionInfo);
|
||||||
|
if (subscriptionInfo == null) {
|
||||||
|
log.info("subscriptionInfo为null,不发送邮件");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo());
|
||||||
|
if (orderByOrderNo == null) {
|
||||||
|
log.info("orderByOrderNo为null,不发送邮件");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Account account = accountMapper.selectById(subscriptionInfo.getAccountId());
|
||||||
|
if (account == null) {
|
||||||
|
log.info("account为null,不发送邮件");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentInfo paymentInfo = resolvePaymentInfo(subscriptionInfo, orderNo, type);
|
||||||
|
if (paymentInfo == null) {
|
||||||
|
log.info("paymentInfo为null,不发送邮件");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolvedType = resolveEmailType(type, paymentInfo);
|
||||||
|
if (isEmailAlreadySent(subscriptionInfo, resolvedType, paymentInfo)) {
|
||||||
|
log.info("邮件已发送,取消重复发送");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
String language = resolveLanguage(account.getLanguage(), account.getCountry(), resolvedType);
|
||||||
|
SubscriptionEmailParamsDTO params = buildEmailParams(paymentInfo, subscriptionInfo, orderByOrderNo, account, language);
|
||||||
|
|
||||||
|
boolean success = SendEmailUtil.subscriptionEmailReminder(resolvedType, params, language, account.getUserEmail());
|
||||||
|
if (success) {
|
||||||
|
markEmailSent(subscriptionInfo, resolvedType, paymentInfo);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析订阅信息
|
||||||
|
* @param subscription Stripe Subscription object (may be null)
|
||||||
|
* @param type 邮件类型
|
||||||
|
* @param orderNo 订单号
|
||||||
|
* @param passedInfo 本地订阅记录 (用于避免事务未提交时重新查询,可为空)
|
||||||
|
*/
|
||||||
|
private SubscriptionInfo resolveSubscriptionInfo(Subscription subscription, String type, String orderNo,
|
||||||
|
SubscriptionInfo passedInfo) {
|
||||||
|
if (subscription != null) {
|
||||||
|
return getSubscriptionInfoBySubId(subscription.getId());
|
||||||
|
}
|
||||||
|
// renewal 场景:从 InvoicePaidHandler 直接传入已更新的 SubscriptionInfo,避免事务未提交导致查询不到
|
||||||
|
if (passedInfo != null) {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
boolean inPeriod = now > passedInfo.getCurrentPeriodStart() && now < passedInfo.getCurrentPeriodEnd();
|
||||||
|
// 续订失败的场景,可能订单状态已被更新为past_due
|
||||||
|
boolean validStatus = "past_due".equals(passedInfo.getStatus()) || "active".equals(passedInfo.getStatus());
|
||||||
|
if (inPeriod && validStatus) {
|
||||||
|
return passedInfo;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringUtil.isNullOrEmpty(orderNo)) {
|
||||||
|
long now = Instant.now().getEpochSecond();
|
||||||
|
List<SubscriptionInfo> infos = subscriptionInfoMapper.selectList(
|
||||||
|
new QueryWrapper<SubscriptionInfo>()
|
||||||
|
.eq("order_no", orderNo)
|
||||||
|
.gt("current_period_start", now)
|
||||||
|
.lt("current_period_end", now)
|
||||||
|
);
|
||||||
|
if (!infos.isEmpty()) {
|
||||||
|
List<SubscriptionInfo> activeOnes = infos.stream()
|
||||||
|
.filter(s -> "active".equals(s.getStatus()))
|
||||||
|
.toList();
|
||||||
|
// todo 逻辑奇怪
|
||||||
|
// if ("cancel".equals(type) || "reminder_expire".equals(type)) {
|
||||||
|
// return infos.getFirst();
|
||||||
|
// }
|
||||||
|
// // todo 逻辑奇怪,待删除
|
||||||
|
// if (activeOnes.isEmpty() && "cancel".equals(type)) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
return activeOnes.isEmpty() ? null : activeOnes.getFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析支付信息
|
||||||
|
*/
|
||||||
|
private PaymentInfo resolvePaymentInfo(SubscriptionInfo subscriptionInfo, String orderNo, String type) {
|
||||||
|
String periodStart = DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_yyyy_MM_dd_HH_mm_ss);
|
||||||
|
String periodEnd = DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_yyyy_MM_dd_HH_mm_ss);
|
||||||
|
QueryWrapper<PaymentInfo> last = new QueryWrapper<PaymentInfo>()
|
||||||
|
.eq("order_no", orderNo)
|
||||||
|
.between("create_time", periodStart, periodEnd)
|
||||||
|
.orderByDesc("id")
|
||||||
|
.last("LIMIT 1");
|
||||||
|
if (!type.contains("fail")) {
|
||||||
|
last.in("trade_state", "paid", "COMPLETED", "Refunded");
|
||||||
|
}
|
||||||
|
List<PaymentInfo> infos = paymentInfoMapper.selectList(last);
|
||||||
|
return infos.isEmpty() ? null : infos.getFirst();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送首次订阅失败邮件
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void sendFailedNewOrderEmail(String orderNo) {
|
||||||
|
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||||
|
if (orderInfo == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Account account = accountMapper.selectById(orderInfo.getAccountId());
|
||||||
|
if (account == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(
|
||||||
|
new QueryWrapper<PaymentInfo>().eq("order_no", orderNo).orderByDesc("id").last("LIMIT 1")
|
||||||
|
);
|
||||||
|
if (paymentInfos.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PaymentInfo paymentInfo = paymentInfos.getFirst();
|
||||||
|
SubscriptionEmailParamsDTO params = new SubscriptionEmailParamsDTO();
|
||||||
|
params.setUsername(account.getUserName());
|
||||||
|
params.setOrderId(paymentInfo.getId().toString());
|
||||||
|
params.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " "));
|
||||||
|
params.setQuantity("1");
|
||||||
|
params.setTotalFee(paymentInfo.getPayerTotal() != null ? paymentInfo.getPayerTotal().toString() : "0");
|
||||||
|
params.setFailMessage(orderInfo.getNote());
|
||||||
|
params.setPaymentMethod(paymentInfo.getPaymentMethod());
|
||||||
|
params.setLast4(paymentInfo.getLast4());
|
||||||
|
|
||||||
|
SendEmailUtil.subscriptionEmailReminder("fail_new", params, account.getLanguage(), account.getUserEmail());
|
||||||
|
|
||||||
|
paymentInfo.setNotified(1);
|
||||||
|
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||||
|
paymentInfoMapper.updateById(paymentInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * 获取用户最新的订阅信息
|
||||||
|
// */
|
||||||
|
// @Override
|
||||||
|
// public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId) {
|
||||||
|
// List<SubscriptionInfo> infos = subscriptionInfoMapper.selectList(
|
||||||
|
// new QueryWrapper<SubscriptionInfo>()
|
||||||
|
// .eq("account_id", accountId)
|
||||||
|
// .orderByDesc("id")
|
||||||
|
// .last("LIMIT 1")
|
||||||
|
// );
|
||||||
|
// return infos.isEmpty() ? null : infos.get(0);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// /**
|
||||||
|
// * 更新订阅取消原因
|
||||||
|
// */
|
||||||
|
// @Override
|
||||||
|
// public void updateCancelReason(String subscriptionId, String reason) {
|
||||||
|
// SubscriptionInfo info = getSubscriptionInfoBySubId(subscriptionId);
|
||||||
|
// if (info != null) {
|
||||||
|
// info.setCancelReason(reason);
|
||||||
|
// subscriptionInfoMapper.updateById(info);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送续费失败邮件
|
||||||
|
*/
|
||||||
|
// @Override
|
||||||
|
// public void sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo) {
|
||||||
|
// PaymentInfo paymentInfo = resolvePaymentInfoForRenewalFail(invoiceId, subscriptionId, orderNo);
|
||||||
|
// if (paymentInfo == null || !Integer.valueOf(0).equals(paymentInfo.getNotified())) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// SubscriptionInfo subscriptionInfo = resolveSubscriptionInfoForRenewalFail(subscriptionId, orderNo);
|
||||||
|
// if (subscriptionInfo == null || !"past_due".equals(subscriptionInfo.getStatus())) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// Account account = accountMapper.selectById(subscriptionInfo.getAccountId());
|
||||||
|
// if (account == null) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo());
|
||||||
|
// SubscriptionEmailParamsDTO params = new SubscriptionEmailParamsDTO();
|
||||||
|
// params.setUsername(account.getUserName());
|
||||||
|
// params.setOrderId(paymentInfo.getId().toString());
|
||||||
|
// params.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " "));
|
||||||
|
// params.setQuantity("1");
|
||||||
|
// params.setTotalFee(paymentInfo.getPayerTotal() != null ? paymentInfo.getPayerTotal().toString() : "0");
|
||||||
|
//
|
||||||
|
// params.setPaymentMethod(paymentInfo.getPaymentMethod());
|
||||||
|
// params.setLast4(paymentInfo.getLast4());
|
||||||
|
// params.setSubscriptionId(subscriptionInfo.getId().toString());
|
||||||
|
// params.setFailMessage(orderByOrderNo != null ? orderByOrderNo.getNote() : "");
|
||||||
|
// params.setSubscriptionType(subscriptionInfo.getType());
|
||||||
|
// params.setStartDate(orderByOrderNo != null ? DateUtil.changeTimeStampFormat(orderByOrderNo.getCreateTime()) : "");
|
||||||
|
//
|
||||||
|
// boolean success = SendEmailUtil.subscriptionEmailReminder("fail_renewal", params, account.getLanguage(), account.getUserEmail());
|
||||||
|
// if (success) {
|
||||||
|
// paymentInfo.setNotified(1);
|
||||||
|
// paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||||
|
// paymentInfoMapper.updateById(paymentInfo);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析邮件类型
|
||||||
|
*/
|
||||||
|
private String resolveEmailType(String type, PaymentInfo paymentInfo) {
|
||||||
|
if (!StringUtil.isNullOrEmpty(type)) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
// todo 判断逻辑不对
|
||||||
|
return (paymentInfo != null && !StringUtil.isNullOrEmpty(paymentInfo.getType()))
|
||||||
|
? paymentInfo.getType() : "new";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查邮件是否已发送
|
||||||
|
*/
|
||||||
|
private boolean isEmailAlreadySent(SubscriptionInfo subscriptionInfo, String type, PaymentInfo paymentInfo) {
|
||||||
|
/*if ("cancel".equals(type)) {
|
||||||
|
return false;
|
||||||
|
}*/
|
||||||
|
String key = RedisUtil.SUBSCRIPTION_SENT_EMAIL_TYPE + subscriptionInfo.getId();
|
||||||
|
Boolean alreadySent = redisUtil.isElementExistsInSet(key, type);
|
||||||
|
return Boolean.TRUE.equals(alreadySent) && paymentInfo != null && Integer.valueOf(1).equals(paymentInfo.getNotified());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 标记邮件已发送
|
||||||
|
*/
|
||||||
|
private void markEmailSent(SubscriptionInfo subscriptionInfo, String type, PaymentInfo paymentInfo) {
|
||||||
|
if (!type.startsWith("reminder") && !type.equals("cancel") && paymentInfo != null) {
|
||||||
|
paymentInfo.setNotified(1);
|
||||||
|
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||||
|
paymentInfoMapper.updateById(paymentInfo);
|
||||||
|
}
|
||||||
|
String key = RedisUtil.SUBSCRIPTION_SENT_EMAIL_TYPE + subscriptionInfo.getId();
|
||||||
|
redisUtil.addToSet(key, type, CommonConstant.REDIS_SET_EXPIRE_TIME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析语言
|
||||||
|
*/
|
||||||
|
private String resolveLanguage(String language, String country, String type) {
|
||||||
|
if (StringUtil.isNullOrEmpty(language)) {
|
||||||
|
return Language.ENGLISH.name();
|
||||||
|
}
|
||||||
|
if (!StringUtil.isNullOrEmpty(type) && type.startsWith("reminder")
|
||||||
|
&& Language.CHINESE_SIMPLIFIED.name().equals(language)
|
||||||
|
&& !StringUtil.isNullOrEmpty(country)
|
||||||
|
&& ("Hong Kong, China".equals(country) || "Taiwan, China".equals(country))) {
|
||||||
|
return "zh-Hant";
|
||||||
|
}
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建邮件参数
|
||||||
|
*/
|
||||||
|
private SubscriptionEmailParamsDTO buildEmailParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo,
|
||||||
|
OrderInfo orderByOrderNo, Account account, String language) {
|
||||||
|
SubscriptionEmailParamsDTO params = new SubscriptionEmailParamsDTO();
|
||||||
|
params.setUsername(account.getUserName());
|
||||||
|
params.setEmail(account.getUserEmail());
|
||||||
|
params.setCountry(paymentInfo.getCountry());
|
||||||
|
params.setOrderId(paymentInfo.getId().toString());
|
||||||
|
// params.setOrderRef("\"" + orderListLink + paymentInfo.getId().toString() + "\"");
|
||||||
|
params.setOrderRef("\"" + paymentInfo.getHostedInvoiceUrl() + "\"");
|
||||||
|
|
||||||
|
params.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " "));
|
||||||
|
params.setQuantity("1");
|
||||||
|
params.setTotalFee(paymentInfo.getPayerTotal() != null ? paymentInfo.getPayerTotal().toString() : "0");
|
||||||
|
params.setLastOrderDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodStart(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
|
||||||
|
params.setEndOfPrepaidTerm(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
|
||||||
|
|
||||||
|
params.setPaymentMethod(paymentInfo.getPaymentMethod());
|
||||||
|
params.setLast4(paymentInfo.getLast4());
|
||||||
|
params.setSubscriptionId(subscriptionInfo.getId().toString());
|
||||||
|
params.setFailMessage(orderByOrderNo != null ? orderByOrderNo.getNote() : "");
|
||||||
|
params.setSubscriptionType(subscriptionInfo.getType());
|
||||||
|
params.setStartDate(DateUtil.changeTimeStampFormat(orderByOrderNo != null ? orderByOrderNo.getCreateTime() : null));
|
||||||
|
|
||||||
|
if (orderByOrderNo != null && orderByOrderNo.getTitle() != null) {
|
||||||
|
switch (orderByOrderNo.getTitle()) {
|
||||||
|
case "AiDA Monthly Subscription":
|
||||||
|
params.setRenewalFee(String.valueOf(ProductEnum.MonthlySubscription.getPrice()));
|
||||||
|
break;
|
||||||
|
case "AiDA Eco Monthly Subscription":
|
||||||
|
params.setRenewalFee(String.valueOf(ProductEnum.Eco_MonthlySubscription.getPrice()));
|
||||||
|
break;
|
||||||
|
case "AiDA Annual Subscription":
|
||||||
|
params.setRenewalFee(String.valueOf(ProductEnum.AnnualSubscription.getPrice()));
|
||||||
|
break;
|
||||||
|
case "AiDA Daily Subscription":
|
||||||
|
params.setRenewalFee(String.valueOf(ProductEnum.DailySubscription.getPrice()));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
params.setRenewalFee("?");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("active".equals(subscriptionInfo.getStatus())) {
|
||||||
|
params.setEndDate("ENGLISH".equals(language) ? "When cancelled" : "手动取消订阅时");
|
||||||
|
} else {
|
||||||
|
params.setEndDate(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
||||||
|
}
|
||||||
|
|
||||||
|
String nextPayDate = StringUtil.isNullOrEmpty(subscriptionInfo.getNextPayDate()) ? "N/A"
|
||||||
|
: DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy);
|
||||||
|
params.setNextPayDate(nextPayDate);
|
||||||
|
params.setRenewalTime(DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy));
|
||||||
|
|
||||||
|
String days = "month".equals(subscriptionInfo.getType()) ? "7"
|
||||||
|
: "year".equals(subscriptionInfo.getType()) ? "14" : "N/A";
|
||||||
|
params.setDays(days);
|
||||||
|
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析续费失败的支付信息
|
||||||
|
*/
|
||||||
|
// private PaymentInfo resolvePaymentInfoForRenewalFail(String invoiceId, String subscriptionId, String orderNo) {
|
||||||
|
// QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||||
|
// if (!StringUtil.isNullOrEmpty(invoiceId)) {
|
||||||
|
// qw.eq("transaction_id", invoiceId);
|
||||||
|
// return paymentInfoMapper.selectOne(qw);
|
||||||
|
// }
|
||||||
|
// qw.eq("order_no", orderNo).orderByDesc("id").last("LIMIT 1");
|
||||||
|
// List<PaymentInfo> infos = paymentInfoMapper.selectList(qw);
|
||||||
|
// if (infos.isEmpty() || !"failed".equals(infos.get(0).getTradeState())) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// return infos.get(0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析续费失败的订阅信息
|
||||||
|
*/
|
||||||
|
// private SubscriptionInfo resolveSubscriptionInfoForRenewalFail(String subscriptionId, String orderNo) {
|
||||||
|
// QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
||||||
|
// if (!StringUtil.isNullOrEmpty(subscriptionId)) {
|
||||||
|
// qw.eq("subscription_id", subscriptionId);
|
||||||
|
// } else {
|
||||||
|
// qw.eq("order_no", orderNo);
|
||||||
|
// }
|
||||||
|
// return subscriptionInfoMapper.selectOne(qw);
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建或更新订阅信息
|
||||||
|
*
|
||||||
|
* Stripe SDK 32.0.0 版本差异说明:
|
||||||
|
* - subscription.getItems().getData().get(0).getPrice().getRecurring().getInterval() 保持一致
|
||||||
|
* - subscription.getCurrentPeriodStart/End() 已移除,改用 subscription.getItems().getData().get(0).getCurrentPeriodStart/End()
|
||||||
|
*
|
||||||
|
* @param subscription Stripe Subscription
|
||||||
|
* @return SubscriptionInfo
|
||||||
|
*/
|
||||||
|
// @Transactional(rollbackFor = Exception.class)
|
||||||
|
// @Override
|
||||||
|
// public SubscriptionInfo createOrUpdateSubscriptionInfo(Subscription subscription) {
|
||||||
|
// SubscriptionInfo info = getSubscriptionInfoBySubId(subscription.getId());
|
||||||
|
// // Stripe SDK 32.0.0: subscription.getCurrentPeriodStart/End() 已移除
|
||||||
|
// // 改用 subscription.getItems().getData().get(0).getCurrentPeriodStart/End()
|
||||||
|
// SubscriptionItem subscriptionItem = subscription.getItems().getData().get(0);
|
||||||
|
// long currentPeriodStart = subscriptionItem.getCurrentPeriodStart();
|
||||||
|
// long currentPeriodEnd = subscriptionItem.getCurrentPeriodEnd();
|
||||||
|
//
|
||||||
|
// if (info == null) {
|
||||||
|
// String orderNo = extractOrderNoFromSubscription(subscription);
|
||||||
|
// if (orderNo == null) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||||
|
// if (orderInfo == null) {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// info = new SubscriptionInfo();
|
||||||
|
// info.setAccountId(orderInfo.getAccountId());
|
||||||
|
// info.setOrderNo(orderNo);
|
||||||
|
// info.setSubscriptionId(subscription.getId());
|
||||||
|
// info.setType(subscriptionItem.getPrice().getRecurring().getInterval());
|
||||||
|
// info.setStatus(subscription.getStatus());
|
||||||
|
// info.setCurrentPeriodStart(currentPeriodStart);
|
||||||
|
// info.setCurrentPeriodEnd(currentPeriodEnd);
|
||||||
|
// info.setNextPayDate(DateUtil.changeTimeStampFormat(currentPeriodEnd, "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
||||||
|
// info.setCreateTime(LocalDateTime.now());
|
||||||
|
// subscriptionInfoMapper.insert(info);
|
||||||
|
// } else {
|
||||||
|
// boolean dirty = false;
|
||||||
|
// if (!Objects.equals(info.getStatus(), subscription.getStatus())) {
|
||||||
|
// info.setStatus(subscription.getStatus());
|
||||||
|
// dirty = true;
|
||||||
|
// }
|
||||||
|
// if (!Objects.equals(info.getCurrentPeriodStart(), currentPeriodStart)) {
|
||||||
|
// info.setCurrentPeriodStart(currentPeriodStart);
|
||||||
|
// dirty = true;
|
||||||
|
// }
|
||||||
|
// if (!Objects.equals(info.getCurrentPeriodEnd(), currentPeriodEnd)) {
|
||||||
|
// info.setCurrentPeriodEnd(currentPeriodEnd);
|
||||||
|
// info.setNextPayDate(DateUtil.changeTimeStampFormat(currentPeriodEnd, "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
||||||
|
// accountService.updateAccountValidity(info.getAccountId(), currentPeriodEnd);
|
||||||
|
// accountService.updateUserRoleAndCredits(info.getAccountId(), info.getOrderNo());
|
||||||
|
// dirty = true;
|
||||||
|
// }
|
||||||
|
// if ("active".equals(info.getStatus()) || "trialing".equals(info.getStatus())) {
|
||||||
|
// accountService.updateAccountValidity(info.getAccountId(), info.getCurrentPeriodEnd());
|
||||||
|
// accountService.updateUserRoleAndCredits(info.getAccountId(), info.getOrderNo());
|
||||||
|
// }
|
||||||
|
// if (dirty) {
|
||||||
|
// info.setUpdateTime(LocalDateTime.now());
|
||||||
|
// subscriptionInfoMapper.updateById(info);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return info;
|
||||||
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据订阅ID获取订阅信息
|
||||||
|
*/
|
||||||
|
private SubscriptionInfo getSubscriptionInfoBySubId(String subId) {
|
||||||
|
List<SubscriptionInfo> infos = subscriptionInfoMapper.selectList(
|
||||||
|
new QueryWrapper<SubscriptionInfo>().eq("subscription_id", subId)
|
||||||
|
);
|
||||||
|
if (infos.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (infos.size() == 1) {
|
||||||
|
return infos.getFirst();
|
||||||
|
}
|
||||||
|
Optional<SubscriptionInfo> active = infos.stream()
|
||||||
|
.filter(s -> "active".equals(s.getStatus()))
|
||||||
|
.findFirst();
|
||||||
|
return active.orElse(infos.getFirst());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Subscription 中提取订单号
|
||||||
|
*/
|
||||||
|
// private String extractOrderNoFromSubscription(Subscription subscription) {
|
||||||
|
// String description = subscription.getDescription();
|
||||||
|
// if (!StringUtil.isNullOrEmpty(description) && description.startsWith("AiDA - ")) {
|
||||||
|
// return description.replace("AiDA - ", "");
|
||||||
|
// }
|
||||||
|
// Map<String, String> metadata = subscription.getMetadata();
|
||||||
|
// if (metadata != null && metadata.containsKey("orderId")) {
|
||||||
|
// return metadata.get("orderId");
|
||||||
|
// }
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
public void cancelSubscription(String subscriptionId, String cancelReason, Long accountId) {
|
||||||
|
Stripe.apiKey = privateKey;
|
||||||
|
log.info("申请取消连续订阅 subscriptionId={}", subscriptionId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 直接通过订阅ID检索订阅对象
|
||||||
|
Subscription subscription = Subscription.retrieve(subscriptionId);
|
||||||
|
|
||||||
|
com.ai.da.mapper.primary.entity.Account account = accountMapper.selectById(accountId);
|
||||||
|
String expectedCustomerId = getCustomer(account.getUserName(), account.getUserEmail());
|
||||||
|
|
||||||
|
// 2. 验证订阅是否属于指定客户(安全校验)
|
||||||
|
if (!expectedCustomerId.equals(subscription.getCustomer())) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
String.format("Subscription %s does not belong to customer %s",
|
||||||
|
subscriptionId, account.getUserEmail())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 执行取消操作
|
||||||
|
// 方式A:立即取消
|
||||||
|
subscription.cancel();
|
||||||
|
|
||||||
|
// 方式B:周期末取消(推荐使用 cancelAt 参数替代)
|
||||||
|
// SubscriptionUpdateParams params = SubscriptionUpdateParams.builder()
|
||||||
|
// .setCancelAtPeriodEnd(true)
|
||||||
|
// .build();
|
||||||
|
// subscription.update(params);
|
||||||
|
|
||||||
|
String reasonKey = "stripe:cancel:reason:" + subscriptionId;
|
||||||
|
// 取消原因1天过期
|
||||||
|
redisUtil.addToString(reasonKey, cancelReason != null ? cancelReason : "", 24 * 60 * 60L);
|
||||||
|
log.info("用户 {} 申请取消连续订阅 {}", accountId, subscriptionId);
|
||||||
|
} catch (StripeException e) {
|
||||||
|
log.error("订阅 {} 取消失败,error={}", subscriptionId, e.getMessage());
|
||||||
|
throw new BusinessException("Subscription cancel failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomer(String username, String userEmail) throws StripeException {
|
||||||
|
CustomerCollection list = Customer.list(CustomerListParams.builder().setEmail(userEmail).build());
|
||||||
|
List<Customer> data = list.getData();
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
return data.get(0).getId();
|
||||||
|
}
|
||||||
|
return createCustomer(username, userEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createCustomer(String name, String userEmail) throws StripeException {
|
||||||
|
Stripe.apiKey = privateKey;
|
||||||
|
|
||||||
|
// Customer允许重复使用
|
||||||
|
CustomerCreateParams params =
|
||||||
|
CustomerCreateParams.builder()
|
||||||
|
.setName(name)
|
||||||
|
.setEmail(userEmail)
|
||||||
|
.build();
|
||||||
|
Customer customer = Customer.create(params);
|
||||||
|
|
||||||
|
return customer.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package com.ai.da.service.impl;
|
||||||
|
|
||||||
|
import com.ai.da.common.utils.RedisUtil;
|
||||||
|
import com.ai.da.service.PayPalCheckoutService;
|
||||||
|
import com.ai.da.service.stripe.handler.StripeEventDispatcher;
|
||||||
|
import com.stripe.exception.SignatureVerificationException;
|
||||||
|
import com.stripe.model.Event;
|
||||||
|
import com.stripe.model.EventDataObjectDeserializer;
|
||||||
|
import com.stripe.model.StripeObject;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stripe Webhook 服务实现
|
||||||
|
* 核心职责:
|
||||||
|
* 1. 签名验证(使用 StripeWebhook.constructEvent())
|
||||||
|
* 2. 幂等性检查(Redis Key: stripe:event:{eventId},TTL 7天)
|
||||||
|
* 3. 异步处理(@Async 或 CompletableFuture)
|
||||||
|
* 4. 事件分发(策略模式 + EventDispatcher)
|
||||||
|
*
|
||||||
|
* 版本差异说明(Stripe SDK 26.2.0 -> 32.0.0):
|
||||||
|
* - Event.getDataObjectDeserializer() 行为保持一致
|
||||||
|
* - constructEvent() 签名保持一致
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class StripeWebhookServiceImpl implements com.ai.da.service.StripeWebhookService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stripe Webhook 幂等性检查 TTL:7天
|
||||||
|
* Stripe 回溯 webhook 最多 72 小时,设置为 7 天确保覆盖所有场景
|
||||||
|
*/
|
||||||
|
private static final long WEBHOOK_IDEMPOTENCY_TTL_SECONDS = TimeUnit.DAYS.toSeconds(7);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private StripeEventDispatcher eventDispatcher;
|
||||||
|
@Resource
|
||||||
|
private PayPalCheckoutService payPalCheckoutService;
|
||||||
|
@Resource
|
||||||
|
private RedisUtil redisUtil;
|
||||||
|
|
||||||
|
@Value("${stripe.webhook-sign-secret}")
|
||||||
|
private String signSecret;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Boolean notify(HttpServletRequest request) {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
String sigHeader = null;
|
||||||
|
String payload = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 解析请求参数
|
||||||
|
sigHeader = request.getHeader("Stripe-Signature");
|
||||||
|
payload = payPalCheckoutService.getBody(request);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Stripe webhook 参数解析异常:{}", e.getMessage(), e);
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 签名验证
|
||||||
|
Event event;
|
||||||
|
try {
|
||||||
|
// Stripe SDK 32.0.0 兼容性:constructEvent 签名保持一致
|
||||||
|
event = com.stripe.net.Webhook.constructEvent(payload, sigHeader, signSecret);
|
||||||
|
} catch (SignatureVerificationException e) {
|
||||||
|
log.error("Stripe webhook 验签失败:{}", e.getMessage(), e);
|
||||||
|
return Boolean.FALSE; // 返回 400 让 Stripe 不重试
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Stripe webhook 解析事件异常:{}", e.getMessage(), e);
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
String eventId = event.getId();
|
||||||
|
String eventType = event.getType();
|
||||||
|
|
||||||
|
log.info("[StripeWebhook] 接收事件,eventId={},type={},created={}",
|
||||||
|
eventId, eventType, event.getCreated());
|
||||||
|
|
||||||
|
// 3. 幂等性检查
|
||||||
|
if (!redisUtil.tryMarkWebhookProcessed(eventId, WEBHOOK_IDEMPOTENCY_TTL_SECONDS)) {
|
||||||
|
log.info("[StripeWebhook] 事件已处理过,跳过,eventId={}", eventId);
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 解析事件数据对象
|
||||||
|
EventDataObjectDeserializer deserializer = event.getDataObjectDeserializer();
|
||||||
|
Optional<StripeObject> optionalObject;
|
||||||
|
try {
|
||||||
|
optionalObject = deserializer.getObject();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[StripeWebhook] 解析事件数据对象异常,eventId={},error={}", eventId, e.getMessage(), e);
|
||||||
|
// 移除幂等标记,允许 Stripe 重试
|
||||||
|
redisUtil.removeFromString("StripeWebhook:processed:" + eventId);
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (optionalObject.isEmpty()) {
|
||||||
|
log.error("[StripeWebhook] 无法解析事件数据对象,eventId={},type={}", eventId, eventType);
|
||||||
|
// 移除幂等标记,允许 Stripe 重试
|
||||||
|
redisUtil.removeFromString("StripeWebhook:processed:" + eventId);
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
StripeObject stripeObject = optionalObject.get();
|
||||||
|
|
||||||
|
// 5. 异步处理事件
|
||||||
|
try {
|
||||||
|
processEventAsync(eventId, eventType, stripeObject);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[StripeWebhook] 启动异步处理异常,eventId={},error={}", eventId, e.getMessage(), e);
|
||||||
|
return Boolean.FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
log.info("[StripeWebhook] 事件已接收并转发处理,eventId={},type={},耗时={}ms",
|
||||||
|
eventId, eventType, elapsed);
|
||||||
|
|
||||||
|
return Boolean.TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 异步处理事件
|
||||||
|
* 使用 CompletableFuture 确保请求快速返回
|
||||||
|
*/
|
||||||
|
private void processEventAsync(String eventId, String eventType, StripeObject stripeObject) {
|
||||||
|
CompletableFuture.runAsync(() -> {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
try {
|
||||||
|
log.info("[StripeWebhook-Async] 开始处理,eventId={},type={}", eventId, eventType);
|
||||||
|
|
||||||
|
boolean success = eventDispatcher.dispatch(eventType, stripeObject);
|
||||||
|
|
||||||
|
long elapsed = System.currentTimeMillis() - startTime;
|
||||||
|
if (success) {
|
||||||
|
log.info("[StripeWebhook-Async] 处理成功,eventId={},type={},耗时={}ms",
|
||||||
|
eventId, eventType, elapsed);
|
||||||
|
} else {
|
||||||
|
log.warn("[StripeWebhook-Async] 处理失败,eventId={},type={},耗时={}ms",
|
||||||
|
eventId, eventType, elapsed);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[StripeWebhook-Async] 处理异常,eventId={},type={},error={}",
|
||||||
|
eventId, eventType, e.getMessage(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import com.ai.da.service.SubscriptionPlanService;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
@@ -28,6 +29,7 @@ import lombok.RequiredArgsConstructor;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
@@ -37,11 +39,9 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.time.format.DateTimeParseException;
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
|
import static com.ai.da.mapper.primary.entity.Account.SystemRole.EDUCATION_SUB;
|
||||||
import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.ACTIVE;
|
import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.ACTIVE;
|
||||||
import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.PENDING;
|
import static com.ai.da.mapper.primary.entity.SubscriptionPlan.SubscriptionStatus.PENDING;
|
||||||
|
|
||||||
@@ -78,9 +78,9 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
baseMapper.insert(subscriptionPlan);
|
baseMapper.insert(subscriptionPlan);
|
||||||
if (subscriptionPlanDTO.getStatus().equals(SubscriptionPlan.SubscriptionStatus.ACTIVE.name())) {
|
if (subscriptionPlan.getStatus().equals(SubscriptionPlan.SubscriptionStatus.ACTIVE.name())) {
|
||||||
// 执行一次激活扫描器
|
// 执行一次激活扫描器
|
||||||
activeSubscriptionPlan();
|
activeSubscriptionPlan(subscriptionPlan.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,52 +104,227 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
|||||||
if (account.getSystemUser().equals(8) || account.getSystemUser().equals(6)) {
|
if (account.getSystemUser().equals(8) || account.getSystemUser().equals(6)) {
|
||||||
throw new BusinessException("Sub-accounts.cannot.be.admins");
|
throw new BusinessException("Sub-accounts.cannot.be.admins");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保证订阅计划绑定的管理员所属组织的唯一性
|
||||||
|
checkAdminCrossOrg(subscriptionPlanDTO.getAdminAccId(), subscriptionPlanDTO.getOrganizationId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断指定的管理员是否已绑定其他组织的订阅计划
|
||||||
|
private void checkAdminCrossOrg(Long adminAccId, Long organizationId) {
|
||||||
|
QueryWrapper<SubscriptionPlan> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.lambda().eq(SubscriptionPlan::getAdminAccId, adminAccId)
|
||||||
|
.ne(SubscriptionPlan::getOrganizationId, organizationId);
|
||||||
|
Long count = baseMapper.selectCount(queryWrapper);
|
||||||
|
if (count > 0) {
|
||||||
|
throw new BusinessException("administrator.user.is.already.bound.to.different.organization");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新 到期时间、积分总量、已使用积分量
|
// 更新 到期时间、积分总量、已使用积分量
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
@Override
|
@Override
|
||||||
public void updatePlan(UpdateSubscriptionPlanDTO updateDTO) {
|
public void updatePlan(UpdateSubscriptionPlanDTO dto) {
|
||||||
if (Objects.isNull(updateDTO.getId())) {
|
if (dto.getId() == null) {
|
||||||
throw new BusinessException("id.cannot.be.empty");
|
throw new BusinessException("id.cannot.be.empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
SubscriptionPlan subscriptionPlan = baseMapper.selectById(updateDTO.getId());
|
SubscriptionPlan plan = baseMapper.selectById(dto.getId());
|
||||||
if (Objects.isNull(subscriptionPlan)) {
|
if (plan == null) {
|
||||||
throw new BusinessException("unknown.subscription.plan");
|
throw new BusinessException("unknown.subscription.plan");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Objects.nonNull(updateDTO.getCurrentPeriodStart()) && !updateDTO.getCurrentPeriodStart().equals(subscriptionPlan.getCurrentPeriodStart())) {
|
boolean activateToday = false;
|
||||||
subscriptionPlan.setCurrentPeriodStart(updateDTO.getCurrentPeriodStart());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Objects.nonNull(updateDTO.getCurrentPeriodEnd()) && !updateDTO.getCurrentPeriodEnd().equals(subscriptionPlan.getCurrentPeriodEnd())) {
|
activateToday = handlePeriodStart(dto, plan);
|
||||||
subscriptionPlan.setCurrentPeriodEnd(updateDTO.getCurrentPeriodEnd());
|
handlePeriodEnd(dto, plan);
|
||||||
}
|
handleAccountNum(dto, plan);
|
||||||
|
handleCreditLimit(dto, plan);
|
||||||
|
handleBasicInfo(dto, plan);
|
||||||
|
|
||||||
if (Objects.nonNull(updateDTO.getAccountNum()) && !updateDTO.getAccountNum().equals(subscriptionPlan.getAccountNum())) {
|
plan.setUpdateTime(LocalDateTime.now());
|
||||||
subscriptionPlan.setAccountNum(updateDTO.getAccountNum());
|
updateById(plan);
|
||||||
}
|
|
||||||
|
|
||||||
if (Objects.nonNull(updateDTO.getCreditLimit()) && !updateDTO.getCreditLimit().equals(subscriptionPlan.getCreditLimit())) {
|
|
||||||
subscriptionPlan.setCreditLimit(updateDTO.getCreditLimit());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Objects.nonNull(updateDTO.getAdminAccId()) && !updateDTO.getAdminAccId().equals(subscriptionPlan.getAdminAccId())) {
|
|
||||||
subscriptionPlan.setAdminAccId(updateDTO.getAdminAccId());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(updateDTO.getName()) && !updateDTO.getName().equals(subscriptionPlan.getName())) {
|
|
||||||
subscriptionPlan.setName(updateDTO.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
subscriptionPlan.setUpdateTime(LocalDateTime.now());
|
|
||||||
updateById(subscriptionPlan);
|
|
||||||
|
|
||||||
|
postUpdateProcess(plan, activateToday);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updatePlan() {
|
// ===================== 字段处理 =====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理开始时间,返回是否需要当天激活
|
||||||
|
*/
|
||||||
|
private boolean handlePeriodStart(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
|
||||||
|
Long newStart = dto.getCurrentPeriodStart();
|
||||||
|
if (newStart == null || newStart.equals(plan.getCurrentPeriodStart())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ACTIVE.name().equals(plan.getStatus())) {
|
||||||
|
throw new BusinessException(
|
||||||
|
"only.subscription.plans.with.a.PENDING.status.can.have.their.start.time.modified"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.setCurrentPeriodStart(newStart);
|
||||||
|
return isToday(newStart);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理结束时间(只能延长)
|
||||||
|
*/
|
||||||
|
private void handlePeriodEnd(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
|
||||||
|
Long newEnd = dto.getCurrentPeriodEnd();
|
||||||
|
if (newEnd == null || newEnd.equals(plan.getCurrentPeriodEnd())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newEnd < plan.getCurrentPeriodEnd()) {
|
||||||
|
throw new BusinessException(
|
||||||
|
"the.subscription.end.date.can.be.extended.only.not.reduced"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.setCurrentPeriodEnd(newEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理账号数量
|
||||||
|
*/
|
||||||
|
private void handleAccountNum(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
|
||||||
|
Integer newAccountNum = dto.getAccountNum();
|
||||||
|
if (newAccountNum == null || newAccountNum.equals(plan.getAccountNum())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newAccountNum < plan.getAccountNum()) {
|
||||||
|
long usedSubAccounts = countExistingSubAccounts(plan.getId());
|
||||||
|
if (newAccountNum < usedSubAccounts + 1) {
|
||||||
|
throw new BusinessException(
|
||||||
|
"total.sub-account.quota.cannot.be.lower.than.existing.sub-accounts"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.setAccountNum(newAccountNum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理积分上限
|
||||||
|
*/
|
||||||
|
private void handleCreditLimit(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
|
||||||
|
BigDecimal newLimit = dto.getCreditLimit();
|
||||||
|
if (newLimit == null || newLimit.equals(plan.getCreditLimit())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newLimit.compareTo(plan.getCreditUsage()) < 0) {
|
||||||
|
throw new BusinessException(
|
||||||
|
"the.credit.limit.set.cannot.be.lower.than.the.amount.of.credits.already.used"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
plan.setCreditLimit(newLimit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础字段
|
||||||
|
*/
|
||||||
|
private void handleBasicInfo(UpdateSubscriptionPlanDTO dto, SubscriptionPlan plan) {
|
||||||
|
if (dto.getAdminAccId() != null
|
||||||
|
&& !dto.getAdminAccId().equals(plan.getAdminAccId())) {
|
||||||
|
// 保证订阅计划绑定的管理员所属组织的唯一性
|
||||||
|
checkAdminCrossOrg(dto.getAdminAccId(), plan.getOrganizationId());
|
||||||
|
plan.setAdminAccId(dto.getAdminAccId());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(dto.getName())
|
||||||
|
&& !dto.getName().equals(plan.getName())) {
|
||||||
|
plan.setName(dto.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(dto.getCountryOrRegion())
|
||||||
|
&& !dto.getCountryOrRegion().equals(plan.getCountryOrRegion())) {
|
||||||
|
plan.setCountryOrRegion(dto.getCountryOrRegion());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 更新后处理 =====================
|
||||||
|
|
||||||
|
private void postUpdateProcess(SubscriptionPlan plan, boolean activateToday) {
|
||||||
|
if (ACTIVE.name().equals(plan.getStatus())) {
|
||||||
|
syncAdminAndSubAccounts(plan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activateToday) {
|
||||||
|
activeSubscriptionPlan(plan.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 账号同步 =====================
|
||||||
|
|
||||||
|
private void syncAdminAndSubAccounts(SubscriptionPlan plan) {
|
||||||
|
Account admin = findActiveAdmin(plan);
|
||||||
|
if (admin != null) {
|
||||||
|
syncAdminAccount(admin, plan);
|
||||||
|
}
|
||||||
|
syncSubAccounts(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Account findActiveAdmin(SubscriptionPlan plan) {
|
||||||
|
return accountMapper.selectOne(
|
||||||
|
new QueryWrapper<Account>().lambda()
|
||||||
|
.eq(Account::getId, plan.getAdminAccId())
|
||||||
|
.eq(Account::getSubscriptionPlanId, plan.getId())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncAdminAccount(Account admin, SubscriptionPlan plan) {
|
||||||
|
long planEndMillis = toMillis(plan.getCurrentPeriodEnd());
|
||||||
|
|
||||||
|
if (!Objects.equals(admin.getValidEndTime(), planEndMillis)) {
|
||||||
|
admin.setValidEndTime(planEndMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admin.getCreditsUsageLimit().compareTo(plan.getCreditLimit()) != 0) {
|
||||||
|
// 这里计算修改前后的差值,上限增长,则差为正,上限下降,则差为负;
|
||||||
|
BigDecimal delta = plan.getCreditLimit()
|
||||||
|
.subtract(admin.getCreditsUsageLimit());
|
||||||
|
|
||||||
|
// 因为管理员的积分中可能包含自己购买的积分,所以这里直接将差值添加到管理员的credit中
|
||||||
|
admin.setCredits(admin.getCredits().add(delta));
|
||||||
|
admin.setCreditsUsageLimit(plan.getCreditLimit());
|
||||||
|
}
|
||||||
|
|
||||||
|
accountMapper.updateById(admin);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void syncSubAccounts(SubscriptionPlan plan) {
|
||||||
|
accountMapper.update(
|
||||||
|
null,
|
||||||
|
new UpdateWrapper<Account>().lambda()
|
||||||
|
.set(Account::getValidEndTime, toMillis(plan.getCurrentPeriodEnd()))
|
||||||
|
.eq(Account::getSubscriptionPlanId, plan.getId())
|
||||||
|
.eq(Account::getSystemUser, EDUCATION_SUB.getCode())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===================== 辅助方法 =====================
|
||||||
|
|
||||||
|
private long countExistingSubAccounts(Long planId) {
|
||||||
|
return accountMapper.selectCount(
|
||||||
|
new QueryWrapper<Account>().lambda()
|
||||||
|
.eq(Account::getSubscriptionPlanId, planId)
|
||||||
|
.eq(Account::getSystemUser, EDUCATION_SUB.getCode())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isToday(Long timestampSeconds) {
|
||||||
|
return timestampSeconds >= getTodayStartTimestamp()
|
||||||
|
&& timestampSeconds < getTodayEndTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
private long toMillis(Long seconds) {
|
||||||
|
return seconds * 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -167,6 +342,12 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
|||||||
queryWrapper.lambda().in(SubscriptionPlan::getStatus, subscriptionPlanPageQuery.getStatus());
|
queryWrapper.lambda().in(SubscriptionPlan::getStatus, subscriptionPlanPageQuery.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(subscriptionPlanPageQuery.getCountryOrRegion())){
|
||||||
|
queryWrapper.lambda().like(SubscriptionPlan::getCountryOrRegion, subscriptionPlanPageQuery.getCountryOrRegion());
|
||||||
|
}
|
||||||
|
|
||||||
|
queryWrapper.lambda().orderByAsc(SubscriptionPlan::getCurrentPeriodStart);
|
||||||
|
|
||||||
return baseMapper.selectList(queryWrapper);
|
return baseMapper.selectList(queryWrapper);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -221,6 +402,9 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
|||||||
wrapper.in("sp.status", query.getStatus());
|
wrapper.in("sp.status", query.getStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(query.getCountryOrRegion())) {
|
||||||
|
wrapper.like("sp.country_or_region", query.getCountryOrRegion());
|
||||||
|
}
|
||||||
// 按创建时间倒序排序
|
// 按创建时间倒序排序
|
||||||
wrapper.ne("sp.is_deleted", 1)
|
wrapper.ne("sp.is_deleted", 1)
|
||||||
.orderByDesc("sp.create_time");
|
.orderByDesc("sp.create_time");
|
||||||
@@ -398,7 +582,7 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否在有效期内
|
// 检查是否在有效期内
|
||||||
if (plan.getCurrentPeriodEnd() != null && isExpired(plan.getCurrentPeriodEnd())) {
|
if (plan.getCurrentPeriodEnd() != null && !isExpired(plan.getCurrentPeriodEnd())) {
|
||||||
throw new BusinessException("valid.subscription.period");
|
throw new BusinessException("valid.subscription.period");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,18 +602,28 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
|||||||
return currentPeriodEnd < currentTimestamp;
|
return currentPeriodEnd < currentTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void activeSubscriptionPlan() {
|
public void activeSubscriptionPlan(Long planId) {
|
||||||
log.info("开始执行订阅计划生效检查...");
|
log.info("开始执行订阅计划生效检查...");
|
||||||
|
|
||||||
// 1. 扫描所有的订阅计划的开始时间currentPeriodStart,找出今天开始生效的计划
|
// 支持按id激活
|
||||||
List<SubscriptionPlan> todayActivePlans = findTodayActivePlans();
|
List<SubscriptionPlan> todayActivePlans = new ArrayList<>();
|
||||||
|
if (Objects.nonNull(planId)) {
|
||||||
|
SubscriptionPlan subscriptionPlan = baseMapper.selectById(planId);
|
||||||
|
if (Objects.nonNull(subscriptionPlan)){
|
||||||
|
todayActivePlans.add(subscriptionPlan);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 1. 扫描所有的订阅计划的开始时间currentPeriodStart,找出今天开始生效的计划
|
||||||
|
todayActivePlans = findTodayActivePlans();
|
||||||
|
|
||||||
if (CollectionUtils.isEmpty(todayActivePlans)) {
|
if (CollectionUtils.isEmpty(todayActivePlans)) {
|
||||||
log.info("今日没有需要生效的订阅计划");
|
log.info("今日没有需要生效的订阅计划");
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("发现{}个今日生效的订阅计划", todayActivePlans.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("发现{}个今日生效的订阅计划", todayActivePlans.size());
|
|
||||||
|
|
||||||
// 2. 处理每个今天开始生效的订阅计划
|
// 2. 处理每个今天开始生效的订阅计划
|
||||||
for (SubscriptionPlan plan : todayActivePlans) {
|
for (SubscriptionPlan plan : todayActivePlans) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user