Compare commits
208 Commits
501032ef17
...
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 |
@@ -99,6 +99,8 @@ jobs:
|
||||
volumes:
|
||||
# 数据挂载
|
||||
- ./log:/log
|
||||
- ./temp:/temp
|
||||
- ./uploads:/temp/uploads
|
||||
ports:
|
||||
- '10090:5567'
|
||||
restart: always
|
||||
@@ -133,8 +135,6 @@ jobs:
|
||||
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
echo "停止旧容器..."
|
||||
docker compose down || true
|
||||
echo "清理Docker资源..."
|
||||
docker system prune -f
|
||||
echo "构建镜像..."
|
||||
docker compose build --no-cache
|
||||
echo "启动服务..."
|
||||
|
||||
7
pom.xml
7
pom.xml
@@ -240,7 +240,7 @@
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
<version>26.2.0</version>
|
||||
<version>32.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- aws s3 -->
|
||||
@@ -427,6 +427,11 @@
|
||||
<artifactId>bcpkix-jdk18on</artifactId>
|
||||
<version>1.78.1</version>
|
||||
</dependency>
|
||||
<!-- AOP -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -28,6 +28,11 @@ public class MQPublisher {
|
||||
amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getSr(), mm);
|
||||
}
|
||||
|
||||
public void sendGenerateResultMessage(String mm) {
|
||||
log.info("send generate result message: {}", mm);
|
||||
amqpTemplate.convertAndSend(rabbitMQProperties.getQueues().getGenerateResult(), mm);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param mailParams 含有的字段
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.ai.da.common.aspect;
|
||||
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.annotation.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Controller日志切面
|
||||
* 记录所有Controller接口的请求参数和用户信息
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
public class ControllerLoggingAspect {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ControllerLoggingAspect.class);
|
||||
|
||||
/**
|
||||
* 定义切点:所有Controller方法
|
||||
*/
|
||||
@Pointcut("execution(* com.ai.da.controller..*(..))")
|
||||
public void controllerMethods() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller方法执行前记录日志
|
||||
*/
|
||||
// @Before("controllerMethods()")
|
||||
public void logControllerBefore(JoinPoint joinPoint) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
|
||||
// 获取当前用户ID
|
||||
Long userId = null;
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
if (authPrincipalVo != null) {
|
||||
userId = authPrincipalVo.getId();
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
Map<String, Object> params = getRequestParams(joinPoint, request);
|
||||
|
||||
logger.info("=== 请求开始 ===");
|
||||
logger.info("用户ID: {}", userId);
|
||||
logger.info("请求URL: {}", request.getRequestURL().toString());
|
||||
logger.info("请求方法: {}", request.getMethod());
|
||||
logger.info("请求IP: {}", getClientIpAddress(request));
|
||||
logger.info("调用方法: {}.{}", joinPoint.getSignature().getDeclaringType().getSimpleName(), joinPoint.getSignature().getName());
|
||||
logger.info("请求参数: {}", params);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取请求参数
|
||||
*/
|
||||
private Map<String, Object> getRequestParams(JoinPoint joinPoint, HttpServletRequest request) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
|
||||
// 1. 获取Query String参数
|
||||
String queryString = request.getQueryString();
|
||||
if (queryString != null && !queryString.isEmpty()) {
|
||||
params.put("queryString", queryString);
|
||||
}
|
||||
|
||||
// 2. 获取方法参数(包含 @PathVariable, @RequestParam, @RequestBody 等)
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
if (args != null && args.length > 0) {
|
||||
Map<String, Object> methodParams = new HashMap<>();
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
Object arg = args[i];
|
||||
// 过滤掉不可序列化的参数
|
||||
if (arg != null) {
|
||||
if (isIgnorable(arg)) {
|
||||
// 对于可忽略的类型,记录类型名
|
||||
methodParams.put("arg" + i, "[" + arg.getClass().getSimpleName() + "]");
|
||||
} else {
|
||||
try {
|
||||
methodParams.put("arg" + i, arg);
|
||||
} catch (Exception e) {
|
||||
methodParams.put("arg" + i, arg.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!methodParams.isEmpty()) {
|
||||
params.put("methodParams", methodParams);
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要过滤的参数类型
|
||||
*/
|
||||
private boolean isIgnorable(Object obj) {
|
||||
return obj instanceof HttpServletRequest
|
||||
|| obj instanceof HttpServletResponse
|
||||
|| obj instanceof MultipartFile
|
||||
|| obj instanceof MultipartFile[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Controller方法抛出异常时记录日志
|
||||
*/
|
||||
@AfterThrowing(pointcut = "controllerMethods()", throwing = "exception")
|
||||
public void logControllerAfterThrowing(JoinPoint joinPoint, Throwable exception) {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
|
||||
Long userId = null;
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
if (authPrincipalVo != null) {
|
||||
userId = authPrincipalVo.getId();
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
if (attributes != null) {
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
params = getRequestParams(joinPoint, request);
|
||||
}
|
||||
|
||||
logger.error("=== 请求异常 ===");
|
||||
logger.error("用户ID: {}", userId);
|
||||
logger.error("调用方法: {}.{}", joinPoint.getSignature().getDeclaringType().getSimpleName(), joinPoint.getSignature().getName());
|
||||
logger.error("请求参数: {}", params);
|
||||
logger.error("异常信息: ", exception);
|
||||
logger.error("=== 异常结束 ===");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端真实IP地址
|
||||
*/
|
||||
private String getClientIpAddress(HttpServletRequest request) {
|
||||
String xForwardedFor = request.getHeader("X-Forwarded-For");
|
||||
if (xForwardedFor != null && !xForwardedFor.isEmpty() && !"unknown".equalsIgnoreCase(xForwardedFor)) {
|
||||
return xForwardedFor.split(",")[0];
|
||||
}
|
||||
|
||||
String xRealIp = request.getHeader("X-Real-IP");
|
||||
if (xRealIp != null && !xRealIp.isEmpty() && !"unknown".equalsIgnoreCase(xRealIp)) {
|
||||
return xRealIp;
|
||||
}
|
||||
|
||||
String proxyClientIp = request.getHeader("Proxy-Client-IP");
|
||||
if (proxyClientIp != null && !proxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(proxyClientIp)) {
|
||||
return proxyClientIp;
|
||||
}
|
||||
|
||||
String wlProxyClientIp = request.getHeader("WL-Proxy-Client-IP");
|
||||
if (wlProxyClientIp != null && !wlProxyClientIp.isEmpty() && !"unknown".equalsIgnoreCase(wlProxyClientIp)) {
|
||||
return wlProxyClientIp;
|
||||
}
|
||||
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -202,7 +202,7 @@ public class MyTaskScheduler {
|
||||
}
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
public void sendTrialOrderExcelToManagements() {
|
||||
// 获取前一天日期
|
||||
LocalDate yesterday = LocalDate.now().minusDays(1);
|
||||
|
||||
@@ -13,8 +13,9 @@ public class CommonConstant {
|
||||
public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60;
|
||||
// 单位 秒 一天过期 in redis
|
||||
public static final Long GENERATE_RESULT_EXPIRE_TIME = 24 * 60 * 60L;
|
||||
// 单位 秒 7天过期
|
||||
public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L;
|
||||
// 单位 秒 7天过期 (todo 测试状态下 3小时过期)
|
||||
// public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L;
|
||||
public static final Long REDIS_SET_EXPIRE_TIME = 3 * 60 * 60L;
|
||||
|
||||
public static class Numbers{
|
||||
public static final Integer NUMBER_10 = 10;
|
||||
@@ -23,6 +24,7 @@ public class CommonConstant {
|
||||
}
|
||||
|
||||
public static final String GENERATE_PATH = "/api/generate_image";
|
||||
public static final String GENERATE_PATH_FLUX2_KLEIN = "/api/generate_image_flux2_klein";
|
||||
|
||||
public static final String GENERATE_SINGLE_LOGO = "/api/generate_single_logo";
|
||||
|
||||
|
||||
@@ -18,9 +18,9 @@ public class ModelConstants {
|
||||
|
||||
// 模型名称常量
|
||||
public static final String PRINTBOARD_ADVANCED_T2I = "qwen-image";
|
||||
public static final String MOODBOARD_ADVANCED = "doubao-seedream-3-0-t2i-250415";
|
||||
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-3-0-t2i-250415";
|
||||
public static final String PRINTBOARD_HIGH_I2I = "doubao-seededit-3-0-i2i-250628";
|
||||
public static final String MOODBOARD_ADVANCED = "doubao-seedream-4-5-251128";
|
||||
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-4-0-250828-high";
|
||||
public static final String PRINTBOARD_HIGH_I2I = "doubao-seedream-4-0-250828-fast";
|
||||
public static final String PRINTBOARD_ADVANCED_I2I = "doubao-seedream-4-0-250828";
|
||||
public static final String IMAGEN_MODEL = "imagen-4.0-generate-001";
|
||||
public static final String NANO_BANANA = "gemini-2.5-flash-image";
|
||||
|
||||
@@ -33,7 +33,11 @@ public enum AuthenticationOperationTypeEnum {
|
||||
*/
|
||||
UPDATE_USERINFO,
|
||||
|
||||
REGISTER;
|
||||
REGISTER,
|
||||
/**
|
||||
* Global_Award 活动验证
|
||||
*/
|
||||
GLOBAL_AWARD;
|
||||
|
||||
public static AuthenticationOperationTypeEnum of(String name) {
|
||||
return Stream.of(AuthenticationOperationTypeEnum.values()).filter(v -> v.name().equals(name)).findFirst().orElse(null);
|
||||
|
||||
@@ -30,7 +30,7 @@ public enum CreditsEventsEnum {
|
||||
INIT_QUARTERLY("init_quarterly", "12000"),
|
||||
INIT_MONTHLY_EDU("init_monthly_edu", "3500"),
|
||||
INIT_TRIAL("init_trial", "100"),
|
||||
INIT_WEEKLY("init_weekly","6000"),
|
||||
INIT_DAILY("init_daily","100"),
|
||||
RESET_YEAR_CREDITS("reset_year_credits","6000"),
|
||||
|
||||
// SUPER_RESOLUTION("Super Resolution","30"),
|
||||
|
||||
@@ -34,6 +34,11 @@ public enum OrderStatusEnum {
|
||||
* 已退款
|
||||
*/
|
||||
REFUND_SUCCESS("已退款"),
|
||||
|
||||
/**
|
||||
* 已部分退款
|
||||
*/
|
||||
PARTIAL_REFUND_SUCCESS("已部分退款"),
|
||||
/**
|
||||
* 退款异常
|
||||
*/
|
||||
|
||||
19
src/main/java/com/ai/da/common/enums/PaymentInfoType.java
Normal file
19
src/main/java/com/ai/da/common/enums/PaymentInfoType.java
Normal file
@@ -0,0 +1,19 @@
|
||||
package com.ai.da.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PaymentInfoType {
|
||||
|
||||
NEW("new"),
|
||||
|
||||
RENEWAL("renewal"),
|
||||
|
||||
CREDIT("credit"),
|
||||
|
||||
MANUAL("manual");
|
||||
|
||||
private final String type;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.ai.da.common.enums;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProductEnum {
|
||||
@@ -23,11 +25,27 @@ public enum ProductEnum {
|
||||
;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
* 显示名称(用于与 orderInfo.title 匹配)
|
||||
*/
|
||||
private final String name;
|
||||
|
||||
private final Long price;
|
||||
|
||||
private final Long credits;
|
||||
|
||||
/**
|
||||
* 根据显示名称获取枚举
|
||||
*
|
||||
* @param name 显示名称(与 orderInfo.title 匹配)
|
||||
* @return 对应的枚举,未找到返回 null
|
||||
*/
|
||||
public static ProductEnum getByName(String name) {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
return Arrays.stream(values())
|
||||
.filter(pe -> pe.name.equals(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,9 @@ public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
"/api/account/designWorksRegister","/api/account/questionnaire","/api/stripe/trade/notify",
|
||||
"/notification","/api/account/activateNewEmail","/api/third/party/auth/google_callback","/api/third/party/parseGoogleCredential","/api/third/party/receiveDesignResults","/api/third/party/parseWeChatCode","/api/third/party/receiveDesignParams"
|
||||
, "/api/account/schoolLogin", "/api/account/enterpriseLogin", "/api/account/organizationNameSearch",
|
||||
"/api/llm/stream"
|
||||
"/api/llm/stream",
|
||||
//GlobalAwardController
|
||||
"/api/global-award"
|
||||
);
|
||||
|
||||
@Override
|
||||
|
||||
@@ -34,14 +34,14 @@ public class AccountTask {
|
||||
accountService.refreshCreditsMonthly();
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
public void getPaidUser() {
|
||||
// 获取code-create 表中 指定日期之后 订单状态为wc-processing的订单
|
||||
accountService.extendValidityForCC();
|
||||
}
|
||||
|
||||
// 每天凌晨0点执行一次
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
// 每天凌晨0点执行一次 目前已没有角色类型为4的用户
|
||||
/*@Scheduled(cron = "0 0 0 * * ?")
|
||||
public void cancelActivityBenefits() {
|
||||
// 1、查询当前所有参与了活动且过期的用户
|
||||
List<Account> accountList = accountService.getExpiredUserBySystemUser(4);
|
||||
@@ -51,7 +51,7 @@ public class AccountTask {
|
||||
log.info("参与活动的用户{} : {} 于 {} 账号有效期到期,置为游客", account.getId(), account.getUserEmail(), account.getValidEndTime());
|
||||
accountService.toVisitor(account);
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// 每天检测正式用户到期情况,每天凌晨0点执行
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
@@ -92,7 +92,7 @@ public class AccountTask {
|
||||
|
||||
@Scheduled(cron = "0 5 0 * * ?")
|
||||
public void activeSubscriptionPlan() {
|
||||
subscriptionPlanService.activeSubscriptionPlan();
|
||||
subscriptionPlanService.activeSubscriptionPlan(null);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
|
||||
@@ -45,7 +45,7 @@ public class PaymentTask {
|
||||
@Resource
|
||||
private PayPalCheckoutService payPalCheckoutService;
|
||||
|
||||
// @Scheduled(cron = "0/30 * * * * ?")
|
||||
// @Scheduled(cron = "0/30 * * * * ?")
|
||||
public void orderConfirmForPaypal() throws SerializeException {
|
||||
|
||||
// log.info("PayPal orderConfirm 被执行......");
|
||||
@@ -97,7 +97,7 @@ public class PaymentTask {
|
||||
//
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
public void updateAffiliateInfoWithPayment(){
|
||||
// log.info("佣金计算定时器");
|
||||
affiliateService.updateAffiliateInfoWithPayment();
|
||||
@@ -109,7 +109,7 @@ public class PaymentTask {
|
||||
affiliateService.syncLinkViewCountToDB();
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
||||
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
||||
public void commissionSummaryReminder(){
|
||||
// 每个月末的最后一天的早上八点执行
|
||||
LocalDate today = LocalDate.now();
|
||||
|
||||
@@ -40,7 +40,7 @@ public class SubscriptionReminderTask {
|
||||
REMINDER_DAYS_CONFIG.put("year", 14);
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
public void subscriptionReminder() {
|
||||
// 获取所有需要通知的订阅
|
||||
List<SubscriptionInfo> subscriptionInfos = getDueSubscriptions();
|
||||
@@ -97,7 +97,7 @@ public class SubscriptionReminderTask {
|
||||
return subscriptionInfoMapper.selectList(qw);
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
// @Scheduled(cron = "0 0 9 * * ?")
|
||||
public void trialReminder() {
|
||||
// 今天的 00:00:00 和 23:59:59
|
||||
LocalDateTime startOfDay = LocalDateTime.now().toLocalDate().atStartOfDay();
|
||||
|
||||
@@ -41,6 +41,13 @@ public class MinioUtil {
|
||||
@Autowired
|
||||
private MinioClient minioClient;
|
||||
|
||||
/**
|
||||
* 获取MinIO客户端实例
|
||||
*/
|
||||
public MinioClient getMinioClient() {
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* description: 判断bucket是否存在,不存在则创建
|
||||
*
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,17 +17,38 @@ import com.tencentcloudapi.ses.v20201002.models.SendEmailRequest;
|
||||
import com.tencentcloudapi.ses.v20201002.models.SendEmailResponse;
|
||||
import com.tencentcloudapi.ses.v20201002.models.Template;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* 邮件发送类
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SendEmailUtil {
|
||||
@Value("${merchant.email:}")
|
||||
private String merchantEmailInstance;
|
||||
@Value("${developer.email: xupei@code-create.com.hk}")
|
||||
private String developerEmailInstance;
|
||||
|
||||
private static String merchantEmail;
|
||||
private static String developerEmail;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
merchantEmail = merchantEmailInstance;
|
||||
developerEmail = developerEmailInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 秘钥id
|
||||
*/
|
||||
@@ -765,9 +786,7 @@ public class SendEmailUtil {
|
||||
|
||||
public static boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||
try {
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
Credential cred = new Credential(SECRET_ID, SECRET_KEy);
|
||||
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
@@ -966,9 +985,7 @@ public class SendEmailUtil {
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
SendEmailRequest req = new SendEmailRequest();
|
||||
req.setFromEmailAddress(SEND_ADDRESS);
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developerEmail = "xupei@code-create.com.hk";
|
||||
req.setDestination(new String[]{/*merchantEmail,*/ developerEmail});
|
||||
req.setDestination(buildMerchantReceiverEmail());
|
||||
Template template = new Template();
|
||||
req.setSubject("New Credit Purchase Order");
|
||||
template.setTemplateID(CREDITS_PURCHASE_MERCHANT);
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,16 +15,16 @@ import com.ai.da.model.vo.PersonalHomepageVO;
|
||||
import com.ai.da.service.AccountService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.Valid;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -139,21 +139,21 @@ public class AccountController {
|
||||
@Operation(summary = "aws状态检测")
|
||||
@GetMapping("/healthy")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
public Response<Map<String,Integer>> checkStatus(){
|
||||
Map<String,Integer> returnMap = new HashMap<>();
|
||||
returnMap.put("code",200);
|
||||
public Response<Map<String, Integer>> checkStatus() {
|
||||
Map<String, Integer> returnMap = new HashMap<>();
|
||||
returnMap.put("code", 200);
|
||||
return Response.success(returnMap);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询账号到期时间")
|
||||
@PostMapping("/getExpiredTime")
|
||||
public Response<Long> getExpiredTime(){
|
||||
public Response<Long> getExpiredTime() {
|
||||
return Response.success(accountService.getExpiredTime());
|
||||
}
|
||||
|
||||
@Operation(summary = "免密登录")
|
||||
@PostMapping("/noLoginRequired")
|
||||
public Response<AccountLoginVO> noLoginRequired(@RequestBody NoLoginRequiredDTO noLoginRequiredDTO, HttpServletRequest request){
|
||||
public Response<AccountLoginVO> noLoginRequired(@RequestBody NoLoginRequiredDTO noLoginRequiredDTO, HttpServletRequest request) {
|
||||
return Response.success(accountService.noLoginRequired(noLoginRequiredDTO, request));
|
||||
}
|
||||
|
||||
@@ -191,6 +191,7 @@ public class AccountController {
|
||||
|
||||
/**
|
||||
* 参与活动 获取福利
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
/* @Operation(summary = "参与活动 获取福利")
|
||||
@@ -201,7 +202,7 @@ public class AccountController {
|
||||
|
||||
@Operation(summary = "将用户账号过期时间设置为过期当天的23:59:59")
|
||||
@GetMapping("/setUserValidToDayEnd")
|
||||
public Response<List<Long>> setUserValidToDayEnd(){
|
||||
public Response<List<Long>> setUserValidToDayEnd() {
|
||||
return Response.success(accountService.setUserValidToDayEnd());
|
||||
}
|
||||
|
||||
@@ -223,19 +224,19 @@ public class AccountController {
|
||||
|
||||
@Operation(summary = "获取个人主页信息")
|
||||
@GetMapping("/personalHomepage")
|
||||
public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id){
|
||||
public Response<PersonalHomepageVO> getPersonalHomepage(@RequestParam("id") Long id) {
|
||||
return Response.success(accountService.getPersonalHomepage(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "getUsernameModifyTimes")
|
||||
@GetMapping("/getNicknameModifyTimes")
|
||||
public Response<Long> getNicknameModifyTimes(){
|
||||
public Response<Long> getNicknameModifyTimes() {
|
||||
return Response.success(accountService.getNicknameModifyTimes());
|
||||
}
|
||||
|
||||
@Operation(summary = "editUserName")
|
||||
@GetMapping("/editUserName")
|
||||
public Response<String> editUserName(@RequestParam("newUserName") String newUserName){
|
||||
public Response<String> editUserName(@RequestParam("newUserName") String newUserName) {
|
||||
accountService.editUserName(newUserName);
|
||||
return Response.success("success");
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class ConvenientInquiryController {
|
||||
@GetMapping("/recentNewUserChart")
|
||||
public Response<Map<String, Object>> recentNewUserChart(@Parameter(description = "startTime") @RequestParam @Nullable String startTime,
|
||||
@Parameter(description = "endTime") @RequestParam @Nullable String endTime,
|
||||
@Parameter(description = "userType") @RequestParam Integer userType) {
|
||||
@Parameter(description = "userType") @RequestParam @Nullable Integer userType) {
|
||||
return Response.success(convenientInquiryService.recentNewUserChart(startTime, endTime, userType));
|
||||
}
|
||||
|
||||
@@ -179,8 +179,10 @@ public class ConvenientInquiryController {
|
||||
|
||||
@Operation(summary = "获取所有用户id")
|
||||
@GetMapping("/getAllUserId")
|
||||
public Response<List<Map<String, Object>>> getAllUsrIdList() {
|
||||
return Response.success(convenientInquiryService.getAllUserIdList());
|
||||
public Response<IPage<Map<String, Object>>> getAllUserIdList(@Parameter(description = "page") @RequestParam Integer page,
|
||||
@Parameter(description = "size") @RequestParam Integer size,
|
||||
@Parameter(description = "email 模糊查询") @RequestParam(required = false) String email) {
|
||||
return Response.success(convenientInquiryService.getAllUserIdList(page, size, email));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有交易信息")
|
||||
|
||||
217
src/main/java/com/ai/da/controller/GlobalAwardController.java
Normal file
217
src/main/java/com/ai/da/controller/GlobalAwardController.java
Normal file
@@ -0,0 +1,217 @@
|
||||
package com.ai.da.controller;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.model.dto.*;
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.model.vo.PageVisitCountVO;
|
||||
import com.ai.da.service.GlobalAwardService;
|
||||
import com.ai.da.service.upload.UploadService;
|
||||
import com.ai.da.service.upload.UploadTask;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import io.swagger.annotations.ApiParam;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/global-award")
|
||||
@Api(tags = "全球奖项API", description = "全球奖项大赛管理和文件上传")
|
||||
public class GlobalAwardController {
|
||||
|
||||
@Resource
|
||||
private GlobalAwardService globalAwardService;
|
||||
|
||||
@Resource
|
||||
private UploadService uploadService;
|
||||
|
||||
// @PostMapping("/uploads/pdf")
|
||||
// public Response<String> uploadPdf(@RequestParam("file") MultipartFile file,
|
||||
// @RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
// return Response.success(globalAwardService.uploadPdf(file, email));
|
||||
// }
|
||||
//
|
||||
// @PostMapping("/uploads/video")
|
||||
// public Response<String> uploadVideo(@RequestParam("file") MultipartFile file,
|
||||
// @RequestParam(value = "email", required = false) String email) throws Exception {
|
||||
// return Response.success(globalAwardService.uploadVideo(file, email));
|
||||
// }
|
||||
|
||||
// ===== 新增分片上传接口 =====
|
||||
|
||||
// ===== PDF分片上传接口 =====
|
||||
|
||||
/** 初始化PDF上传任务 */
|
||||
@PostMapping("/uploads/pdf/init")
|
||||
@ApiOperation(value = "初始化PDF上传", notes = "创建新的PDF上传任务并返回上传参数")
|
||||
public Response<UploadInitResponse> initPdfUpload(@ApiParam(value = "PDF上传初始化请求", required = true) @RequestBody UploadInitRequest request) {
|
||||
UploadTask uploadTask = uploadService.initPdfUpload(request);
|
||||
return Response.success(UploadInitResponse.builder()
|
||||
.uploadId(uploadTask.getUploadId())
|
||||
.chunkSize(uploadTask.getChunkSize())
|
||||
.totalChunks(uploadTask.getTotalChunks())
|
||||
.expiresAt(uploadTask.getExpiresAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
/** 上传PDF分片 */
|
||||
@PostMapping("/uploads/pdf/chunk")
|
||||
@ApiOperation(value = "上传PDF分片", notes = "上传PDF文件的单个分片")
|
||||
public Response<UploadChunkResponse> uploadPdfChunk(
|
||||
@ApiParam(value = "PDF文件分片", required = true) @RequestParam("chunk") MultipartFile chunk,
|
||||
@ApiParam(value = "上传任务ID", required = true) @RequestParam("uploadId") String uploadId,
|
||||
@ApiParam(value = "分片索引(从0开始)", required = true) @RequestParam("chunkIndex") int chunkIndex,
|
||||
@ApiParam(value = "分片总数", required = true) @RequestParam("totalChunks") int totalChunks) {
|
||||
|
||||
UploadChunkResponse uploadChunkResponse = uploadService.uploadPdfChunk(uploadId, chunk, chunkIndex, totalChunks);
|
||||
return Response.success(uploadChunkResponse);
|
||||
}
|
||||
|
||||
/** 完成PDF上传 */
|
||||
@PostMapping("/uploads/pdf/complete")
|
||||
@ApiOperation(value = "完成PDF上传", notes = "完成PDF上传并合并所有分片")
|
||||
public Response<UploadCompleteResponse> completePdfUpload(@ApiParam(value = "PDF上传完成请求", required = true) @RequestBody UploadCompleteRequest request) {
|
||||
UploadCompleteResponse uploadCompleteResponse = uploadService.completePdfUpload(
|
||||
request.getUploadId(),
|
||||
request.getFileName(),
|
||||
request.getTotalSize(),
|
||||
request.getEmail(),
|
||||
request.getSecureToken());
|
||||
return Response.success(uploadCompleteResponse);
|
||||
}
|
||||
|
||||
/** 查询PDF上传状态 */
|
||||
@GetMapping("/uploads/pdf/status/{uploadId}")
|
||||
@ApiOperation(value = "查询PDF上传状态", notes = "获取PDF上传任务的当前状态")
|
||||
public Response<UploadStatusResponse> getPdfUploadStatus(@ApiParam(value = "上传任务ID", required = true) @PathVariable String uploadId) {
|
||||
UploadStatusResponse pdfUploadStatus = uploadService.getPdfUploadStatus(uploadId);
|
||||
return Response.success(pdfUploadStatus);
|
||||
}
|
||||
|
||||
// ===== 视频分片上传接口 =====
|
||||
|
||||
/** 初始化视频上传任务 */
|
||||
@PostMapping("/uploads/video/init")
|
||||
@ApiOperation(value = "初始化视频上传", notes = "创建新的视频上传任务并返回上传参数")
|
||||
public Response<UploadInitResponse> initVideoUpload(@ApiParam(value = "视频上传初始化请求", required = true) @RequestBody UploadInitRequest request) {
|
||||
UploadTask uploadTask = uploadService.initVideoUpload(request);
|
||||
return Response.success(UploadInitResponse.builder()
|
||||
.uploadId(uploadTask.getUploadId())
|
||||
.chunkSize(uploadTask.getChunkSize())
|
||||
.totalChunks(uploadTask.getTotalChunks())
|
||||
.expiresAt(uploadTask.getExpiresAt())
|
||||
.build());
|
||||
}
|
||||
|
||||
/** 上传视频分片 */
|
||||
@PostMapping("/uploads/video/chunk")
|
||||
@ApiOperation(value = "上传视频分片", notes = "上传视频文件的单个分片")
|
||||
public Response<UploadChunkResponse> uploadVideoChunk(
|
||||
@ApiParam(value = "视频文件分片", required = true) @RequestParam("chunk") MultipartFile chunk,
|
||||
@ApiParam(value = "上传任务ID", required = true) @RequestParam("uploadId") String uploadId,
|
||||
@ApiParam(value = "分片索引(从0开始)", required = true) @RequestParam("chunkIndex") int chunkIndex,
|
||||
@ApiParam(value = "分片总数", required = true) @RequestParam("totalChunks") int totalChunks) {
|
||||
|
||||
UploadChunkResponse uploadChunkResponse = uploadService.uploadVideoChunk(uploadId, chunk, chunkIndex, totalChunks);
|
||||
return Response.success(uploadChunkResponse);
|
||||
}
|
||||
|
||||
/** 完成视频上传 */
|
||||
@PostMapping("/uploads/video/complete")
|
||||
@ApiOperation(value = "完成视频上传", notes = "完成视频上传并合并所有分片")
|
||||
public Response<UploadCompleteResponse> completeVideoUpload(@ApiParam(value = "视频上传完成请求", required = true) @RequestBody UploadCompleteRequest request) {
|
||||
UploadCompleteResponse uploadCompleteResponse = uploadService.completeVideoUpload(
|
||||
request.getUploadId(),
|
||||
request.getFileName(),
|
||||
request.getTotalSize(),
|
||||
request.getEmail(),
|
||||
request.getSecureToken());
|
||||
return Response.success(uploadCompleteResponse);
|
||||
}
|
||||
|
||||
/** 查询视频上传状态 */
|
||||
@GetMapping("/uploads/video/status/{uploadId}")
|
||||
@ApiOperation(value = "查询视频上传状态", notes = "获取视频上传任务的当前状态")
|
||||
public Response<UploadStatusResponse> getVideoUploadStatus(@ApiParam(value = "上传任务ID", required = true) @PathVariable String uploadId) {
|
||||
UploadStatusResponse videoUploadStatus = uploadService.getVideoUploadStatus(uploadId);
|
||||
return Response.success(videoUploadStatus);
|
||||
}
|
||||
|
||||
@PostMapping("/contestants/save")
|
||||
@ApiOperation(value = "保存参赛者信息", notes = "保存或更新参赛者信息及已上传的文件")
|
||||
public Response<Map<String,Object>> submit(@ApiParam(value = "参赛者信息", required = true) @RequestBody ContestantDTO request) {
|
||||
return Response.success(globalAwardService.saveContestant(request));
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/{id}")
|
||||
@ApiOperation(value = "根据id查询参赛者", notes = "根据id获取参赛者信息")
|
||||
public Response<ContestantDTO> getContestantByID(@ApiParam(value = "参赛者id", required = true) @PathVariable("id") String id) {
|
||||
ContestantDTO dto = globalAwardService.getContestantByID(id);
|
||||
return Response.success(dto);
|
||||
}
|
||||
|
||||
@GetMapping("/checkEmail")
|
||||
public Response<String> checkEmail(@RequestParam("email") String email) {
|
||||
globalAwardService.checkEmail(email);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@GetMapping("/checkCode")
|
||||
public Response<CheckOTPVO> checkCode(@RequestParam("email") String email, @RequestParam("code") String code) {
|
||||
return Response.success(globalAwardService.checkCode(email, code));
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/export")
|
||||
@ApiOperation(value = "导出参赛者列表为Excel", notes = "导出所有参赛者信息为xlsx并触发下载")
|
||||
public void exportContestants(HttpServletResponse response) throws Exception {
|
||||
byte[] data = globalAwardService.exportContestants();
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"contestants.xlsx\"");
|
||||
response.setContentLength(data.length);
|
||||
response.getOutputStream().write(data);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
@PostMapping("/contestants/export/files")
|
||||
@ApiOperation(value = "导出参赛者文件为ZIP", notes = "根据参赛者编号范围导出PDF、视频和信息文件为ZIP,直接响应给浏览器")
|
||||
public void exportContestantFiles(@ApiParam(value = "参赛者文件导出请求", required = true) @RequestBody ContestantExportRequest request, HttpServletResponse response) throws Exception {
|
||||
byte[] zipData = globalAwardService.exportContestantFilesAsZip(request.getMinContestantNumber(), request.getMaxContestantNumber());
|
||||
if (zipData.length == 0) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
response.getWriter().write("No contestants found in the specified range.");
|
||||
return;
|
||||
}
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"contestants.zip\"");
|
||||
response.setContentLength(zipData.length);
|
||||
response.getOutputStream().write(zipData);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/count")
|
||||
@ApiOperation(value = "查询参赛者总数", notes = "查询数据库中参赛者的总数量和最大参赛者编号")
|
||||
public Response<ContestantCountVO> getContestantCount() {
|
||||
return Response.success(globalAwardService.getContestantCount());
|
||||
}
|
||||
|
||||
@PostMapping("/page/visit")
|
||||
@ApiOperation(value = "记录比赛页面访问量", notes = "记录比赛页面的访问量,包含两种统计方式:每次访问/刷新计一次,以及5秒内刷新只计一次")
|
||||
public Response<Void> recordPageVisit(@ApiParam(value = "会话ID,用于5秒内去重判断", required = false) @RequestParam(value = "sessionId", required = false) String sessionId) {
|
||||
globalAwardService.recordPageVisit(sessionId);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
@GetMapping("/page/visit/count")
|
||||
@ApiOperation(value = "获取比赛页面访问量", notes = "获取比赛页面的两种访问量:rawVisitCount(每次访问/刷新计一次)和 uniqueVisitCount(5秒内刷新只计一次)")
|
||||
public Response<PageVisitCountVO> getPageVisitCount() {
|
||||
return Response.success(globalAwardService.getPageVisitCount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
@@ -119,4 +122,14 @@ public class PythonController {
|
||||
return Response.success(superResolutionService.prepareForSR(superResolutionDTO));
|
||||
}
|
||||
|
||||
@CrossOrigin
|
||||
@Operation(summary = "Seg Anything 转发接口")
|
||||
@PostMapping("/segAnything")
|
||||
public Response<String> segAnything(@RequestBody Map<String, Object> payload) {
|
||||
// 将前端传来的 Map 转为 fastjson JSONObject 并转发给 python 服务
|
||||
JSONObject requestJson = (JSONObject) JSON.toJSON(payload);
|
||||
String url = pythonService.segAnything(requestJson);
|
||||
return Response.success(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ai.da.controller;
|
||||
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
@@ -10,6 +11,7 @@ import com.ai.da.model.dto.ProductPurchaseDTO;
|
||||
import com.ai.da.model.dto.QueryCouponsPageDTO;
|
||||
import com.ai.da.model.vo.CheckCouponsVO;
|
||||
import com.ai.da.service.StripeService;
|
||||
import com.ai.da.service.StripeSubscriptionService;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.paypal.http.HttpResponse;
|
||||
import com.paypal.payments.Refund;
|
||||
@@ -40,6 +42,8 @@ public class StripeController {
|
||||
private StripeService stripeService;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
|
||||
@Operation(summary = "创建支付链接")
|
||||
@PostMapping("/createOrder")
|
||||
@@ -53,30 +57,29 @@ public class StripeController {
|
||||
@Operation(summary = "支付通知")
|
||||
@PostMapping("/trade/notify")
|
||||
public void callback(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
try{
|
||||
Boolean result = stripeService.notify(request);
|
||||
if (result){
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
}else {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}catch (Exception e){
|
||||
log.error("Stripe Controller层异常捕捉, {}", e.getMessage());
|
||||
e.printStackTrace();
|
||||
boolean result;
|
||||
try {
|
||||
result = stripeService.notify(request);
|
||||
} catch (Exception e) {
|
||||
log.error("Stripe Controller层异常捕捉, {}", e.getMessage(), e);
|
||||
String key_1 = RedisUtil.STRIPE_EXCEPTION_LOG + DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_HH);
|
||||
String key_2 = key_1 + ":" + DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_hh_mm_ss);
|
||||
String key_2 = key_1 + ":" + DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD_HH_MM_SS);
|
||||
String stackTrace = stripeService.getStackTrace(e, 10);
|
||||
redisUtil.addToString(key_2, stackTrace);
|
||||
Long size = redisUtil.getSize(key_1);
|
||||
// 给我发送邮件
|
||||
if (webhookReminderFlag.equals("1") && size == 3){
|
||||
if ("1".equals(webhookReminderFlag) && size == 3) {
|
||||
SendEmailUtil.commonExceptionReminder("Stripe Webhook 回调处理出现异常", new String[]{"xupei3360@163.com"});
|
||||
}
|
||||
result = false;
|
||||
}
|
||||
if (result) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
} else {
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "申请退款")
|
||||
/* @Operation(summary = "申请退款")
|
||||
@GetMapping("/trade/refund/{orderNo}/{reason}")
|
||||
public Response<HttpResponse<Refund>> refund(@PathVariable String orderNo, @PathVariable String reason) throws IOException {
|
||||
String response = stripeService.refund(null,orderNo,reason);
|
||||
@@ -85,7 +88,7 @@ public class StripeController {
|
||||
}else {
|
||||
return Response.fail("Request for refund failed.");
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
@Operation(summary = "获取订阅")
|
||||
@GetMapping("/getSubscription")
|
||||
@@ -100,7 +103,8 @@ public class StripeController {
|
||||
@Operation(summary = "取消订阅")
|
||||
@GetMapping("/cancelSubscription")
|
||||
public Response<String> cancelSubscription(@RequestParam String subscriptionId, @RequestParam(required = false) String reason) {
|
||||
stripeService.cancelSubscription(subscriptionId, reason);
|
||||
Long accountId = UserContext.getUserHolder().getId();
|
||||
stripeSubscriptionService.cancelSubscription(subscriptionId, reason, accountId);
|
||||
return Response.success("success");
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ public class SubscriptionPlanController {
|
||||
@Operation(summary = "activeSubscriptionPlan")
|
||||
@GetMapping("/activeSubscriptionPlan")
|
||||
public Response<String> activeSubscriptionPlan() {
|
||||
subscriptionPlanService.activeSubscriptionPlan();
|
||||
subscriptionPlanService.activeSubscriptionPlan(null);
|
||||
return Response.success();
|
||||
}
|
||||
|
||||
|
||||
12
src/main/java/com/ai/da/mapper/primary/ContestantMapper.java
Normal file
12
src/main/java/com/ai/da/mapper/primary/ContestantMapper.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.ai.da.mapper.primary;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.Contestant;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
public interface ContestantMapper extends BaseMapper<Contestant> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface DesignMapper extends CommonMapper<Design> {
|
||||
Long insertDesign(Design design);
|
||||
|
||||
List<UserDesignStatisticDTO> getDesignStatistic(String startTime, String endTime, List<Long> ids, String email,
|
||||
String role, String organizationName);
|
||||
String role, String organizationName, boolean filterBySecond);
|
||||
|
||||
List<Design> selectDeleteList();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.ai.da.mapper.primary;
|
||||
|
||||
import com.ai.da.common.config.mybatis.plus.CommonMapper;
|
||||
import com.ai.da.mapper.primary.entity.UserPreference;
|
||||
|
||||
public interface UserPreferenceMapper extends CommonMapper<UserPreference> {
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.ai.da.mapper.primary;
|
||||
import com.ai.da.common.config.mybatis.plus.CommonMapper;
|
||||
import com.ai.da.mapper.primary.entity.WorkspaceRelStyle;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Mapper 接口
|
||||
*
|
||||
@@ -11,5 +13,11 @@ import com.ai.da.mapper.primary.entity.WorkspaceRelStyle;
|
||||
*/
|
||||
public interface WorkspaceRelStyleMapper extends CommonMapper<WorkspaceRelStyle> {
|
||||
|
||||
/**
|
||||
* 根据projectId查询workspaceRelStyles
|
||||
* @param projectId 项目ID
|
||||
* @return workspaceRelStyles列表
|
||||
*/
|
||||
List<WorkspaceRelStyle> selectByProjectId(Long projectId);
|
||||
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
@@ -76,13 +77,13 @@ public class Account implements Serializable {
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date createDate;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone="GMT+8")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
|
||||
private Date updateDate;
|
||||
|
||||
private Integer isTrial;
|
||||
@@ -142,4 +143,26 @@ public class Account implements Serializable {
|
||||
private String givenName;
|
||||
|
||||
private Long subscriptionPlanId;
|
||||
|
||||
// 在类内部定义的枚举
|
||||
@Getter
|
||||
public enum SystemRole {
|
||||
VISITOR("游客", 0),
|
||||
YEARLY("年付用户", 1),
|
||||
MONTHLY("月付用户", 2),
|
||||
TRIAL("试用用户", 3),
|
||||
EVENT_USER("参加活动获取30天有效期和6000个积分的用户", 4),
|
||||
ENTERPRISE_ADMIN("企业管理员账号", 5),
|
||||
ENTERPRISE_SUB("企业子账号", 6),
|
||||
EDUCATION_ADMIN("学校管理员", 7),
|
||||
EDUCATION_SUB("学校子账号", 8);
|
||||
|
||||
private final String desc;
|
||||
private final int code;
|
||||
|
||||
SystemRole(String desc, int code) {
|
||||
this.desc = desc;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.ai.da.mapper.primary.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* submissions 表对应实体 — 参赛选手信息 (Contestant)
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
@TableName("contestants")
|
||||
public class Contestant {
|
||||
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
private String email;
|
||||
|
||||
@TableField("contestant_number")
|
||||
private Integer contestantNumber;
|
||||
|
||||
@TableField("first_name")
|
||||
private String firstName;
|
||||
|
||||
@TableField("last_name")
|
||||
private String lastName;
|
||||
|
||||
private String gender;
|
||||
|
||||
private String occupation;
|
||||
|
||||
private Integer age;
|
||||
|
||||
@TableField("country_region_city")
|
||||
private String countryRegionCity;
|
||||
|
||||
@TableField("phone_number")
|
||||
private String phoneNumber;
|
||||
|
||||
@TableField("design_title")
|
||||
private String designTitle;
|
||||
|
||||
@TableField("design_description")
|
||||
private String designDescription;
|
||||
|
||||
@TableField("pdf_path")
|
||||
private String pdfPath;
|
||||
|
||||
@TableField("video_path")
|
||||
private String videoPath;
|
||||
|
||||
@TableField("video_duration")
|
||||
private Integer videoDuration;
|
||||
|
||||
@TableField("video_size")
|
||||
private Long videoSize;
|
||||
|
||||
@TableField("pdf_size")
|
||||
private Long pdfSize;
|
||||
|
||||
@TableField("portfolio_url")
|
||||
private String portfolioUrl;
|
||||
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@TableField("updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -62,4 +62,9 @@ public class DesignItemDetailPrint {
|
||||
* 更新时间
|
||||
*/
|
||||
private LocalDateTime updateDate;
|
||||
|
||||
/**
|
||||
* 对象信息(JSON格式)
|
||||
*/
|
||||
private String object;
|
||||
}
|
||||
|
||||
@@ -23,8 +23,6 @@ public class OrderInfo extends BaseEntity{
|
||||
|
||||
private String note;
|
||||
|
||||
private byte autoRenewal;
|
||||
|
||||
private String paymentType;//支付方式
|
||||
|
||||
// 可用于标记用户订单是否首次订阅
|
||||
|
||||
@@ -67,6 +67,11 @@ public class SubscriptionPlan extends BaseEntity{
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 国家或地区
|
||||
*/
|
||||
private String countryOrRegion;
|
||||
|
||||
// 在类内部定义的枚举
|
||||
@Getter
|
||||
public enum SubscriptionStatus {
|
||||
|
||||
@@ -90,6 +90,19 @@ public class TDesignPythonOutfitDetail implements Serializable {
|
||||
*/
|
||||
@Schema(description = "图层优先级")
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 镜像模式
|
||||
*/
|
||||
@Schema(description = "镜像模式")
|
||||
private String transpose;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
@Schema(description = "旋转角度")
|
||||
private Double rotate;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ai.da.mapper.primary.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = false)
|
||||
@Accessors(chain = true)
|
||||
@TableName("user_preference")
|
||||
public class UserPreference implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@TableId(value = "id", type = IdType.AUTO)
|
||||
private Long id;
|
||||
private Long accountId;
|
||||
private String path;
|
||||
private LocalDateTime dataTime;
|
||||
private String category;
|
||||
private String style;
|
||||
private Long workspaceRelStyleId;
|
||||
private Long projectId;
|
||||
private Long designItemId;
|
||||
|
||||
}
|
||||
74
src/main/java/com/ai/da/model/dto/ContestantDTO.java
Normal file
74
src/main/java/com/ai/da/model/dto/ContestantDTO.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* Contestant request DTO for Global Award
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "参赛者信息", description = "全球奖项大赛参赛者信息数据传输对象")
|
||||
public class ContestantDTO {
|
||||
@ApiModelProperty(value = "邮箱地址", required = true, example = "user@example.com")
|
||||
private String email;
|
||||
|
||||
@ApiModelProperty(value = "名字", required = true, example = "John")
|
||||
private String firstName;
|
||||
|
||||
@ApiModelProperty(value = "姓氏", required = true, example = "Doe")
|
||||
private String lastName;
|
||||
|
||||
@ApiModelProperty(value = "性别", required = true, example = "Male", allowableValues = "Male,Female,Other")
|
||||
private String gender;
|
||||
|
||||
@ApiModelProperty(value = "职业", required = true, example = "Designer")
|
||||
private String occupation;
|
||||
|
||||
@ApiModelProperty(value = "年龄", required = true, example = "25")
|
||||
private Integer age;
|
||||
|
||||
@ApiModelProperty(value = "国家/地区/城市", required = true, example = "China/Shanghai/Shanghai")
|
||||
private String countryRegionCity;
|
||||
|
||||
@ApiModelProperty(value = "电话号码", required = true, example = "+86 138 0000 0000")
|
||||
private String phoneNumber;
|
||||
|
||||
@ApiModelProperty(value = "作品集链接", required = false, example = "https://portfolio.example.com")
|
||||
private String portfolioUrl;
|
||||
|
||||
@ApiModelProperty(value = "设计作品标题", required = true, example = "Modern Office Building Design")
|
||||
private String designTitle;
|
||||
|
||||
@ApiModelProperty(value = "设计作品描述", required = true, example = "A modern office building design featuring sustainable materials...")
|
||||
private String designDescription;
|
||||
|
||||
@ApiModelProperty(value = "PDF文件路径", required = false, example = "contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String pdfPath;
|
||||
|
||||
@ApiModelProperty(value = "视频文件路径", required = false, example = "contestants/user@example.com/2024/01/video_1234567890.mp4")
|
||||
private String videoPath;
|
||||
|
||||
@ApiModelProperty(value = "视频时长(秒)", required = false, example = "120")
|
||||
private Integer videoDuration;
|
||||
|
||||
@ApiModelProperty(value = "视频大小(字节)", required = false, example = "10485760")
|
||||
private Long videoSize;
|
||||
|
||||
@ApiModelProperty(value = "PDF 文件大小(字节)", required = false, example = "524288")
|
||||
private Long pdfSize;
|
||||
|
||||
// /**
|
||||
// * 是否确认覆盖已存在记录(false 表示发现已有记录时仅返回 existingRecord,不覆盖)
|
||||
// */
|
||||
// @ApiModelProperty(value = "是否确认覆盖已存在记录", required = false, example = "false")
|
||||
// private Boolean confirm = false;
|
||||
|
||||
@NotBlank
|
||||
private String secureToken;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 参赛者文件导出请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "参赛者文件导出请求", description = "用于导出指定范围的参赛者文件")
|
||||
public class ContestantExportRequest {
|
||||
|
||||
@ApiModelProperty(value = "最小参赛者编号", required = true, example = "10000")
|
||||
private Integer minContestantNumber;
|
||||
|
||||
@ApiModelProperty(value = "最大参赛者编号", required = true, example = "10010")
|
||||
private Integer maxContestantNumber;
|
||||
}
|
||||
@@ -43,6 +43,10 @@ public class DesignSingleIncludeLayersDTO implements Serializable {
|
||||
@Schema(description = "项目id")
|
||||
private Long projectId;
|
||||
|
||||
@NotBlank(message = "designType cannot be empty")
|
||||
@Schema(description = "default -> 新增sketch || merge")
|
||||
private String designType;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DesignSingleIncludeLayersDTO{" +
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.Gradient;
|
||||
@@ -67,4 +68,18 @@ public class DesignSingleItemDTO implements Serializable {
|
||||
|
||||
private PartialDesignDTO partialDesign;
|
||||
|
||||
@Schema(description = "镜像模式 ")
|
||||
private int[] transpose;
|
||||
|
||||
@Schema(description = "45")
|
||||
private double rotate;
|
||||
|
||||
@Hidden
|
||||
@Schema(description = "带overall印花未分割图片")
|
||||
private String undividedLayerBase64;
|
||||
|
||||
@Hidden
|
||||
@Schema(description = "带overall/single印花未分割图片")
|
||||
private String undividedLayerWithSinglePrintBase64;
|
||||
|
||||
}
|
||||
|
||||
55
src/main/java/com/ai/da/model/dto/ImageProcessRequest.java
Normal file
55
src/main/java/com/ai/da/model/dto/ImageProcessRequest.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 图片处理请求体
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class ImageProcessRequest {
|
||||
|
||||
/**
|
||||
* OSS桶名(bucket_name)
|
||||
*/
|
||||
private String bucket_name;
|
||||
|
||||
/**
|
||||
* OSS对象名(object_name)
|
||||
*/
|
||||
private String object_name;
|
||||
|
||||
/**
|
||||
* 输入图片路径列表(input_image_paths)
|
||||
*/
|
||||
private List<String> input_image_paths;
|
||||
|
||||
/**
|
||||
* 图像宽度(width)
|
||||
*/
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 图像高度(height)
|
||||
*/
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 文本提示(prompt)
|
||||
*/
|
||||
private String prompt;
|
||||
|
||||
/**
|
||||
* 推理步数(steps)
|
||||
*/
|
||||
private Integer steps;
|
||||
|
||||
/**
|
||||
* 引导系数(guidance)
|
||||
*/
|
||||
private Double guidance;
|
||||
|
||||
}
|
||||
@@ -20,17 +20,17 @@ public class PartialDesignDTO implements Serializable {
|
||||
@Schema(description = "图片的base64格式")
|
||||
private String partialDesignBase64;
|
||||
|
||||
@Schema(description = "图层信息")
|
||||
private List<String> layers;
|
||||
/* @Schema(description = "图层信息")
|
||||
private List<String> layers;*/
|
||||
|
||||
public PartialDesignDTO(String partialDesignMinioPath) {
|
||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||
}
|
||||
|
||||
public PartialDesignDTO(String partialDesignMinioPath, List<String> layers) {
|
||||
/* public PartialDesignDTO(String partialDesignMinioPath, List<String> layers) {
|
||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||
this.layers = layers;
|
||||
}
|
||||
}*/
|
||||
|
||||
public PartialDesignDTO(String partialDesignMinioPath, String partialDesignPath) {
|
||||
this.partialDesignMinioPath = partialDesignMinioPath;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -24,6 +25,7 @@ public class ProductPurchaseDTO {
|
||||
@Schema(description = "EcoMonth || Month || Year")
|
||||
private String subscribeType;
|
||||
|
||||
@Hidden
|
||||
@Schema(description = "是否自动续订 one_time || recurring")
|
||||
private Boolean autoRenewal;
|
||||
|
||||
|
||||
@@ -48,4 +48,7 @@ public class SubscriptionPlanDTO {
|
||||
@Schema(description = "订阅计划状态")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String CountryOrRegion;
|
||||
|
||||
}
|
||||
|
||||
@@ -12,9 +12,12 @@ public class SubscriptionPlanPageQuery extends QueryPageByTimeDTO {
|
||||
@Schema(description = "组织id")
|
||||
private Long organizationId;
|
||||
|
||||
@Schema(description = "管理id")
|
||||
@Schema(description = "管理员id")
|
||||
private Long adminAccId;
|
||||
|
||||
@Schema(description = "状态 PENDING||ACTIVE||EXPIRED")
|
||||
private List<String> status;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String countryOrRegion;
|
||||
}
|
||||
|
||||
@@ -32,4 +32,7 @@ public class UpdateSubscriptionPlanDTO {
|
||||
@Schema(description = "订阅重命名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String countryOrRegion;
|
||||
|
||||
}
|
||||
|
||||
33
src/main/java/com/ai/da/model/dto/UploadChunkResponse.java
Normal file
33
src/main/java/com/ai/da/model/dto/UploadChunkResponse.java
Normal file
@@ -0,0 +1,33 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 分片上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "分片上传响应", description = "单个文件分片上传成功的响应数据")
|
||||
public class UploadChunkResponse {
|
||||
|
||||
/**
|
||||
* 分片索引
|
||||
*/
|
||||
@ApiModelProperty(value = "分片索引(从0开始)", required = true, example = "0")
|
||||
private Integer chunkIndex;
|
||||
|
||||
/**
|
||||
* 是否上传成功
|
||||
*/
|
||||
@ApiModelProperty(value = "是否上传成功", required = true, example = "true")
|
||||
private Boolean uploaded;
|
||||
|
||||
/**
|
||||
* 分片大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "分片大小(字节)", required = true, example = "1048576")
|
||||
private Long size;
|
||||
}
|
||||
53
src/main/java/com/ai/da/model/dto/UploadCompleteRequest.java
Normal file
53
src/main/java/com/ai/da/model/dto/UploadCompleteRequest.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
/**
|
||||
* 完成上传请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "完成上传请求", description = "文件上传完成时使用的请求参数")
|
||||
public class UploadCompleteRequest {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@NotBlank(message = "上传任务ID不能为空")
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
@ApiModelProperty(value = "原始文件名", required = true, example = "design.pdf")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件总大小(字节)
|
||||
*/
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Positive(message = "文件大小必须大于0")
|
||||
@ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760")
|
||||
private Long totalSize;
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
@NotBlank(message = "用户邮箱不能为空")
|
||||
@ApiModelProperty(value = "用户邮箱", required = true, example = "user@example.com")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 安全令牌(邮箱验证令牌)
|
||||
*/
|
||||
@NotBlank(message = "安全令牌不能为空")
|
||||
@ApiModelProperty(value = "安全令牌", required = true, example = "abc123def456")
|
||||
private String secureToken;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 完成上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "完成上传响应", description = "文件上传完成并合并成功的响应数据")
|
||||
public class UploadCompleteResponse {
|
||||
|
||||
/**
|
||||
* 文件在MinIO中的路径
|
||||
*/
|
||||
@ApiModelProperty(value = "文件在MinIO中的存储路径", required = true, example = "contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String filePath;
|
||||
|
||||
/**
|
||||
* 文件的完整URL
|
||||
*/
|
||||
@ApiModelProperty(value = "文件的完整访问URL", required = true, example = "https://minio.example.com/contestants/user@example.com/2024/01/design_1234567890.pdf")
|
||||
private String fileUrl;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "文件大小(字节)", required = true, example = "10485760")
|
||||
private Long fileSize;
|
||||
}
|
||||
53
src/main/java/com/ai/da/model/dto/UploadInitRequest.java
Normal file
53
src/main/java/com/ai/da/model/dto/UploadInitRequest.java
Normal file
@@ -0,0 +1,53 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Data;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
|
||||
/**
|
||||
* 初始化上传请求DTO
|
||||
*/
|
||||
@Data
|
||||
@ApiModel(value = "初始化上传请求", description = "文件上传初始化时使用的请求参数")
|
||||
public class UploadInitRequest {
|
||||
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
@NotBlank(message = "文件名不能为空")
|
||||
@ApiModelProperty(value = "文件名", required = true, example = "design.pdf")
|
||||
private String fileName;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
@NotNull(message = "文件大小不能为空")
|
||||
@Positive(message = "文件大小必须大于0")
|
||||
@ApiModelProperty(value = "文件大小(字节)", required = true, example = "10485760")
|
||||
private Long fileSize;
|
||||
|
||||
/**
|
||||
* 文件类型(MIME类型)
|
||||
*/
|
||||
@NotBlank(message = "文件类型不能为空")
|
||||
@ApiModelProperty(value = "文件类型(MIME类型)", required = true, example = "application/pdf")
|
||||
private String fileType;
|
||||
|
||||
|
||||
/**
|
||||
* 用户邮箱
|
||||
*/
|
||||
@ApiModelProperty(value = "用户邮箱", required = true, example = "user@example.com")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 安全令牌(邮箱验证令牌)
|
||||
*/
|
||||
@NotBlank(message = "安全令牌不能为空")
|
||||
@ApiModelProperty(value = "安全令牌", required = true, example = "abc123def456")
|
||||
private String secureToken;
|
||||
}
|
||||
41
src/main/java/com/ai/da/model/dto/UploadInitResponse.java
Normal file
41
src/main/java/com/ai/da/model/dto/UploadInitResponse.java
Normal file
@@ -0,0 +1,41 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 初始化上传响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "初始化上传响应", description = "文件上传初始化成功的响应数据")
|
||||
public class UploadInitResponse {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 分片大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "每个分片的大小(字节)", required = true, example = "1048576")
|
||||
private Integer chunkSize;
|
||||
|
||||
/**
|
||||
* 总分片数
|
||||
*/
|
||||
@ApiModelProperty(value = "文件被分成多少个分片", required = true, example = "10")
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 任务过期时间
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务过期时间", required = true, example = "2024-01-20T10:30:00")
|
||||
private LocalDateTime expiresAt;
|
||||
}
|
||||
59
src/main/java/com/ai/da/model/dto/UploadStatusResponse.java
Normal file
59
src/main/java/com/ai/da/model/dto/UploadStatusResponse.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 上传状态响应DTO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@ApiModel(value = "上传状态响应", description = "查询上传任务当前状态的响应数据")
|
||||
public class UploadStatusResponse {
|
||||
|
||||
/**
|
||||
* 上传任务ID
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务唯一标识", required = true, example = "550e8400-e29b-41d4-a716-446655440000")
|
||||
private String uploadId;
|
||||
|
||||
/**
|
||||
* 上传状态
|
||||
*/
|
||||
@ApiModelProperty(value = "上传任务状态", required = true, example = "uploading", allowableValues = "initiated,uploading,completed,failed,expired")
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 上传进度百分比 (0-100)
|
||||
*/
|
||||
@ApiModelProperty(value = "上传进度百分比(0-100)", required = true, example = "60.0")
|
||||
private Double progress;
|
||||
|
||||
/**
|
||||
* 已上传分片索引集合
|
||||
*/
|
||||
@ApiModelProperty(value = "已上传分片的索引集合", required = true, example = "[0,1,2,3,4]")
|
||||
private Set<Integer> uploadedChunks;
|
||||
|
||||
/**
|
||||
* 总分片数
|
||||
*/
|
||||
@ApiModelProperty(value = "文件被分成多少个分片", required = true, example = "10")
|
||||
private Integer totalChunks;
|
||||
|
||||
/**
|
||||
* 文件总大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "文件总大小(字节)", required = true, example = "10485760")
|
||||
private Long totalSize;
|
||||
|
||||
/**
|
||||
* 已上传大小(字节)
|
||||
*/
|
||||
@ApiModelProperty(value = "已上传的数据大小(字节)", required = true, example = "6291456")
|
||||
private Long uploadedSize;
|
||||
}
|
||||
16
src/main/java/com/ai/da/model/vo/CheckOTPVO.java
Normal file
16
src/main/java/com/ai/da/model/vo/CheckOTPVO.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CheckOTPVO {
|
||||
|
||||
private String secureToken;
|
||||
|
||||
private ContestantDTO contestantDTO;
|
||||
}
|
||||
17
src/main/java/com/ai/da/model/vo/ContestantCountVO.java
Normal file
17
src/main/java/com/ai/da/model/vo/ContestantCountVO.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ContestantCountVO {
|
||||
|
||||
private Long count;
|
||||
|
||||
private Integer maxContestantNumber;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.Gradient;
|
||||
@@ -62,11 +62,11 @@ public class DesignItemClothesDetailVO {
|
||||
@Schema(description = "渐变色信息")
|
||||
private Gradient gradient;
|
||||
|
||||
@Schema(description = "未分割的图层")
|
||||
/* @Schema(description = "未分割的图层")
|
||||
private String undividedLayer;
|
||||
|
||||
@Schema(description = "添加single印花的未分割的图层")
|
||||
private String undividedLayerWithSinglePrint;
|
||||
private String undividedLayerWithSinglePrint;*/
|
||||
|
||||
@Schema(description = "局部design")
|
||||
private PartialDesignDTO partialDesign;
|
||||
|
||||
@@ -59,4 +59,16 @@ public class DesignPythonOutfitVO {
|
||||
* 图层优先级 从10开始,优先级数字越大越靠近上层
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 镜像模式
|
||||
*/
|
||||
@Schema(description = "镜像模式")
|
||||
private int[] transpose;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
@Schema(description = "旋转角度")
|
||||
private Double rotate;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ public class DesignSinglePrint implements Serializable {
|
||||
@Schema(description = "印花优先级")
|
||||
private Integer priority;
|
||||
|
||||
private String object;
|
||||
|
||||
public DesignSinglePrint() {
|
||||
}
|
||||
|
||||
|
||||
23
src/main/java/com/ai/da/model/vo/PageVisitCountVO.java
Normal file
23
src/main/java/com/ai/da/model/vo/PageVisitCountVO.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PageVisitCountVO {
|
||||
|
||||
/**
|
||||
* 每次访问或刷新都计一次(不去重)
|
||||
*/
|
||||
private Long rawVisitCount;
|
||||
|
||||
/**
|
||||
* 5秒内刷新只算一次(去重后的真实访客数)
|
||||
*/
|
||||
private Long uniqueVisitCount;
|
||||
}
|
||||
@@ -34,4 +34,8 @@ public class QueryUserConditionsVO extends PageQueryBaseVo {
|
||||
|
||||
private Integer systemUser;
|
||||
|
||||
private Long subscriptionPlanId;
|
||||
|
||||
private Long organizationId;
|
||||
|
||||
}
|
||||
|
||||
@@ -45,4 +45,7 @@ public class SubscriptionPlanVO {
|
||||
@Schema(description = "命名")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "国家或地区")
|
||||
private String countryOrRegion;
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ public class DesignPythonBasic {
|
||||
|
||||
private String single_overall;
|
||||
|
||||
private String preview_submit;
|
||||
// private String preview_submit;
|
||||
|
||||
private String switch_category;
|
||||
/**
|
||||
@@ -40,4 +40,7 @@ public class DesignPythonBasic {
|
||||
|
||||
private Boolean layer_order = Boolean.FALSE;
|
||||
|
||||
// default | merge
|
||||
private String design_type = "default";
|
||||
|
||||
}
|
||||
|
||||
@@ -87,6 +87,22 @@ public class DesignPythonItem {
|
||||
*/
|
||||
private String seg_mask_url;
|
||||
|
||||
/**
|
||||
* 镜像模式
|
||||
*/
|
||||
private int[] transpose;
|
||||
|
||||
/**
|
||||
* 旋转角度
|
||||
*/
|
||||
private double rotate;
|
||||
|
||||
/**
|
||||
* 前端处理了print之后的结果图,python对该图进行分割
|
||||
* designType为merge时,该字段必须有值,否则会导致python端没有数据返回
|
||||
*/
|
||||
private String merge_image_path;
|
||||
|
||||
public static List<String> OUTWEAR_DRESS_BLOUSE = Arrays.asList(CollectionLevel2TypeEnum.OUTWEAR.getRealName(),
|
||||
CollectionLevel2TypeEnum.DRESS.getRealName(), CollectionLevel2TypeEnum.BLOUSE.getRealName());
|
||||
|
||||
@@ -143,7 +159,8 @@ public class DesignPythonItem {
|
||||
|
||||
public DesignPythonItem(String type, String path, String color, PrintToPython print, Long businessId,
|
||||
Long image_id, List<Long> offset, Float[] resize_scale, Integer priority, String gradient,
|
||||
String gradientString, String seg_mask_url) {
|
||||
String gradientString, String seg_mask_url, int[] transpose, double rotate,
|
||||
String merge_image_path) {
|
||||
this.type = type;
|
||||
this.path = path;
|
||||
this.color = color;
|
||||
@@ -157,6 +174,9 @@ public class DesignPythonItem {
|
||||
this.gradient = gradient;
|
||||
this.gradientString = gradientString;
|
||||
this.seg_mask_url = seg_mask_url;
|
||||
this.transpose = transpose;
|
||||
this.rotate = rotate;
|
||||
this.merge_image_path = merge_image_path;
|
||||
}
|
||||
|
||||
public DesignPythonItem(String type, String path, String color, PrintToPython print, String icon, Long businessId, Long image_id) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.ai.da.python.vo;
|
||||
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class DesignPythonItemPrint {
|
||||
@@ -53,7 +53,7 @@ public class DesignPythonItemPrint {
|
||||
if (ifDesign){
|
||||
this.print_path_list = print_path_list;
|
||||
this.location = Collections.singletonList(Arrays.asList(0.0f, 0.0f));
|
||||
this.print_scale_list = Collections.singletonList(Arrays.asList(0.0f, 0.0f));
|
||||
this.print_scale_list = Collections.singletonList(Arrays.asList(1.0f, 1.0f));
|
||||
this.print_angle_list = Arrays.asList(0.0, 0.0);
|
||||
}
|
||||
|
||||
|
||||
@@ -246,4 +246,6 @@ public interface AccountService extends IService<Account> {
|
||||
void setEduAdminToExpire(Account adminAccount);
|
||||
|
||||
String getOrganizationTypeByRole(Integer roleNum);
|
||||
|
||||
void validateUserValidaExpire(Account account);
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ public interface ConvenientInquiryService extends IService<Questionnaire> {
|
||||
|
||||
IPage<Account> getUserInfo(QueryUserConditionsVO queryUserConditionsVO);
|
||||
|
||||
List<Map<String, Object>> getAllUserIdList();
|
||||
IPage<Map<String, Object>> getAllUserIdList(Integer pageNum, Integer pageSize, String email);
|
||||
|
||||
PageBaseResponse<PaymentInfoVO> queryTransactionRecords(QueryPaymentInfoDTO queryPaymentInfoDTO);
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ public interface DesignItemService extends IService<DesignItem> {
|
||||
|
||||
DesignSingleVO designSingleIncludeLayers(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO);
|
||||
|
||||
Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers);
|
||||
Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers, DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO);
|
||||
|
||||
Map<String, String> setTypeAndUndividedLayer(JSONArray layers);
|
||||
|
||||
|
||||
68
src/main/java/com/ai/da/service/GlobalAwardService.java
Normal file
68
src/main/java/com/ai/da/service/GlobalAwardService.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.ai.da.service;
|
||||
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.model.vo.PageVisitCountVO;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface GlobalAwardService {
|
||||
String uploadPdf(MultipartFile file, String email) throws Exception;
|
||||
|
||||
String uploadVideo(MultipartFile file, String email) throws Exception;
|
||||
|
||||
Map<String, Object> saveContestant(ContestantDTO request);
|
||||
|
||||
com.ai.da.model.dto.ContestantDTO getContestantByID(String email);
|
||||
|
||||
void checkEmail(String email);
|
||||
|
||||
CheckOTPVO checkCode(String email, String otp);
|
||||
|
||||
void checkSecurityToken(String email, String securityToken);
|
||||
|
||||
/**
|
||||
* 导出参赛者列表为 Excel(二进制)
|
||||
* @return Excel 文件的字节数组
|
||||
*/
|
||||
byte[] exportContestants() throws Exception;
|
||||
|
||||
/**
|
||||
* 将参赛者列表导出并保存到服务端本地目录(使用服务配置的 uploadDir/exports)
|
||||
*/
|
||||
void saveContestantsToLocal() throws Exception;
|
||||
|
||||
/**
|
||||
* 将参赛者文件打包为 ZIP 并返回字节数组(不落盘,直接响应给浏览器)
|
||||
* @param minContestantNumber 最小参赛者编号
|
||||
* @param maxContestantNumber 最大参赛者编号
|
||||
* @return ZIP 文件的字节数组
|
||||
*/
|
||||
byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception;
|
||||
|
||||
/**
|
||||
* 查询参赛者总数和最大参赛者编号
|
||||
* @return 参赛者数量和最大参赛者编号
|
||||
*/
|
||||
ContestantCountVO getContestantCount();
|
||||
|
||||
/**
|
||||
* 记录比赛页面的访问量
|
||||
* <ul>
|
||||
* <li>rawVisitCount: 每次访问或刷新都计一次(不去重)</li>
|
||||
* <li>uniqueVisitCount: 5秒内刷新只算一次(基于会话去重)</li>
|
||||
* </ul>
|
||||
* @param sessionId 会话ID(用于5秒去重判断)
|
||||
*/
|
||||
void recordPageVisit(String sessionId);
|
||||
|
||||
/**
|
||||
* 获取比赛页面的两种访问量
|
||||
* @return 原始访问量和去重访问量
|
||||
*/
|
||||
PageVisitCountVO getPageVisitCount();
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public interface OrderInfoService extends IService<OrderInfo> {
|
||||
OrderInfo createOrderByProductId(Integer productId, String paymentType, HttpServletRequest request);
|
||||
|
||||
OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
||||
HttpServletRequest request, byte autoRenewal);
|
||||
HttpServletRequest request);
|
||||
|
||||
void saveCodeUrl(String orderNo, String codeUrl);
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.paypal.orders.Order;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.PaymentMethod;
|
||||
import com.stripe.model.checkout.Session;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -23,9 +25,15 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
||||
|
||||
void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO, String type);
|
||||
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice);
|
||||
void createOrUpdatePaymentInfoForStripe(Session session);
|
||||
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge);
|
||||
Map<String, String> getPaymentMethodInfo(String sessionId, String subscriptionId);
|
||||
|
||||
PaymentMethod getPaymentMethodBySubscriptionId(String subscriptionId);
|
||||
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice, Map<String, String> paymentMethodInfo, List<Session.Discount> discounts);
|
||||
|
||||
// PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge);
|
||||
|
||||
List<PaymentInfo> getPaymentInfoByOrderNo(String orderId, String order);
|
||||
|
||||
@@ -35,5 +43,9 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
||||
|
||||
List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode);
|
||||
|
||||
PaymentInfo updatePaymentRefundStatus(Charge charge);
|
||||
// PaymentInfo updatePaymentRefundStatus(Charge charge);
|
||||
|
||||
void updatePaymentRefundStatusByChargeId(Charge charge, String status);
|
||||
|
||||
void updatePaymentRefundStatusByInvoiceId(String invoiceId, String status);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ public interface RabbitMQService {
|
||||
|
||||
void publishMessageToGenerate(String message);
|
||||
|
||||
void publishMessageToGenerateResult(String message);
|
||||
|
||||
void publishMessageToSR(String message);
|
||||
|
||||
Integer getMessageCount(String queueUrl);
|
||||
|
||||
@@ -24,10 +24,18 @@ public interface RefundInfoService extends IService<RefundInfo> {
|
||||
|
||||
List<RefundInfo> getByChargeId(String chargeId);
|
||||
|
||||
RefundInfo getByRefundId(String refundId);
|
||||
|
||||
RefundInfo createRefundForStripe(Refund refund);
|
||||
|
||||
RefundInfo updateRefundStatusForStripe(Refund refund);
|
||||
|
||||
RefundInfo updateRefundForStripe(Charge charge);
|
||||
|
||||
RefundInfo handleRefundCreated(Refund refund);
|
||||
|
||||
RefundInfo handleRefundSucceeded(Refund refund);
|
||||
|
||||
RefundInfo handleRefundFailed(Refund refund);
|
||||
|
||||
}
|
||||
|
||||
@@ -21,42 +21,20 @@ public interface StripeService {
|
||||
|
||||
SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId);
|
||||
|
||||
String refund(String amount, String orderId, String reason);
|
||||
|
||||
void checkOrderStatus(String orderNo);
|
||||
|
||||
List<String> getSubscriptionIds(String name, String userEmail) throws StripeException;
|
||||
|
||||
Map<String, String> getPaymentMethodByInvoiceId(String invoiceId);
|
||||
|
||||
void cancelSubscription(String orderNo, String cancelReason);
|
||||
|
||||
void cancelSubscriptionTemp(String subscriptionId);
|
||||
|
||||
Map<String, String> getPaymentMethod(String paymentMethodId);
|
||||
|
||||
boolean sendEmail(String subscriptionId, String type, String orderNo);
|
||||
|
||||
String getLanguage(String language, String country, String type);
|
||||
|
||||
/*void updateSubscription(String subscriptionId);
|
||||
|
||||
void resume(String subscriptionId);*/
|
||||
|
||||
// void subscriptionReminder();
|
||||
|
||||
void checkSubscriptionExpiration();
|
||||
|
||||
String createSubscriptionTemp(String name, String email);
|
||||
|
||||
String changeCustomerPayment(String name, String email);
|
||||
|
||||
boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo);
|
||||
|
||||
List<Map<String,String>> getCustomerPaymentMethod(String name, String email);
|
||||
|
||||
String detachCustomerAllPaymentMethod(String name, String email);
|
||||
|
||||
// Map getIp(HttpServletRequest request);
|
||||
|
||||
String getStackTrace(Exception e, int maxLines);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.ai.da.service;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.stripe.model.Subscription;
|
||||
|
||||
/**
|
||||
* Stripe 订阅服务接口
|
||||
*
|
||||
* 订阅事件处理已迁移至策略处理器:
|
||||
* - customer.subscription.created -> CheckoutSessionCompletedHandler
|
||||
* - customer.subscription.updated -> SubscriptionUpdatedHandler
|
||||
* - customer.subscription.deleted -> SubscriptionDeletedHandler
|
||||
* - customer.subscription.trial_will_end -> SubscriptionUpdatedHandler
|
||||
*
|
||||
* 本接口中保留需要被其他组件调用的辅助方法
|
||||
*/
|
||||
public interface StripeSubscriptionService {
|
||||
|
||||
/**
|
||||
* 发送订阅相关邮件
|
||||
* @param subscription Stripe Subscription object (may be null)
|
||||
* @param type 邮件类型
|
||||
* @param orderNo 订单号
|
||||
* @param passedSubscriptionInfo 本地订阅记录 (用于避免事务未提交时重新查询,可为空)
|
||||
*/
|
||||
boolean sendSubscriptionEmail(Subscription subscription, String type, String orderNo,
|
||||
com.ai.da.mapper.primary.entity.SubscriptionInfo passedSubscriptionInfo);
|
||||
|
||||
/**
|
||||
* 发送首次订阅失败邮件
|
||||
*/
|
||||
void sendFailedNewOrderEmail(String orderNo);
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
void cancelSubscription(String subscriptionId, String cancelReason, Long accountId);
|
||||
|
||||
/**
|
||||
* 发送续费失败邮件
|
||||
*/
|
||||
// void sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo);
|
||||
//
|
||||
// /**
|
||||
// * 获取用户最新的订阅信息
|
||||
// */
|
||||
// SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId);
|
||||
//
|
||||
// /**
|
||||
// * 更新订阅取消原因
|
||||
// */
|
||||
// void updateCancelReason(String subscriptionId, String reason);
|
||||
//
|
||||
// /**
|
||||
// * 创建或更新订阅信息
|
||||
// */
|
||||
// SubscriptionInfo createOrUpdateSubscriptionInfo(Subscription subscription);
|
||||
}
|
||||
13
src/main/java/com/ai/da/service/StripeWebhookService.java
Normal file
13
src/main/java/com/ai/da/service/StripeWebhookService.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package com.ai.da.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
public interface StripeWebhookService {
|
||||
|
||||
/**
|
||||
* 处理 Stripe webhook 回调
|
||||
* @param request HTTP 请求
|
||||
* @return true=处理成功(返回200),false=处理失败(返回500,Stripe会重试)
|
||||
*/
|
||||
Boolean notify(HttpServletRequest request);
|
||||
}
|
||||
@@ -26,7 +26,7 @@ public interface SubscriptionPlanService extends IService<SubscriptionPlan> {
|
||||
|
||||
void switchSubAccSubscriptionPlan(Long subscriptionPlanId, Long subAccId);
|
||||
|
||||
void activeSubscriptionPlan();
|
||||
void activeSubscriptionPlan(Long planId);
|
||||
|
||||
void expireSubscription();
|
||||
}
|
||||
|
||||
@@ -39,9 +39,10 @@ import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -83,6 +84,9 @@ import java.util.stream.Collectors;
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
|
||||
@Resource
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Resource
|
||||
private AccountMapper accountMapper;
|
||||
|
||||
@@ -240,12 +244,13 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
AccountLoginVO response = CopyUtil.copyObject(account, AccountLoginVO.class);
|
||||
response.setEmail(account.getUserEmail());
|
||||
String token = LocalCacheUtils.getTokenCache(String.valueOf(account.getId()));
|
||||
if (StringUtils.isNotBlank(token)) {
|
||||
/*if (StringUtils.isNotBlank(token)) {
|
||||
//用户已登入
|
||||
response.setToken(token);
|
||||
} else {
|
||||
response.setToken(createAccountToken(account));
|
||||
}
|
||||
}*/
|
||||
response.setToken(createAccountToken(account));
|
||||
response.setUserId(account.getId());
|
||||
response.setSystemUser(account.getSystemUser());
|
||||
// 设置头像
|
||||
@@ -279,7 +284,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
// 定义常量(临时)
|
||||
private static final Integer SYSTEM_USER_TYPE_EDU_ADMIN = 7;
|
||||
|
||||
private void validateUserValidaExpire(Account account) {
|
||||
public void validateUserValidaExpire(Account account) {
|
||||
Long currentTime = new Date().getTime();
|
||||
if (account.getSystemUser().equals(0)) {
|
||||
return;
|
||||
@@ -297,7 +302,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if (isEduAdmin) {
|
||||
setEduAdminToExpire(account);
|
||||
} else {
|
||||
toVisitor(account);
|
||||
// 这里调用代理的 toVisitor 方法
|
||||
AccountService proxy = applicationContext.getBean(AccountService.class);
|
||||
proxy.toVisitor(account);
|
||||
// return;
|
||||
throw new BusinessException("account.expired", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
@@ -1639,6 +1646,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
log.warn("当前用户 {} 在AiDA中没有账号", email);
|
||||
throw new BusinessException("user.has.no.account", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
// 解决循环依赖问题
|
||||
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
||||
// 2、先判断当前用户是否已经填写过问卷
|
||||
CreditsDetail record = creditsService.getByAccountIdAndChangeEvent(account.getId(), "Fill out the questionnaire", "+100");
|
||||
@@ -1949,6 +1957,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
return baseMapper.selectList(queryWrapper);
|
||||
}
|
||||
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void toVisitor(Account account) {
|
||||
accountMapper.toVisitor(account.getId());
|
||||
}
|
||||
@@ -2507,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());
|
||||
}
|
||||
|
||||
@@ -3374,12 +3383,14 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
Account account = accountMapper.selectById(accountId);
|
||||
if (!Objects.isNull(account.getValidEndTime())
|
||||
&& account.getValidEndTime().equals(currentPeriodEnd * 1000)) {
|
||||
log.info("accountId:{}未更新账号有效期。current validEnd:{}, new validEnd:{}", accountId, account.getValidEndTime(), currentPeriodEnd);
|
||||
return false;
|
||||
} else {
|
||||
account.setValidEndTime(currentPeriodEnd * 1000);
|
||||
accountMapper.updateById(account);
|
||||
log.info("accountId:{} 将账号有效期更新到 {}", accountId, currentPeriodEnd);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -3392,34 +3403,36 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
if (description.equals(ProductEnum.DailySubscription.getName())) {
|
||||
productCredits = ProductEnum.DailySubscription.getCredits();
|
||||
account.setSystemUser(3);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_WEEKLY.getValue())));
|
||||
} else if (description.equals(ProductEnum.MonthlySubscription.getName())) {
|
||||
productCredits = ProductEnum.MonthlySubscription.getCredits();
|
||||
account.setSystemUser(2);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_MONTHLY.getValue())));
|
||||
} else if (description.equals(ProductEnum.Eco_MonthlySubscription.getName())) {
|
||||
productCredits = ProductEnum.Eco_MonthlySubscription.getCredits();
|
||||
account.setSystemUser(2);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_MONTHLY_ECO.getValue())));
|
||||
} else if (description.equals(ProductEnum.AnnualSubscription.getName())) {
|
||||
productCredits = ProductEnum.AnnualSubscription.getCredits();
|
||||
account.setSystemUser(1);
|
||||
account.setCredits(BigDecimal.valueOf(Long.parseLong(CreditsEventsEnum.INIT_YEARLY.getValue())));
|
||||
} else {
|
||||
log.error("未知订阅类型: {}", description);
|
||||
return;
|
||||
}
|
||||
account.setCredits(BigDecimal.valueOf(productCredits));
|
||||
accountMapper.updateById(account);
|
||||
log.info("accountId:{},更新用户角色为{},总积分为{}", accountId, account.getSystemUser(), productCredits);
|
||||
|
||||
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
||||
// 先判断是否已添加添加积分变更记录
|
||||
CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo);
|
||||
// 添加积分变更记录(订单续订时的积分变更也需要记录)
|
||||
creditsService.insertToCreditsDetail(accountId,
|
||||
description + "--Stripe",
|
||||
String.valueOf(productCredits),
|
||||
"set", orderNo);
|
||||
/*CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(orderNo);
|
||||
if (Objects.isNull(creditsDetail)) {
|
||||
creditsService.insertToCreditsDetail(accountId,
|
||||
description + "--Stripe",
|
||||
String.valueOf(productCredits),
|
||||
"positive", orderNo);
|
||||
}
|
||||
}*/
|
||||
} else {
|
||||
log.error("orderNo: {} 无法找到对应的记录", orderNo);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
@@ -80,9 +79,7 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
affiliate.setPromotionMethod(promotionMethod);
|
||||
baseMapper.insert(affiliate);
|
||||
// 邮件通知审批者
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||
}else {
|
||||
@@ -440,9 +437,7 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
affiliateEmailParamsDTO.setUnpaidEarnings(String.valueOf(unpaidCommission));
|
||||
affiliateEmailParamsDTO.setPaidEarnings(String.valueOf(paidCommission));
|
||||
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
// 邮件通知
|
||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary");
|
||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), affiliateEmailParamsDTO, "summary");
|
||||
@@ -607,4 +602,8 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
coupon.setUnpaidCommission(unpaidCommission);
|
||||
}
|
||||
|
||||
private String[] buildMerchantReceiverEmail() {
|
||||
return SendEmailUtil.buildMerchantReceiverEmail();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -520,14 +520,16 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
||||
.filter(f -> f.getDesignType().equals(DesignTypeEnum.COLLECTION.getRealName()))
|
||||
.map(DesignCollectionPrintElementDTO::getId)
|
||||
.collect(Collectors.toList());
|
||||
List<CollectionElement> printBoardElements = new ArrayList<>();
|
||||
elementVO.setPrintBoardElements(printBoardElements);
|
||||
if (!CollectionUtils.isEmpty(printBoardIds)) {
|
||||
// 从数据库批量查询printBoard元素
|
||||
List<CollectionElement> printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds);
|
||||
printBoardElements.addAll(collectionElementMapper.selectBatchIds(printBoardIds));
|
||||
// 验证查询结果的完整性
|
||||
if (CollectionUtil.isEmpty(printBoardElements) || printBoardElements.size() != printBoardIds.size()) {
|
||||
throw new BusinessException("get.printBoards.data.is.mismatch");
|
||||
}
|
||||
elementVO.setPrintBoardElements(printBoardElements);
|
||||
// elementVO.setPrintBoardElements(printBoardElements);
|
||||
usedElementIds.addAll(printBoardIds); // 记录已使用的元素ID
|
||||
}
|
||||
// 处理类型为LIBRARY的printBoard元素
|
||||
@@ -543,7 +545,8 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
||||
Map<Long, DesignCollectionPrintElementDTO> idToMap = designDTO.getPrintBoards()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v));
|
||||
libraryCollectionElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap));
|
||||
printBoardElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap));
|
||||
// libraryCollectionElements.addAll(covertLibrarysToPrintCollections(librarys, idToMap));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -559,7 +562,8 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
||||
Map<Long, DesignCollectionPrintElementDTO> idToMap = designDTO.getPrintBoards()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(DesignCollectionPrintElementDTO::getId, v -> v));
|
||||
generateCollectionElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap));
|
||||
printBoardElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap));
|
||||
// generateCollectionElements.addAll(covertGeneratesToPrintCollections(generateDetailList, idToMap));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.xssf.usermodel.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -149,7 +148,17 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
|| ADMIN_IDS.contains(account.getId())
|
||||
|| ADMIN_IDS_READ_ONLY.contains(account.getId())
|
||||
)) {
|
||||
if (StringUtil.isNullOrEmpty(startTime)) startTime = "2024-02-01 00:00:00";
|
||||
boolean filterBySecond ;
|
||||
if (StringUtil.isNullOrEmpty(startTime)) {
|
||||
startTime = "2024-02-01 00:00:00";
|
||||
filterBySecond = true;
|
||||
} else {
|
||||
LocalDateTime thresholdTime = LocalDateTime.of(2024, 5, 1, 0, 0, 0);
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(startTime, formatter);
|
||||
filterBySecond = startDateTime.isBefore(thresholdTime);
|
||||
}
|
||||
|
||||
if (StringUtil.isNullOrEmpty(endTime)) {
|
||||
// yyyy-MM-dd HH:mm:ss "HH"表示24小时制 "hh"表示12小时制
|
||||
endTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
|
||||
@@ -173,7 +182,7 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
default:
|
||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
return designMapper.getDesignStatistic(startTime, endTime, ids, email, role, account.getOrganizationName());
|
||||
return designMapper.getDesignStatistic(startTime, endTime, ids, email, role, account.getOrganizationName(), filterBySecond);
|
||||
} else {
|
||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
@@ -695,14 +704,19 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
|
||||
String username = UserContext.getUserHolder().getUsername();
|
||||
Account account = new Account();
|
||||
account.setId(accountId);
|
||||
// 修改用户有效期截止日期、用户类型、积分
|
||||
if (!Objects.isNull(validEndTime)) {
|
||||
account.setValidEndTime(validEndTime);
|
||||
log.info("管理员:{},修改用户 {} 信息,将账号到期时间置为:{}", username, accountId, validEndTime);
|
||||
}
|
||||
if (!Objects.isNull(systemUser)) {
|
||||
if (!Objects.isNull(systemUser) && !systemUser.equals(0)) {
|
||||
account.setSystemUser(systemUser);
|
||||
log.info("管理员:{},修改用户 {} 信息,将账号身份置为:{}", username, accountId, systemUser);
|
||||
} else if (systemUser.equals(0)){
|
||||
// 将用户身份设置为游客
|
||||
accountService.toVisitor(account);
|
||||
return true;
|
||||
}
|
||||
/*if (!StringUtils.isNullOrEmpty(systemUser)) {
|
||||
int systemUser = 0;
|
||||
@@ -728,7 +742,6 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
}
|
||||
|
||||
// todo 如果修改管理员账号的积分上限或子账号数量,则其所有子账号的积分上限需要重新计算
|
||||
account.setId(accountId);
|
||||
account.setUpdateDate(new Date());
|
||||
return accountMapper.updateById(account) == 1;
|
||||
// accountService.update(account,null);
|
||||
@@ -774,6 +787,14 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
queryWrapper.lt("create_date", queryUserConditionsVO.getEndTime());
|
||||
}
|
||||
|
||||
if (!Objects.isNull(queryUserConditionsVO.getSubscriptionPlanId())) {
|
||||
queryWrapper.eq("subscription_plan_id", queryUserConditionsVO.getSubscriptionPlanId());
|
||||
}
|
||||
|
||||
if (!Objects.isNull(queryUserConditionsVO.getOrganizationId())) {
|
||||
queryWrapper.eq("organization_id", queryUserConditionsVO.getOrganizationId());
|
||||
}
|
||||
|
||||
// 排序
|
||||
if (!StringUtils.isNullOrEmpty(queryUserConditionsVO.getOrder()) && !StringUtils.isNullOrEmpty(queryUserConditionsVO.getOrderBy())) {
|
||||
String orderBy = "id";
|
||||
@@ -798,27 +819,46 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
return accountMapper.selectPage(new Page<>(queryUserConditionsVO.getPage(), queryUserConditionsVO.getSize()), queryWrapper);
|
||||
}
|
||||
|
||||
public List<Map<String, Object>> getAllUserIdList() {
|
||||
public IPage<Map<String, Object>> getAllUserIdList(Integer pageNum, Integer pageSize, String email) {
|
||||
Long accountId = UserContext.getUserHolder().getId();
|
||||
Account account = accountMapper.selectById(accountId);
|
||||
// 允许查看数据的用户id
|
||||
if (Objects.nonNull(account.getSystemUser())
|
||||
&& (account.getSystemUser().equals(5)
|
||||
|| account.getSystemUser().equals(7)
|
||||
|| ADMIN_IDS.contains(account.getId())
|
||||
|| ADMIN_IDS_READ_ONLY.contains(account.getId())
|
||||
)) {
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.select("id as value, user_name as label");
|
||||
if ((account.getSystemUser().equals(7) || account.getSystemUser().equals(5))
|
||||
&& !StringUtil.isNullOrEmpty(account.getOrganizationName())) {
|
||||
queryWrapper.lambda().eq(Account::getOrganizationName, account.getOrganizationName());
|
||||
}
|
||||
return accountMapper.selectMaps(queryWrapper);
|
||||
} else {
|
||||
|
||||
// 权限校验
|
||||
if (Objects.isNull(account.getSystemUser())
|
||||
|| (!account.getSystemUser().equals(5)
|
||||
&& !account.getSystemUser().equals(7)
|
||||
&& !ADMIN_IDS.contains(account.getId())
|
||||
&& !ADMIN_IDS_READ_ONLY.contains(account.getId()))) {
|
||||
throw new BusinessException("have.no.permission", ResultEnum.PROMPT.getCode());
|
||||
}
|
||||
// return maps.stream().map(map -> (Long)map.get("id")).collect(Collectors.toList());
|
||||
|
||||
// 创建分页对象
|
||||
Page<Account> page = new Page<>(pageNum, pageSize);
|
||||
|
||||
// 构建查询条件
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.select("id", "user_name", "user_email");
|
||||
|
||||
if ((account.getSystemUser().equals(7) || account.getSystemUser().equals(5))
|
||||
&& !StringUtil.isNullOrEmpty(account.getOrganizationName())) {
|
||||
queryWrapper.lambda().eq(Account::getOrganizationName, account.getOrganizationName());
|
||||
}
|
||||
|
||||
if (!StringUtil.isNullOrEmpty(email)) {
|
||||
queryWrapper.lambda().like(Account::getUserEmail, email);
|
||||
}
|
||||
|
||||
// 执行分页查询
|
||||
IPage<Account> accountPage = accountMapper.selectPage(page, queryWrapper);
|
||||
|
||||
// 转换为 IPage<Map> 并重命名字段
|
||||
return accountPage.convert(acc -> {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("value", acc.getId());
|
||||
map.put("label", acc.getUserName());
|
||||
map.put("email", acc.getUserEmail());
|
||||
return map;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -838,19 +878,20 @@ public class ConvenientInquiryServiceImpl extends ServiceImpl<QuestionnaireMappe
|
||||
if (!StringUtil.isNullOrEmpty(queryPaymentInfoDTO.getOrder()) && queryPaymentInfoDTO.getOrder().equals("ASC")) {
|
||||
order = "ASC";
|
||||
}
|
||||
String status = StringUtil.isNullOrEmpty(queryPaymentInfoDTO.getStatus()) ? "Success" : queryPaymentInfoDTO.getStatus();
|
||||
List<PaymentInfoVO> paymentInfoVOS = paymentInfoMapper.queryPaymentInfo(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
||||
queryPaymentInfoDTO.getType(), status,
|
||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(),
|
||||
size, offset, order, queryPaymentInfoDTO.getPayer());
|
||||
// 查询数据总量
|
||||
Long total = paymentInfoMapper.queryPaymentInfoCount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
||||
queryPaymentInfoDTO.getType(), status,
|
||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
||||
// 查询符合查询条件的总金额
|
||||
BigDecimal payerTotal = paymentInfoMapper.queryTotalPaymentAmount(queryPaymentInfoDTO.getPlatform(), queryPaymentInfoDTO.getPayerTotal(),
|
||||
queryPaymentInfoDTO.getType(), queryPaymentInfoDTO.getStatus(),
|
||||
queryPaymentInfoDTO.getType(), status,
|
||||
queryPaymentInfoDTO.getCountry(), queryPaymentInfoDTO.getCity(),
|
||||
queryPaymentInfoDTO.getStartTime(), queryPaymentInfoDTO.getEndTime(), queryPaymentInfoDTO.getPayer());
|
||||
// 总页数
|
||||
|
||||
@@ -83,7 +83,7 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
*
|
||||
* @param changeEvent 导致积分变更的事件
|
||||
* @param credits 变更的积分
|
||||
* @param changeType 变更类型 : positive->增 negative->减
|
||||
* @param changeType 变更类型 : positive->增 negative->减 set->重置
|
||||
*/
|
||||
@Override
|
||||
public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo) {
|
||||
@@ -94,9 +94,11 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
if ("positive".equals(changeType)) {
|
||||
// finalCredits = account.getCredits().add(new BigDecimal(credits));
|
||||
changeCredits = "+" + credits;
|
||||
} else {
|
||||
} else if ("negative".equals(changeType)) {
|
||||
// finalCredits = account.getCredits().subtract(new BigDecimal(credits));
|
||||
changeCredits = "-" + credits;
|
||||
} else {
|
||||
changeCredits = credits;
|
||||
}
|
||||
creditsDetail.setAccountId(accountId);
|
||||
creditsDetail.setChangeEvent(changeEvent);
|
||||
@@ -107,6 +109,7 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
creditsDetail.setCreateTime(LocalDateTime.now());
|
||||
|
||||
baseMapper.insert(creditsDetail);
|
||||
log.info("creditsDetail inserted");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -363,9 +363,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
public List<TDesignPythonOutfitDetail> saveDesignSingleItemDetailAndLayers(DesignPythonObjects pythonObjects
|
||||
, Long designId, Long designItemId, Long userId
|
||||
, JSONObject outfit, String timeZone, List<DesignSingleItemDTO> designSingleItemDTOList
|
||||
, Map<String, List<String>> priorityAndUndividedLayer
|
||||
/*, Map<String, List<String>> priorityAndUndividedLayer*/
|
||||
, boolean changeModelFlag
|
||||
, Long modelId, String modelType, boolean isSingleCollectionFlag) {
|
||||
, Long modelId, String modelType, boolean isSingleCollectionFlag, String designType) {
|
||||
|
||||
DesignItem designItem = new DesignItem();
|
||||
// String url = pythonObjects.getObjects().get(0).getBasic().getSave_name();
|
||||
@@ -424,11 +424,15 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
designItemDetail.setColor(detail.getColor());
|
||||
designItemDetail.setIconPath(detail.getIcon());
|
||||
// designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getType().toLowerCase()));
|
||||
/*// 取消存储UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||
if (!detail.getType().equals("Body")) {
|
||||
designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(0));
|
||||
designItemDetail.setUndividedLayerWithSinglePrint(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(1));
|
||||
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(0))) {
|
||||
designItemDetail.setUndividedLayer(priorityAndUndividedLayer.get(detail.getPriority().toString()).getFirst());
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(1))) {
|
||||
designItemDetail.setUndividedLayerWithSinglePrint(priorityAndUndividedLayer.get(detail.getPriority().toString()).get(1));
|
||||
}
|
||||
}*/
|
||||
// 印花存储在design_item_detail_print表中 这里还要存吗?
|
||||
// DesignPythonItemPrint printObject = detail.getPrintToPython();
|
||||
// designItemDetail.setPrintPath(Objects.isNull(printObject) ? "" : printObject.getPath());
|
||||
@@ -436,7 +440,18 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
// designItemDetail.setPrintJson(JSON.toJSONString(printObject));
|
||||
|
||||
designItemDetail.setPartialDesign(Objects.isNull(detail.getPrint()) ? null : detail.getPrint().getPartial());
|
||||
designItemDetail.setGradientString(detail.getGradientString());
|
||||
|
||||
designItemDetails.add(designItemDetail);
|
||||
|
||||
// 处理gradientString为null的情况,强制更新为null
|
||||
if (Objects.isNull(detail.getGradientString()) && Objects.nonNull(designItemDetail.getId())) {
|
||||
UpdateWrapper<DesignItemDetail> updateWrapper = new UpdateWrapper<>();
|
||||
updateWrapper.eq("id", designItemDetail.getId());
|
||||
updateWrapper.set("gradient_string", null);
|
||||
updateWrapper.set("update_date", DateUtil.getByTimeZone(timeZone));
|
||||
designItemDetailService.update(null, updateWrapper);
|
||||
}
|
||||
});
|
||||
|
||||
// 逻辑删除未复用的旧记录
|
||||
@@ -481,6 +496,11 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
priorityOffset = designSingleItemDTOList.stream()
|
||||
.collect(Collectors.toMap(DesignSingleItemDTO::getPriority, DesignSingleItemDTO::getOffset));
|
||||
}
|
||||
|
||||
// 创建 priority 到 DesignSingleItemDTO 的映射,用于获取 transpose 和 rotate
|
||||
Map<Integer, DesignSingleItemDTO> priorityToItemDTOMap = designSingleItemDTOList.stream()
|
||||
.collect(Collectors.toMap(DesignSingleItemDTO::getPriority, dto -> dto, (old, newVal) -> old));
|
||||
|
||||
List<TDesignPythonOutfitDetail> list = new ArrayList<>();
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
JSONObject jsonObject = layers.getJSONObject(i);
|
||||
@@ -509,6 +529,12 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
designPythonOutfitDetail.setOffset(String.valueOf(priorityOffset.get(Math.abs(priority))));
|
||||
}
|
||||
designPythonOutfitDetail.setPriority(priority);
|
||||
// 从前端传入的 DesignSingleItemDTO 中获取 transpose 和 rotate,不再从 Python 返回的数据获取
|
||||
DesignSingleItemDTO itemDTO = priorityToItemDTOMap.get(Math.abs(priority));
|
||||
if (itemDTO != null) {
|
||||
designPythonOutfitDetail.setTranspose(itemDTO.getTranspose() != null ? Arrays.toString(itemDTO.getTranspose()) : null);
|
||||
designPythonOutfitDetail.setRotate(itemDTO.getRotate());
|
||||
}
|
||||
|
||||
list.add(designPythonOutfitDetail);
|
||||
}
|
||||
@@ -683,6 +709,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public DesignSingleVO designSingleIncludeLayers(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO) {
|
||||
if (!designSingleIncludeLayersDTO.getDesignType().equals("merge") && !designSingleIncludeLayersDTO.getDesignType().equals("default")) {
|
||||
throw new BusinessException("The value of DesignType can only be 'default' or 'merge' ");
|
||||
}
|
||||
// 记录入参 base64数据太长,所以这里去掉
|
||||
DesignSingleIncludeLayersDTO clone = SerializationUtils.clone(designSingleIncludeLayersDTO);
|
||||
clone.getDesignSingleItemDTOList().forEach(i -> {
|
||||
@@ -706,6 +735,17 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
log.info("set partialDesignBase64为空,便于日志打印");
|
||||
i.getPartialDesign().setPartialDesignBase64(null);
|
||||
}
|
||||
// undividedLayerBase64 undividedLayerWithSinglePrintBase64 置空
|
||||
/*// 前端合成的未分割的图
|
||||
if (!StringUtil.isNullOrEmpty(i.getUndividedLayerBase64())) {
|
||||
log.info("set UndividedLayerBase64为空,便于日志打印");
|
||||
i.setUndividedLayerBase64(null);
|
||||
}
|
||||
// 前端合成的未分割的图
|
||||
if (!StringUtil.isNullOrEmpty(i.getUndividedLayerWithSinglePrintBase64())) {
|
||||
log.info("set UndividedLayerWithSinglePrintBase64为空,便于日志打印");
|
||||
i.setUndividedLayerWithSinglePrintBase64(null);
|
||||
}*/
|
||||
});
|
||||
|
||||
log.info("designSingle request入参 ==> " + JSONObject.toJSONString(clone));
|
||||
@@ -800,7 +840,7 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
maskBase64ToPath(designSingleIncludeLayersDTO, setNull);
|
||||
// maskBase64ToPath(designSingleIncludeLayersDTO, Boolean.TRUE);
|
||||
|
||||
partialDesignBase64ToImage(designSingleIncludeLayersDTO, userId, designSingleIncludeLayersDTO.getIsPreview());
|
||||
partialDesignBase64ToImage(designSingleIncludeLayersDTO, userId, designSingleIncludeLayersDTO.getIsPreview(), designSingleIncludeLayersDTO.getDesignType());
|
||||
|
||||
// 组装入参
|
||||
DesignPythonObjects objects = pythonService.covertDesignSingleParam(
|
||||
@@ -818,13 +858,13 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
JSONObject outfit = data.getJSONObject("0");
|
||||
|
||||
JSONArray layers = outfit.getJSONArray("layers");
|
||||
Map<String, List<String>> priorityAndUndividedLayer = setPriorityAndUndividedLayer(layers);
|
||||
// Map<String, List<String>> priorityAndUndividedLayer = setPriorityAndUndividedLayer(layers, designSingleIncludeLayersDTO);
|
||||
if (!designSingleIncludeLayersDTO.getIsPreview()) {
|
||||
// 更新及保存图层信息
|
||||
tDesignPythonOutfitDetails = saveDesignSingleItemDetailAndLayers(objects, design.getId(), designSingleIncludeLayersDTO.getDesignItemId()
|
||||
, userId, outfit, designSingleIncludeLayersDTO.getTimeZone()
|
||||
, designSingleIncludeLayersDTO.getDesignSingleItemDTOList()
|
||||
, priorityAndUndividedLayer, changeModelFlag, modelId, modelType, isSingleCollectionFlag);
|
||||
/*, priorityAndUndividedLayer*/, changeModelFlag, modelId, modelType, isSingleCollectionFlag, designSingleIncludeLayersDTO.getDesignType());
|
||||
|
||||
saveCollectionElement(designSingleIncludeLayersDTO);
|
||||
} else {
|
||||
@@ -858,8 +898,8 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
outfit.getString("synthesis_url"),
|
||||
designSingleIncludeLayersDTO.getDesignSingleItemDTOList(),
|
||||
detailsVO,
|
||||
design.getSingleOverall(),
|
||||
priorityAndUndividedLayer);
|
||||
design.getSingleOverall()/*,
|
||||
priorityAndUndividedLayer*/);
|
||||
}
|
||||
|
||||
// 方法1:仅查询(无事务)
|
||||
@@ -919,12 +959,12 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
item.setMaskUrl(path);
|
||||
}
|
||||
}
|
||||
log.info("服装{} 的maskUrl为null", item.getType());
|
||||
// log.info("服装{} 的maskUrl为null", item.getType());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void partialDesignBase64ToImage(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO, Long accountId, boolean preview) {
|
||||
private void partialDesignBase64ToImage(DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO, Long accountId, boolean preview, String designType) {
|
||||
designSingleIncludeLayersDTO.getDesignSingleItemDTOList().forEach(item -> {
|
||||
PartialDesignDTO partialDesignDTO = item.getPartialDesign();
|
||||
if (!Objects.isNull(item.getPartialDesign())
|
||||
@@ -946,21 +986,90 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
item.getPartialDesign().setPartialDesignMinioPath(newPath);
|
||||
} else if (Objects.isNull(item.getPartialDesign())
|
||||
|| StringUtil.isNullOrEmpty(item.getPartialDesign().getPartialDesignMinioPath())) {
|
||||
item.setPartialDesign(new PartialDesignDTO(null));
|
||||
if (designType.equals("merge")) {
|
||||
// 先去数据库进行查找,如果数据库中也是空,则提示需要提供,否则无法生成
|
||||
DesignItemDetail designItemDetail = designItemDetailService.getById(item.getId());
|
||||
if (Objects.isNull(designItemDetail)){
|
||||
log.error("未知designItemDetailId: {}", item.getId());
|
||||
throw new BusinessException("designItemDetails.not.found");
|
||||
} else if (StringUtil.isNullOrEmpty(designItemDetail.getPartialDesign())) {
|
||||
item.setPartialDesign(new PartialDesignDTO(designItemDetail.getUndividedLayer()));
|
||||
/*log.error("merge模式下,必须提供partialDesign");
|
||||
throw new BusinessException("required.partialDesign");*/
|
||||
} else {
|
||||
item.setPartialDesign(new PartialDesignDTO(designItemDetail.getPartialDesign()));
|
||||
}
|
||||
} else {
|
||||
item.setPartialDesign(new PartialDesignDTO(null));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void undividedLayerBase64ToImage(List<DesignSingleItemDTO> designSingleItemDTOS) {
|
||||
designSingleItemDTOS.forEach(item -> {
|
||||
if (!StringUtil.isNullOrEmpty(item.getUndividedLayerBase64())) {
|
||||
if (item.getUndividedLayerBase64().startsWith("data:image") && !item.getUndividedLayerBase64().startsWith("https://")) {
|
||||
// 将原图地址作为修改后的图片地址,放在不同的桶
|
||||
String filename = "image/image_" + UUID.randomUUID();
|
||||
String path = minioUtil.base64UploadToPath(item.getUndividedLayerBase64(), clothingBucket, filename);
|
||||
log.info("undividedLayer, 新的path为{}", path);
|
||||
if (StringUtil.isNullOrEmpty(path)) {
|
||||
log.error("undividedLayer图片base64上传失败");
|
||||
throw new BusinessException("file.upload.fail");
|
||||
}
|
||||
item.setUndividedLayerBase64(path);
|
||||
} else {
|
||||
item.setUndividedLayerBase64(null);
|
||||
}
|
||||
}
|
||||
|
||||
if (!StringUtil.isNullOrEmpty(item.getUndividedLayerWithSinglePrintBase64())) {
|
||||
if (item.getUndividedLayerWithSinglePrintBase64().startsWith("data:image") && !item.getUndividedLayerWithSinglePrintBase64().startsWith("https://")) {
|
||||
// 将原图地址作为修改后的图片地址,放在不同的桶
|
||||
String filename = "image/image_" + UUID.randomUUID();
|
||||
String path = minioUtil.base64UploadToPath(item.getUndividedLayerWithSinglePrintBase64(), clothingBucket, filename);
|
||||
log.info("getUndividedLayerWithSinglePrint, 新的path为{}", path);
|
||||
if (StringUtil.isNullOrEmpty(path)) {
|
||||
log.error("getUndividedLayerWithSinglePrintBase64图片base64上传失败");
|
||||
throw new BusinessException("file.upload.fail");
|
||||
}
|
||||
item.setUndividedLayerWithSinglePrintBase64(path);
|
||||
} else {
|
||||
item.setUndividedLayerWithSinglePrintBase64(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers) {
|
||||
HashMap<String, List<String>> priorityAndLayer = new HashMap<>();
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
JSONObject jsonObject = layers.getJSONObject(i);
|
||||
String priority = jsonObject.getString("priority");
|
||||
String category = jsonObject.getString("image_category").split("_")[0];
|
||||
if (!category.equals("body") && !priorityAndLayer.containsKey(priority))
|
||||
priorityAndLayer.put(priority, Arrays.asList(jsonObject.getString("pattern_overall_image_url"), jsonObject.getString("pattern_print_image_url")));
|
||||
public Map<String, List<String>> setPriorityAndUndividedLayer(JSONArray layers, DesignSingleIncludeLayersDTO designSingleIncludeLayersDTO) {
|
||||
String designType = "default";
|
||||
if (Objects.nonNull(designSingleIncludeLayersDTO)) {
|
||||
designType = designSingleIncludeLayersDTO.getDesignType();
|
||||
}
|
||||
HashMap<String, List<String>> priorityAndLayer = new HashMap<>();
|
||||
if (designType.equals("default")) {
|
||||
for (int i = 0; i < layers.size(); i++) {
|
||||
JSONObject jsonObject = layers.getJSONObject(i);
|
||||
String priority = jsonObject.getString("priority");
|
||||
String category = jsonObject.getString("image_category").split("_")[0];
|
||||
if (!category.equals("body") && !priorityAndLayer.containsKey(priority)) {
|
||||
// pattern_overall_image_url | pattern_print_image_url 这俩字段来源有俩,merge模式下,来自前端,default模式下,来自python
|
||||
priorityAndLayer.put(priority, Arrays.asList(jsonObject.getString("pattern_overall_image_url"), jsonObject.getString("pattern_print_image_url")));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (designSingleIncludeLayersDTO.getIsPreview()) {
|
||||
// 如果是预览,则不处理、不存储前端传过来的数据
|
||||
return priorityAndLayer;
|
||||
}
|
||||
undividedLayerBase64ToImage(designSingleIncludeLayersDTO.getDesignSingleItemDTOList());
|
||||
for (DesignSingleItemDTO designSingleItemDTO : designSingleIncludeLayersDTO.getDesignSingleItemDTOList()) {
|
||||
priorityAndLayer.put(designSingleItemDTO.getPriority().toString(), Arrays.asList(designSingleItemDTO.getUndividedLayerBase64(), designSingleItemDTO.getUndividedLayerWithSinglePrintBase64()));
|
||||
}
|
||||
}
|
||||
|
||||
return priorityAndLayer;
|
||||
}
|
||||
|
||||
@@ -1075,8 +1184,8 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
String currentFullBodyView,
|
||||
List<DesignSingleItemDTO> designSingleItemDTOList,
|
||||
List<DesignPythonOutfitVO> layersObject,
|
||||
String singleOrOverall,
|
||||
Map<String, List<String>> priorityAndUndividedLayer) {
|
||||
String singleOrOverall/*,
|
||||
Map<String, List<String>> priorityAndUndividedLayer*/) {
|
||||
|
||||
DesignSingleVO designSingleVO = new DesignSingleVO();
|
||||
ArrayList<DesignItemClothesDetailVO> clothes = new ArrayList<>();
|
||||
@@ -1116,10 +1225,15 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
String preSignedUrl = StringUtil.isNullOrEmpty(partialDesignMinioPath) ? null : minioUtil.getPreSignedUrl(partialDesignMinioPath, CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true);
|
||||
designItemClothesDetailVO.setPartialDesign(new PartialDesignDTO(partialDesignMinioPath, preSignedUrl));
|
||||
|
||||
/*// 取消存储/返回UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||
if (priorityAndUndividedLayer.containsKey(singleItem.getPriority().toString())) {
|
||||
designItemClothesDetailVO.setUndividedLayer(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(0), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
designItemClothesDetailVO.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(1), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(0))) {
|
||||
designItemClothesDetailVO.setUndividedLayer(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).getFirst(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(1))) {
|
||||
designItemClothesDetailVO.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(priorityAndUndividedLayer.get(singleItem.getPriority().toString()).get(1), CommonConstant.MINIO_IMAGE_EXPIRE_TIME, true));
|
||||
}
|
||||
}*/
|
||||
body.setLayersObject(layersObject.stream().filter(layers -> layers.getImageCategory().equals("body")).collect(Collectors.toList()));
|
||||
|
||||
clothes.add(designItemClothesDetailVO);
|
||||
@@ -1201,6 +1315,7 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
designItemDetailPrint.setPosition(print.getLocation().toString());
|
||||
designItemDetailPrint.setAngle(print.getAngle());
|
||||
designItemDetailPrint.setPriority(priority);
|
||||
designItemDetailPrint.setObject(print.getObject());
|
||||
|
||||
designItemDetailPrints.add(designItemDetailPrint);
|
||||
});
|
||||
@@ -1294,6 +1409,9 @@ public class DesignItemServiceImpl extends ServiceImpl<DesignItemMapper, DesignI
|
||||
}
|
||||
}
|
||||
|
||||
if (Objects.isNull(designSingleItem.getPrintObject()) || Objects.isNull(designSingleItem.getPrintObject().getPrints())) {
|
||||
return;
|
||||
}
|
||||
// 添加print到library
|
||||
designSingleItem.getPrintObject().getPrints().forEach(print -> {
|
||||
if (!StringUtil.isNullOrEmpty(print.getDesignType()) && print.getDesignType().equals("Collection")) {
|
||||
|
||||
@@ -46,6 +46,7 @@ import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
@@ -101,8 +102,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
private final UserLikeGroupService userLikeGroupService;
|
||||
private final UserLikeService userLikeService;
|
||||
private final UserBehaviorMapper userBehaviorMapper;
|
||||
private final UserPreferenceLogMapper userPreferenceLogMapper;
|
||||
private final UserPreferenceMapper userPreferenceMapper;
|
||||
private final WorkspaceService workspaceService;
|
||||
private final WorkspaceRelStyleMapper workspaceRelStyleMapper;
|
||||
|
||||
@Value("${minio.endpoint}")
|
||||
private String endpoint;
|
||||
@@ -714,7 +716,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
(existing, replacement) -> replacement));
|
||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
log.info("all typeLayers Map:{}", typeAndUndividedLayer);
|
||||
Map<String, List<String>> priorityAndUndividedLayer = designItemService.setPriorityAndUndividedLayer(layers);
|
||||
Map<String, List<String>> priorityAndUndividedLayer = designItemService.setPriorityAndUndividedLayer(layers, null);
|
||||
for (DesignPythonItem detail : item.getItems()) {
|
||||
if (null == detail) {
|
||||
continue;
|
||||
@@ -754,7 +756,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
print.setPosition("[0.0,0.0]");
|
||||
// print.setScale(1d);
|
||||
// todo mark 将print默认scale置为0.3
|
||||
print.setScale(Arrays.toString(new Float[]{0.3f, 0.3f}));
|
||||
print.setScale(Arrays.toString(new Float[]{1.0f, 1.0f}));
|
||||
print.setAngle(0.0);
|
||||
print.setPriority(1);
|
||||
QueryWrapper<CollectionElement> getPrintboardLevel2TypeQw = new QueryWrapper<>();
|
||||
@@ -762,7 +764,20 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
getPrintboardLevel2TypeQw.lambda().orderByDesc(CollectionElement::getCreateDate);
|
||||
getPrintboardLevel2TypeQw.last("limit 1");
|
||||
CollectionElement one = collectionElementService.getOne(getPrintboardLevel2TypeQw);
|
||||
print.setLevel2Type(one.getLevel2Type());
|
||||
if (Objects.isNull(one)) {
|
||||
QueryWrapper<Library> libraryQueryWrapper = new QueryWrapper<>();
|
||||
libraryQueryWrapper.lambda().eq(Library::getUrl, print.getPath());
|
||||
libraryQueryWrapper.lambda().orderByDesc(Library::getCreateDate);
|
||||
getPrintboardLevel2TypeQw.last("limit 1");
|
||||
Library library = libraryService.getOne(libraryQueryWrapper);
|
||||
if (Objects.isNull(library)) {
|
||||
print.setLevel2Type("Pattern");
|
||||
} else {
|
||||
print.setLevel2Type(library.getLevel2Type());
|
||||
}
|
||||
} else {
|
||||
print.setLevel2Type(one.getLevel2Type());
|
||||
}
|
||||
print.setCreateDate(LocalDateTime.now());
|
||||
designItemDetailPrintService.save(print);
|
||||
}
|
||||
@@ -852,7 +867,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
Map<String, Integer> typePriority = list.stream().collect(Collectors.toMap(d -> d.getImageCategory().split("_")[0],
|
||||
d -> Math.abs(d.getPriority()),
|
||||
(existing, replacement) -> replacement));
|
||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
// Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
for (DesignPythonItem detail : item.getItems()) {
|
||||
if (null == detail) {
|
||||
continue;
|
||||
@@ -863,9 +878,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
designItemDetail.setDesignItemId(designItemId);
|
||||
designItemDetail.setCollectionElementId(detail.getElementId());
|
||||
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
||||
if (!detail.getType().equals("Body")) {
|
||||
/*if (!detail.getType().equals("Body")) {
|
||||
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
||||
}
|
||||
}*/
|
||||
|
||||
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
||||
designItemDetail.setPath(detail.getBody_path());
|
||||
@@ -1109,18 +1124,20 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
//修改designItem为like状态
|
||||
designItemService.updateLikeStatus(designLikeDTO.getDesignItemId(), (byte) 1);
|
||||
// 记录喜欢的系统sketch
|
||||
addSystemLikeSketch(designItem);
|
||||
addSystemLikeSketch(designItem, designLikeDTO.getProjectId());
|
||||
// 更新项目更新时间
|
||||
projectService.modifyProjectUpdateTime(designLikeDTO.getProjectId());
|
||||
return new DesignLikeVO(userLikeSortId, userGroupId, groupDetailId, pictureName, userLike.getId(), userLikeSort.getSort());
|
||||
}
|
||||
|
||||
private void addSystemLikeSketch(DesignItem designItem) {
|
||||
public void addSystemLikeSketch(DesignItem designItem, Long projectId) {
|
||||
AuthPrincipalVo userHolder = UserContext.getUserHolder();
|
||||
QueryWrapper<DesignItemDetail> qw = new QueryWrapper<>();
|
||||
qw.lambda().eq(DesignItemDetail::getDesignItemId, designItem.getId());
|
||||
qw.lambda().ne(DesignItemDetail::getType, "Body");
|
||||
List<DesignItemDetail> designItemDetails = designItemDetailMapper.selectList(qw);
|
||||
List<WorkspaceRelStyle> workspaceRelStyles = workspaceRelStyleMapper.selectByProjectId(projectId);
|
||||
|
||||
for (DesignItemDetail designItemDetail : designItemDetails) {
|
||||
if (designItemDetail.getPath().startsWith("aida-sys-image")) {
|
||||
String[] split = designItemDetail.getPath().split("/");
|
||||
@@ -1133,28 +1150,34 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
userBehavior.setCreateTime(LocalDateTime.now());
|
||||
userBehaviorMapper.insert(userBehavior);
|
||||
|
||||
UserPreferenceLogTest userPreferenceLogTest = new UserPreferenceLogTest();
|
||||
userPreferenceLogTest.setPath(designItemDetail.getPath());
|
||||
UserPreference userPreference = new UserPreference();
|
||||
userPreference.setPath(designItemDetail.getPath());
|
||||
SysFile sysFile = sysFileService.getOne(new LambdaQueryWrapper<SysFile>().eq(SysFile::getUrl, designItemDetail.getPath()));
|
||||
userPreferenceLogTest.setAccountId(userHolder.getId());
|
||||
if (sysFile != null){
|
||||
userPreferenceLogTest.setCategory(sysFile.getLevel3Type().toLowerCase()+"_"+sysFile.getLevel2Type().toLowerCase());
|
||||
userPreferenceLogTest.setSysFileId(sysFile.getId());
|
||||
userPreferenceLogTest.setStyle(sysFile.getStyle());
|
||||
}else {
|
||||
log.error("sysFile not found:{}",designItemDetail.getPath());
|
||||
SendEmailUtil.commonExceptionReminder("url在sysFile里找不到"+designItemDetail.getPath(), new String[]{"litianxiangxtt@163.com"});
|
||||
userPreference.setAccountId(userHolder.getId());
|
||||
if (sysFile != null) {
|
||||
userPreference.setCategory(sysFile.getLevel3Type().toLowerCase() + "_" + sysFile.getLevel2Type().toLowerCase());
|
||||
userPreference.setStyle(sysFile.getStyle());
|
||||
} else {
|
||||
log.error("sysFile not found:{}", designItemDetail.getPath());
|
||||
SendEmailUtil.commonExceptionReminder("url在sysFile里找不到" + designItemDetail.getPath(), new String[]{"litianxiangxtt@163.com"});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// userPreferenceLogTest.setUserLikeGroupId(userLike.getUserLikeGroupId());
|
||||
userPreferenceLogTest.setDataTime(designItemDetail.getCreateDate().toInstant()
|
||||
userPreference.setDataTime(new Date().toInstant()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDateTime());
|
||||
userPreferenceLogMapper.insert(userPreferenceLogTest);
|
||||
userPreference.setDesignItemId(designItem.getId());
|
||||
userPreference.setProjectId(projectId);
|
||||
if (workspaceRelStyles == null||workspaceRelStyles.isEmpty()) {
|
||||
//查不到记录,style应该是all
|
||||
userPreference.setWorkspaceRelStyleId(0L);
|
||||
} else {
|
||||
userPreference.setWorkspaceRelStyleId(workspaceRelStyles.get(0).getStyleId());
|
||||
}
|
||||
userPreferenceMapper.insert(userPreference);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private List<Long> validateMergeElement(List<CollectionElement> oldElements, List<DesignItemDetail> designItemDetails) {
|
||||
@@ -1322,12 +1345,13 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
d.setScope(o.getPath().startsWith("aida-sys-image") ? "sys" : "user");
|
||||
d.setLevel1Type(converTypeToLevel1(o.getType()));
|
||||
d.setGradient(JSONObject.parseObject(o.getGradientString(), Gradient.class));
|
||||
/*// 取消存储/返回UndividedLayer和UndividedLayerWithSinglePrint字段
|
||||
if (!StringUtil.isNullOrEmpty(o.getUndividedLayer())) {
|
||||
d.setUndividedLayer(minioUtil.getPreSignedUrl(o.getUndividedLayer(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
||||
}
|
||||
if (!StringUtil.isNullOrEmpty(o.getUndividedLayerWithSinglePrint())) {
|
||||
d.setUndividedLayerWithSinglePrint(minioUtil.getPreSignedUrl(o.getUndividedLayerWithSinglePrint(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME));
|
||||
}
|
||||
}*/
|
||||
// 根据designItemDetailId获取印花
|
||||
List<DesignItemDetailPrint> prints = designItemDetailPrintService.getByDesignItemDetailId(o.getId(), "print");
|
||||
prints.removeIf(print -> !minioUtil.doesObjectExist(print.getPath()));
|
||||
@@ -1576,6 +1600,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
}
|
||||
}
|
||||
designSinglePrint.setScale(scales);
|
||||
designSinglePrint.setObject(detailPrint.getObject());
|
||||
prints.add(designSinglePrint);
|
||||
} else {
|
||||
// 多个印花
|
||||
@@ -1596,7 +1621,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
scales = Arrays.asList(scale, scale);
|
||||
}
|
||||
}
|
||||
prints.add(new DesignSinglePrint(
|
||||
DesignSinglePrint designSinglePrint = new DesignSinglePrint(
|
||||
print.getLevel2Type(),
|
||||
minioUtil.getPreSignedUrl(print.getPath(), 24 * 60),
|
||||
print.getPath(),
|
||||
@@ -1604,7 +1629,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
scales,
|
||||
print.getAngle(),
|
||||
print.getPriority(),
|
||||
ifSingle));
|
||||
ifSingle);
|
||||
designSinglePrint.setObject(print.getObject());
|
||||
prints.add(designSinglePrint);
|
||||
// }
|
||||
});
|
||||
}
|
||||
@@ -1656,6 +1683,24 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
.lt("create_date", endTime)
|
||||
.select("count(id) as count");
|
||||
|
||||
// 如果startTime早于2024-05-01 00:00:00,添加额外的筛选条件
|
||||
LocalDateTime thresholdTime = LocalDateTime.of(2024, 5, 1, 0, 0, 0);
|
||||
|
||||
try {
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
LocalDateTime startDateTime = LocalDateTime.parse(startTime, formatter);
|
||||
|
||||
if (startDateTime.isBefore(thresholdTime)) {
|
||||
// 使用 notLike 来排除以 ":01" 或 ":02" 结尾的时间
|
||||
// 方案2.1:使用 apply 方法执行数据库函数 (create_date 字段的实际存储格式(可能包含毫秒),所以取最后三个字符进行匹配)
|
||||
queryWrapper.apply("DATE_FORMAT(create_date, '%s') NOT IN ('01', '02')");
|
||||
/*queryWrapper.notLike("create_date", "%:01")
|
||||
.notLike("create_date", "%:02");*/
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse startTime: {}, skip time-based filtering", startTime, e);
|
||||
}
|
||||
|
||||
if (!Objects.isNull(accountIds) && !accountIds.isEmpty()) {
|
||||
queryWrapper.in("account_id", accountIds);
|
||||
}
|
||||
@@ -1663,7 +1708,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
List<Map<String, Object>> result = baseMapper.selectMaps(queryWrapper);
|
||||
if (result != null && !result.isEmpty()) {
|
||||
Object countObj = result.get(0).get("count");
|
||||
return (Long) countObj;
|
||||
return countObj != null ? ((Number) countObj).longValue() : 0L;
|
||||
} else {
|
||||
return 0L;
|
||||
}
|
||||
@@ -2532,7 +2577,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
Map<String, Integer> typePriority = list.stream().collect(Collectors.toMap(d -> d.getImageCategory().split("_")[0],
|
||||
d -> Math.abs(d.getPriority()),
|
||||
(existing, replacement) -> replacement));
|
||||
Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
// Map<String, String> typeAndUndividedLayer = designItemService.setTypeAndUndividedLayer(layers);
|
||||
for (DesignPythonItem detail : item.getItems()) {
|
||||
if (null == detail) {
|
||||
continue;
|
||||
@@ -2543,9 +2588,9 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
designItemDetail.setDesignItemId(designItemId);
|
||||
designItemDetail.setCollectionElementId(detail.getElementId());
|
||||
designItemDetail.setCreateDate(DateUtil.getByTimeZone(timeZone));
|
||||
if (!detail.getType().equals("Body")) {
|
||||
/*if (!detail.getType().equals("Body")) {
|
||||
designItemDetail.setUndividedLayer(typeAndUndividedLayer.get(designItemDetail.getType()));
|
||||
}
|
||||
}*/
|
||||
if (SysFileLevel2TypeEnum.BODY.getRealName().equals(detail.getType())) {
|
||||
designItemDetail.setPath(detail.getBody_path());
|
||||
//BODY不关联businessId
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ai.da.common.response.ResultEnum;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.ai.da.common.utils.MailUtil;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
import com.ai.da.common.utils.SendEmailUtil;
|
||||
import com.ai.da.mapper.primary.AccountMapper;
|
||||
import com.ai.da.mapper.primary.EmailLogMapper;
|
||||
import com.ai.da.mapper.primary.EmailTemplateMapper;
|
||||
@@ -585,9 +586,7 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
public boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||
try {
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
List<String> merchantReceiver = Arrays.asList(/*merchantEmail,*/ developer);
|
||||
List<String> merchantReceiver = buildMerchantReceiverList();
|
||||
|
||||
String merchantSubject = null;
|
||||
String merchantTemplate = null;
|
||||
@@ -723,15 +722,13 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
private final static String CREDITS_PURCHASE_MERCHANT = "133275_AiDA 积分购买通知-merchant.html";
|
||||
public void creditsPurchaseReminder(String username, String quantity, String amount) {
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developerEmail = "xupei@code-create.com.hk";
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
// 设置试用订单相关数据
|
||||
jsonObject.put("username", username);
|
||||
jsonObject.put("quantity", quantity);
|
||||
jsonObject.put("totalFee", amount);
|
||||
|
||||
sendEmail(Arrays.asList(/*merchantEmail,*/ developerEmail), jsonObject, CREDITS_PURCHASE_MERCHANT, "New Credit Purchase Order", null, null);
|
||||
sendEmail(buildMerchantReceiverList(), jsonObject, CREDITS_PURCHASE_MERCHANT, "New Credit Purchase Order", null, null);
|
||||
}
|
||||
|
||||
private final static String COMMON_EXCEPTION_REMINDER = "135279_common-exception-reminder.html";
|
||||
@@ -742,6 +739,10 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
sendEmail(destination, param, COMMON_EXCEPTION_REMINDER, "AiDA发生异常,请及时处理", null, null);
|
||||
}
|
||||
|
||||
private List<String> buildMerchantReceiverList() {
|
||||
return Arrays.asList(SendEmailUtil.buildMerchantReceiverEmail());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
@@ -59,10 +61,12 @@ import org.bytedeco.javacv.Java2DFrameConverter;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
@@ -194,10 +198,13 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
generate.setText(text);
|
||||
Long elementId = generateThroughImageTextDTO.getCollectionElementId();
|
||||
// validateGeneraType(generate, text, elementId);
|
||||
if (!StringUtil.isNullOrEmpty(text)) {
|
||||
text = modifyPrompt(text, generate, generateThroughImageTextDTO.getLevel1Type(), generateThroughImageTextDTO.getAgeGroup());
|
||||
if (!(generateThroughImageTextDTO.getLevel1Type().equals(MOOD_BOARD.getRealName())&&generateThroughImageTextDTO.getModelName().equals("high"))){
|
||||
if (!StringUtil.isNullOrEmpty(text)) {
|
||||
text = modifyPrompt(text, generate, generateThroughImageTextDTO.getLevel1Type(), generateThroughImageTextDTO.getAgeGroup());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// todo 这一步现在还是有必要的吗?
|
||||
// 2.1 sketch或print在t_collection_element表/t_library表中的信息是否需要更新 如 level2Type
|
||||
CollectionElement collectionElement = collectionElementService.editLevel2Type(elementId, generateThroughImageTextDTO.getLevel2Type(), generateThroughImageTextDTO.getDesignType());
|
||||
@@ -218,6 +225,8 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
version = "fast";
|
||||
params.put("version", "fast");
|
||||
}
|
||||
// 4、将请求信息落库,将本次generate的请求信息添加到t_generate表中
|
||||
saveGenerateImmediately(generate);
|
||||
// 3.1 确定不同类型的印花分别调哪个接口
|
||||
if (generateThroughImageTextDTO.getLevel1Type().equals(PRINT_BOARD.getRealName())) {
|
||||
switch (generateThroughImageTextDTO.getLevel2Type()) {
|
||||
@@ -243,15 +252,28 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
||||
}
|
||||
} else {
|
||||
GenerateToPythonDTO generateToPythonDTO = new GenerateToPythonDTO(generateThroughImageTextDTO.getUniqueId(), text, Objects.isNull(collectionElement) ? "" : collectionElement.getUrl(),
|
||||
mode, category, generateThroughImageTextDTO.getGender(), version);
|
||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
||||
if (Objects.equals(version, "fast")) {
|
||||
GenerateToPythonDTO generateToPythonDTO = new GenerateToPythonDTO(generateThroughImageTextDTO.getUniqueId(), text, Objects.isNull(collectionElement) ? "" : collectionElement.getUrl(),
|
||||
mode, category, generateThroughImageTextDTO.getGender(), version);
|
||||
jsonString = JSON.toJSONString(generateToPythonDTO, SerializerFeature.WriteMapNullValue);
|
||||
} else {
|
||||
|
||||
|
||||
path = CommonConstant.GENERATE_PATH_FLUX2_KLEIN;
|
||||
// 构建object_name: {userId}/{category}/{uuid}.png
|
||||
String objectName = generateThroughImageTextDTO.getUserId() + "/" + category + "/" + UUID.randomUUID() + ".png";
|
||||
|
||||
ImageProcessRequest imageProcessRequest = ImageProcessRequest.builder()
|
||||
.object_name(objectName)
|
||||
.bucket_name(userBucket)
|
||||
.prompt(text).build();
|
||||
jsonString = JSON.toJSONString(imageProcessRequest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Boolean requestResult = pythonService.generateSketchOrPrint(jsonString, port, path);
|
||||
Boolean requestResult = pythonService.generateSketchOrPrint(jsonString, port, path, generateThroughImageTextDTO.getUniqueId());
|
||||
|
||||
// 4、将请求信息落库,将本次generate的请求信息添加到t_generate表中
|
||||
save(generate);
|
||||
|
||||
// 5、将本次请求存入redis
|
||||
String key = generateResultKey + ":" + generateThroughImageTextDTO.getUniqueId();
|
||||
@@ -266,6 +288,40 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
|
||||
}
|
||||
|
||||
public void saveGenerateImmediately(Generate generate) {
|
||||
save(generate);
|
||||
// 使用 TransactionSynchronizationManager 在事务真正提交后再设锁
|
||||
// 否则 save() 完成后事务尚未 commit,MQ 消费者立即读到 null
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
String lockKey = "generate:lock:" + generate.getUniqueId();
|
||||
redisUtil.addToString(lockKey, "1", 60L);
|
||||
log.debug("Save lock set after commit for uniqueId: {}", generate.getUniqueId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void waitForSaveLock(String uniqueId) {
|
||||
String lockKey = "generate:lock:" + uniqueId;
|
||||
int maxRetries = 30;
|
||||
int retryIntervalMs = 200;
|
||||
for (int i = 0; i < maxRetries; i++) {
|
||||
if (Boolean.TRUE.equals(redisUtil.hasKey(lockKey))) {
|
||||
log.debug("Save lock acquired for uniqueId: {} after {} retries", uniqueId, i);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(retryIntervalMs);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("Interrupted while waiting for save lock: {}", uniqueId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.warn("Save lock timeout for uniqueId: {}, proceeding anyway", uniqueId);
|
||||
}
|
||||
|
||||
public GenerateModeEnum getMode(GenerateThroughImageTextDTO generateThroughImageTextDTO) {
|
||||
if (!StringUtil.isNullOrEmpty(generateThroughImageTextDTO.getText())) {
|
||||
if (Objects.nonNull(generateThroughImageTextDTO.getCollectionElementId())) {
|
||||
@@ -284,11 +340,16 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void processGenerateResult(String taskId, String url, String category) {
|
||||
log.info("============ProcessGenerateResult listening==========");
|
||||
log.debug("taskId: " + taskId);
|
||||
String status = null;
|
||||
// 1、处理模型返回的数据
|
||||
GenerateDetail generateDetail = new GenerateDetail();
|
||||
GenerateCollectionItemVO generateCollectionItemVO = new GenerateCollectionItemVO();
|
||||
Generate generate;
|
||||
try {
|
||||
// 等待 HTTP 线程写入完成后再查库
|
||||
waitForSaveLock(taskId);
|
||||
generate = selectByUniqueId(taskId);
|
||||
} catch (MybatisPlusException e) {
|
||||
log.error(e.getMessage());
|
||||
@@ -311,14 +372,15 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
generateDetail.setUrl(url);
|
||||
generateDetail.setGenerateId(generate.getId());
|
||||
generateDetail.setCreateDate(LocalDateTime.now());
|
||||
generateDetail.setMd5(md5);
|
||||
generateDetail.setMd5("");
|
||||
// 将相应的url保存到数据库
|
||||
generateDetailMapper.insert(generateDetail);
|
||||
log.debug("generateDetail: " + generateDetail.toString());
|
||||
|
||||
// String uuid = taskId.substring(0, taskId.substring(0, taskId.lastIndexOf("-")).lastIndexOf("-"));
|
||||
String key = generateResultKey + ":" + taskId;
|
||||
String imageName = url.substring(url.lastIndexOf("/") + 1);
|
||||
String status = imageName.equals("white_image.jpg") ? "Invalid" : "Success";
|
||||
status = imageName.equals("white_image.jpg") ? "Invalid" : "Success";
|
||||
if (StringUtil.isNullOrEmpty(category)) {
|
||||
Generate generateRecord = selectByUniqueId(taskId);
|
||||
category = generateRecord.getLevel2Type();
|
||||
@@ -326,6 +388,8 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
GenerateResultVO generateResultVO = new GenerateResultVO(taskId, generateDetail.getId(), url, status, category);
|
||||
// 更新redis
|
||||
redisUtil.addToString(key, new Gson().toJson(generateResultVO), CommonConstant.GENERATE_RESULT_EXPIRE_TIME);
|
||||
log.debug("generateResultVO: " + generateResultVO.toString());
|
||||
|
||||
|
||||
// 执行积分扣除
|
||||
// ** 注:如果生成的图片都是空白 则不扣积分
|
||||
@@ -785,8 +849,9 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
long requestEndTime = System.currentTimeMillis();
|
||||
log.info("HTTP请求完成 - 响应状态: {}, 耗时: {}ms, taskId: {}",
|
||||
response.code(), (requestEndTime - requestStartTime), taskId);
|
||||
String result = response.body().string();
|
||||
if (!response.isSuccessful()) {
|
||||
log.warn("Google API响应失败,状态码: {} for taskId: {}", response.code(), taskId);
|
||||
log.warn("Google API响应失败,状态码: {} for taskId: {},结果:{}", response.code(), taskId, result);
|
||||
if (attempt < maxRetries) {
|
||||
Thread.sleep(retryDelay * attempt); // 递增延迟
|
||||
continue;
|
||||
@@ -795,7 +860,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
}
|
||||
|
||||
String result = response.body().string();
|
||||
|
||||
// log.info("Google 响应结果:{}", result);
|
||||
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(result);
|
||||
|
||||
@@ -1065,6 +1130,12 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
|
||||
String result = response.body().string();
|
||||
|
||||
if (response.code() != 200) {
|
||||
log.error("Google API 请求失败 - taskId: {}, 尝试: {}, URL: {}, 状态码: {}, 响应结果: {}",
|
||||
taskId, attempt, endpoint, response.code(), result);
|
||||
throw new BusinessException("system.error");
|
||||
}
|
||||
|
||||
// log.info("Google 响应结果:{}", result);
|
||||
com.alibaba.fastjson.JSONObject jsonResponse = JSON.parseObject(result);
|
||||
|
||||
@@ -1203,7 +1274,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
* @param modelName advanced high normal
|
||||
*/
|
||||
private HashMap<String, String> chooseModelAndPrompt(GenerateThroughImageTextDTO generateDTO, String modelName) {
|
||||
if (StringUtil.isNullOrEmpty(modelName)){
|
||||
if (StringUtil.isNullOrEmpty(modelName)) {
|
||||
throw new BusinessException("system error");
|
||||
}
|
||||
HashMap<String, String> modelAndPromptMap = new HashMap<>();
|
||||
@@ -1221,7 +1292,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
String style = generateDTO.getText().substring(0, firstCommaIndex).trim();
|
||||
|
||||
String prompt = generateDTO.getText().substring(firstCommaIndex + 1).trim();
|
||||
prompt = getPrintboardPrompt(style, prompt,modelName);
|
||||
prompt = getPrintboardPrompt(style, prompt, modelName, isUseImage);
|
||||
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||
|
||||
|
||||
@@ -1260,8 +1331,31 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
modelAndPromptMap.put(ModelConstants.USE_MODEL, ModelConstants.LOCAL_MODEL);
|
||||
}
|
||||
} else if (ModelConstants.SKETCHBOARD.equals(generateDTO.getLevel1Type())) {
|
||||
String prompt = generateDTO.getText() + "rules:front view sketch only,plain white background, single garment only, orthographic, centered on white background, borderless canvas, thin monochrome black line art.\n" +
|
||||
String style = "";
|
||||
String userPrompt = "";
|
||||
// 找到第一个逗号的位置
|
||||
int firstCommaIndex = generateDTO.getText().indexOf(",");
|
||||
if (firstCommaIndex != -1) {
|
||||
// 截取第一个逗号前的内容作为style
|
||||
style = generateDTO.getText().substring(0, firstCommaIndex).trim();
|
||||
// 截取第一个逗号后的所有内容作为userPrompt(去除首尾空格)
|
||||
userPrompt = generateDTO.getText().substring(firstCommaIndex + 1).trim();
|
||||
|
||||
if ("Lolita".equals(style)) {
|
||||
style = "洛丽塔";
|
||||
}
|
||||
} else {
|
||||
// 兼容无逗号的情况:style为空,全部内容作为userPrompt
|
||||
userPrompt = generateDTO.getText().trim();
|
||||
}
|
||||
|
||||
String prompt = userPrompt + "rules:front view sketch only,plain white background, single garment only, orthographic, centered on white background, borderless canvas, thin monochrome black line art.\n" +
|
||||
" No clothes hanger, no fake clothes hanger, no human-related lines, no color fill, no words, no text, no black background, no boundary or frame.";
|
||||
|
||||
if (!style.trim().isEmpty() && !"all".equalsIgnoreCase(style)) {
|
||||
prompt += ".sketch style:" + style.trim();
|
||||
}
|
||||
|
||||
modelAndPromptMap.put(ModelConstants.PROMPT, prompt);
|
||||
if (isUseImage) {
|
||||
if (ModelConstants.ADVANCED.equals(modelName)) {
|
||||
@@ -1459,6 +1553,13 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
if (imagePath != null) {
|
||||
requestBuilder.image(finalImagePath1);
|
||||
}
|
||||
if (useModel.equals(ModelConstants.PRINTBOARD_HIGH_I2I)|| useModel.equals(ModelConstants.PRINTBOARD_HIGH_T2I)) {
|
||||
GenerateImagesRequest.OptimizePromptOptions optimizePromptOptions = new GenerateImagesRequest.OptimizePromptOptions();
|
||||
optimizePromptOptions.setMode("fast");
|
||||
requestBuilder.optimizePromptOptions(optimizePromptOptions);
|
||||
//由于PRINTBOARD_HIGH_T2I,PRINTBOARD_HIGH_I2I与PRINTBOARD_ADVANCED_I2I使用模型一致,为了区别积分扣除,PRINTBOARD_HIGH_I2I加入了-fast或者-high,但传入模型时需要去掉-fast或者-high,用PRINTBOARD_ADVANCED_I2I的常量做替代
|
||||
requestBuilder.model(ModelConstants.PRINTBOARD_ADVANCED_I2I);
|
||||
}
|
||||
|
||||
// 保存生成记录到数据库
|
||||
Generate generate = new Generate(
|
||||
@@ -1570,7 +1671,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
|
||||
|
||||
private String getPrintboardPrompt(String style, String userInput, String modelName) {
|
||||
private String getPrintboardPrompt(String style, String userInput, String modelName, boolean isUseImage) {
|
||||
String systemPrompt = null;
|
||||
String prompt;
|
||||
|
||||
@@ -1596,12 +1697,16 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
"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);
|
||||
} else {
|
||||
throw new BusinessException("style error:" + style);
|
||||
}
|
||||
|
||||
if (userInput == null || userInput.trim().isEmpty()) {
|
||||
throw new BusinessException("prompt null");
|
||||
if (isUseImage) {
|
||||
prompt = "Theme: Image content" + "\nRequirement: " + systemPrompt;
|
||||
} else {
|
||||
throw new BusinessException("prompt null");
|
||||
}
|
||||
} else {
|
||||
prompt = "Theme: " + userInput.trim() + "\nRequirement: " + systemPrompt;
|
||||
}
|
||||
@@ -2001,7 +2106,9 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
public Generate selectByUniqueId(String uniqueId) {
|
||||
QueryWrapper<Generate> qw = new QueryWrapper<>();
|
||||
qw.eq("unique_id", uniqueId);
|
||||
|
||||
log.debug("selectByUniqueId: " + uniqueId);
|
||||
Generate one = getOne(qw);
|
||||
log.debug("Generate: " + one);
|
||||
return getOne(qw);
|
||||
}
|
||||
|
||||
@@ -3827,11 +3934,48 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
|
||||
public byte[] downloadVideoOrImage(String url) {
|
||||
try (CloseableHttpClient client = HttpClients.createDefault();
|
||||
InputStream in = client.execute(new HttpGet(url)).getEntity().getContent()) {
|
||||
return IOUtils.toByteArray(in);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
int maxRetries = 3;
|
||||
int retryDelayMs = 1000;
|
||||
IOException lastException = null;
|
||||
|
||||
for (int attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return downloadWithTimeout(url, 30000, 60000);
|
||||
} catch (IOException e) {
|
||||
lastException = e;
|
||||
log.warn("下载失败 (尝试 {}/{}): {}", attempt, maxRetries, e.getMessage());
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
try {
|
||||
Thread.sleep((long) retryDelayMs * attempt); // 递增延迟
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new RuntimeException(ie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException("下载失败,已重试 " + maxRetries + " 次", lastException);
|
||||
}
|
||||
|
||||
private byte[] downloadWithTimeout(String url, int connectTimeout, int socketTimeout) throws IOException {
|
||||
RequestConfig requestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(connectTimeout)
|
||||
.setSocketTimeout(socketTimeout)
|
||||
.setConnectionRequestTimeout(connectTimeout)
|
||||
.build();
|
||||
|
||||
try (CloseableHttpClient client = HttpClients.custom()
|
||||
.setDefaultRequestConfig(requestConfig)
|
||||
.build();
|
||||
CloseableHttpResponse response = client.execute(new HttpGet(url))) {
|
||||
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
if (statusCode != 200) {
|
||||
throw new IOException("HTTP Error: " + statusCode);
|
||||
}
|
||||
return IOUtils.toByteArray(response.getEntity().getContent());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4172,11 +4316,11 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
// 处理不同状态
|
||||
switch (statusEnum) {
|
||||
case TASK_NOT_FOUND:
|
||||
// 审核没过
|
||||
// 审核没过
|
||||
case REQUEST_MODERATED:
|
||||
// 审核没过
|
||||
// 审核没过
|
||||
case CONTENT_MODERATED:
|
||||
// 出错
|
||||
// 出错
|
||||
case ERROR:
|
||||
return "Fail";
|
||||
case PENDING_F:
|
||||
@@ -4295,7 +4439,7 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
MotionModeEnum motionModeEnum = MotionModeEnum.of(poseTransformDTO.getMode());
|
||||
switch (motionModeEnum) {
|
||||
case POSE_TO_VIDEO:
|
||||
params.put("pose_id", poseTransformDTO.getPoseId());
|
||||
params.put("pose_id", poseTransformDTO.getPoseId().toString());
|
||||
params.put("image_url", poseTransformDTO.getProductImage());
|
||||
break;
|
||||
case PROMPT_TO_VIDEO:
|
||||
|
||||
656
src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java
Normal file
656
src/main/java/com/ai/da/service/impl/GlobalAwardServiceImpl.java
Normal file
@@ -0,0 +1,656 @@
|
||||
package com.ai.da.service.impl;
|
||||
|
||||
import com.ai.da.common.config.exception.BusinessException;
|
||||
import com.ai.da.common.enums.AuthenticationOperationTypeEnum;
|
||||
import com.ai.da.common.utils.*;
|
||||
import com.ai.da.mapper.primary.AccountMapper;
|
||||
import com.ai.da.mapper.primary.ContestantMapper;
|
||||
import com.ai.da.mapper.primary.NotificationMapper;
|
||||
import com.ai.da.mapper.primary.entity.Account;
|
||||
import com.ai.da.mapper.primary.entity.Contestant;
|
||||
import com.ai.da.mapper.primary.entity.Notification;
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.dto.PublishSysNotificationDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.model.vo.PageVisitCountVO;
|
||||
import com.ai.da.service.GlobalAwardService;
|
||||
import com.ai.da.service.MessageCenterService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
|
||||
@Resource
|
||||
private ContestantMapper contestantMapper;
|
||||
|
||||
private final AccountMapper accountMapper;
|
||||
|
||||
private final MessageCenterService messageCenterService;
|
||||
|
||||
private final NotificationMapper notificationMapper;
|
||||
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
@Value("${file.upload.temp.dir}")
|
||||
private String uploadDir;
|
||||
|
||||
private static final DateTimeFormatter YYYY_MM_DD = DateTimeFormatter.ofPattern("yyyy/MM");
|
||||
|
||||
private static final String tokenCacheKey = AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + ":";
|
||||
|
||||
@Value("${minio.bucket:contestants}")
|
||||
private String minioBucket;
|
||||
|
||||
@Value("${global.award.link}")
|
||||
private String link;
|
||||
|
||||
@Resource
|
||||
private MinioUtil minioUtil;
|
||||
|
||||
@Override
|
||||
public String uploadPdf(MultipartFile file, String email) throws Exception {
|
||||
validatePdf(file);
|
||||
String path = storeFile(file, email, "pdf");
|
||||
return path;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadVideo(MultipartFile file, String email) throws Exception {
|
||||
validateVideo(file);
|
||||
String path = storeFile(file, email, "video");
|
||||
return path;
|
||||
}
|
||||
|
||||
private void validatePdf(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("File is empty.");
|
||||
}
|
||||
String ct = file.getContentType();
|
||||
if (ct == null || !ct.toLowerCase().contains("pdf")) {
|
||||
throw new BusinessException("Only PDF files are allowed.");
|
||||
}
|
||||
// size limit example 20MB
|
||||
if (file.getSize() > 20L * 1024 * 1024) {
|
||||
throw new BusinessException("PDF file size exceeds the limit.");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateVideo(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new BusinessException("File is empty.");
|
||||
}
|
||||
String ct = file.getContentType();
|
||||
if (ct == null || !(ct.toLowerCase().contains("mp4") || ct.toLowerCase().contains("video") )) {
|
||||
throw new BusinessException("Invalid video file type.");
|
||||
}
|
||||
// size limit example 100MB
|
||||
if (file.getSize() > 100L * 1024 * 1024) {
|
||||
throw new BusinessException("Video file size exceeds the limit.");
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeEmail(String email) {
|
||||
if (email == null) {
|
||||
return "anonymous";
|
||||
}
|
||||
return email.replaceAll("[^a-zA-Z0-9]", "_");
|
||||
}
|
||||
|
||||
private String storeFile(MultipartFile file, String email, String kind) throws IOException {
|
||||
String normalized = normalizeEmail(email);
|
||||
String datePart = LocalDateTime.now().format(YYYY_MM_DD);
|
||||
String ext = "";
|
||||
String original = file.getOriginalFilename();
|
||||
if (original != null && original.contains(".")) {
|
||||
ext = original.substring(original.lastIndexOf('.'));
|
||||
}
|
||||
String filename = System.currentTimeMillis() + "_" + UUID.randomUUID().toString() + ext;
|
||||
String relativePath = "contestants/" + normalized + "/" + datePart + "/" + filename;
|
||||
|
||||
String uploadedPath = minioUtil.upload(minioBucket, relativePath, file, null);
|
||||
log.info("uploaded via MinioUtil: {}", uploadedPath);
|
||||
return uploadedPath;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> saveContestant(ContestantDTO request) {
|
||||
Map<String,Object> resp = new HashMap<>();
|
||||
if (request.getEmail() == null) {
|
||||
throw new BusinessException("Email is required.");
|
||||
}
|
||||
|
||||
checkSecurityToken(request.getEmail(), request.getSecureToken());
|
||||
|
||||
QueryWrapper<Contestant> qw = new QueryWrapper<>();
|
||||
qw.eq("email", request.getEmail());
|
||||
Contestant existing = contestantMapper.selectOne(qw);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (existing == null) {
|
||||
// 通过行锁 + 重试机制保证 contestant_number 在并发下自增分配
|
||||
final int maxAttempts = 5;
|
||||
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
// 获取当前最大 contestant_number 并加行锁(LIMIT 1 FOR UPDATE)
|
||||
QueryWrapper<Contestant> qMax = new QueryWrapper<>();
|
||||
qMax.isNotNull("contestant_number");
|
||||
qMax.orderByDesc("contestant_number");
|
||||
qMax.last("LIMIT 1 FOR UPDATE");
|
||||
Contestant last = contestantMapper.selectOne(qMax);
|
||||
Integer nextNumber = (last == null || last.getContestantNumber() == null) ? 10000 : last.getContestantNumber() + 1;
|
||||
|
||||
Contestant toInsert = Contestant.builder()
|
||||
.email(request.getEmail())
|
||||
.firstName(request.getFirstName())
|
||||
.lastName(request.getLastName())
|
||||
.gender(request.getGender())
|
||||
.occupation(request.getOccupation())
|
||||
.age(request.getAge())
|
||||
.countryRegionCity(request.getCountryRegionCity())
|
||||
.phoneNumber(request.getPhoneNumber())
|
||||
.designTitle(request.getDesignTitle())
|
||||
.designDescription(request.getDesignDescription())
|
||||
.pdfPath(request.getPdfPath())
|
||||
.videoPath(request.getVideoPath())
|
||||
.videoDuration(request.getVideoDuration())
|
||||
.videoSize(request.getVideoSize())
|
||||
.pdfSize(request.getPdfSize())
|
||||
.contestantNumber(nextNumber)
|
||||
.portfolioUrl(request.getPortfolioUrl())
|
||||
.createdAt(now)
|
||||
.updatedAt(now)
|
||||
.build();
|
||||
|
||||
contestantMapper.insert(toInsert);
|
||||
|
||||
resp.put("success", true);
|
||||
sendSiteMsg(toInsert.getId(), toInsert.getEmail());
|
||||
return resp;
|
||||
} catch (Exception e) {
|
||||
log.warn("Attempt {} to assign contestant_number failed", attempt, e);
|
||||
String msg = e.getMessage() == null ? "" : e.getMessage().toLowerCase();
|
||||
if ((msg.contains("duplicate") || msg.contains("uniq_contestant_number") || msg.contains("contestant_number")) && attempt < maxAttempts) {
|
||||
try {
|
||||
Thread.sleep(100L);
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
throw new BusinessException("Failed to assign contestant number after retries.");
|
||||
} else {
|
||||
// update existing contestant
|
||||
existing.setFirstName(request.getFirstName());
|
||||
existing.setLastName(request.getLastName());
|
||||
existing.setGender(request.getGender());
|
||||
existing.setOccupation(request.getOccupation());
|
||||
existing.setAge(request.getAge());
|
||||
existing.setCountryRegionCity(request.getCountryRegionCity());
|
||||
existing.setPhoneNumber(request.getPhoneNumber());
|
||||
existing.setDesignTitle(request.getDesignTitle());
|
||||
existing.setDesignDescription(request.getDesignDescription());
|
||||
existing.setPdfPath(request.getPdfPath());
|
||||
existing.setVideoPath(request.getVideoPath());
|
||||
existing.setVideoDuration(request.getVideoDuration());
|
||||
existing.setVideoSize(request.getVideoSize());
|
||||
existing.setPdfSize(request.getPdfSize());
|
||||
existing.setPortfolioUrl(request.getPortfolioUrl());
|
||||
existing.setUpdatedAt(now);
|
||||
contestantMapper.updateById(existing);
|
||||
resp.put("success", true);
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] exportContestants() throws Exception {
|
||||
List<Contestant> list = contestantMapper.selectList(new QueryWrapper<>());
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(); ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
Sheet sheet = workbook.createSheet("contestants");
|
||||
int rowIdx = 0;
|
||||
Row header = sheet.createRow(rowIdx++);
|
||||
String[] headers = new String[] {
|
||||
"contestantNumber", "email", "firstName", "lastName", "gender", "occupation",
|
||||
"age", "countryRegionCity", "phoneNumber", "designTitle", "designDescription",
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "portfolioUrl", "createdAt", "updatedAt"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell c = header.createCell(i);
|
||||
c.setCellValue(headers[i]);
|
||||
}
|
||||
|
||||
for (Contestant cst : list) {
|
||||
Row r = sheet.createRow(rowIdx++);
|
||||
int ci = 0;
|
||||
r.createCell(ci++).setCellValue(cst.getContestantNumber() == null ? "" : cst.getContestantNumber().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getEmail() == null ? "" : cst.getEmail());
|
||||
r.createCell(ci++).setCellValue(cst.getFirstName() == null ? "" : cst.getFirstName());
|
||||
r.createCell(ci++).setCellValue(cst.getLastName() == null ? "" : cst.getLastName());
|
||||
r.createCell(ci++).setCellValue(cst.getGender() == null ? "" : cst.getGender());
|
||||
r.createCell(ci++).setCellValue(cst.getOccupation() == null ? "" : cst.getOccupation());
|
||||
r.createCell(ci++).setCellValue(cst.getAge() == null ? "" : cst.getAge().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getCountryRegionCity() == null ? "" : cst.getCountryRegionCity());
|
||||
r.createCell(ci++).setCellValue(cst.getPhoneNumber() == null ? "" : cst.getPhoneNumber());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignTitle() == null ? "" : cst.getDesignTitle());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignDescription() == null ? "" : cst.getDesignDescription());
|
||||
r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoDuration() == null ? "" : cst.getVideoDuration().toString());
|
||||
if (cst.getVideoSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double vMb = cst.getVideoSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", vMb));
|
||||
}
|
||||
if (cst.getPdfSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double pMb = cst.getPdfSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", pMb));
|
||||
}
|
||||
r.createCell(ci++).setCellValue(cst.getPortfolioUrl() == null ? "" : cst.getPortfolioUrl());
|
||||
r.createCell(ci++).setCellValue(cst.getCreatedAt() == null ? "" : cst.getCreatedAt().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getUpdatedAt() == null ? "" : cst.getUpdatedAt().toString());
|
||||
}
|
||||
|
||||
workbook.write(out);
|
||||
out.flush();
|
||||
return out.toByteArray();
|
||||
} catch (IOException e) {
|
||||
log.error("export contestants failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveContestantsToLocal() throws Exception {
|
||||
List<Contestant> list = contestantMapper.selectList(new QueryWrapper<>());
|
||||
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
|
||||
String ts = LocalDateTime.now().format(fmt);
|
||||
Path exportDir = Paths.get(uploadDir == null ? "uploads" : uploadDir, "exports");
|
||||
Files.createDirectories(exportDir);
|
||||
Path outPath = exportDir.resolve("contestants_" + ts + ".xlsx");
|
||||
|
||||
try (XSSFWorkbook workbook = new XSSFWorkbook(); FileOutputStream fos = new FileOutputStream(outPath.toFile())) {
|
||||
Sheet sheet = workbook.createSheet("contestants");
|
||||
int rowIdx = 0;
|
||||
Row header = sheet.createRow(rowIdx++);
|
||||
String[] headers = new String[] {
|
||||
"contestantNumber", "email", "firstName", "lastName", "gender", "occupation",
|
||||
"age", "countryRegionCity", "phoneNumber", "designTitle", "designDescription",
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "portfolioUrl", "createdAt", "updatedAt"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell c = header.createCell(i);
|
||||
c.setCellValue(headers[i]);
|
||||
}
|
||||
|
||||
for (Contestant cst : list) {
|
||||
Row r = sheet.createRow(rowIdx++);
|
||||
int ci = 0;
|
||||
r.createCell(ci++).setCellValue(cst.getContestantNumber() == null ? "" : cst.getContestantNumber().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getEmail() == null ? "" : cst.getEmail());
|
||||
r.createCell(ci++).setCellValue(cst.getFirstName() == null ? "" : cst.getFirstName());
|
||||
r.createCell(ci++).setCellValue(cst.getLastName() == null ? "" : cst.getLastName());
|
||||
r.createCell(ci++).setCellValue(cst.getGender() == null ? "" : cst.getGender());
|
||||
r.createCell(ci++).setCellValue(cst.getOccupation() == null ? "" : cst.getOccupation());
|
||||
r.createCell(ci++).setCellValue(cst.getAge() == null ? "" : cst.getAge().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getCountryRegionCity() == null ? "" : cst.getCountryRegionCity());
|
||||
r.createCell(ci++).setCellValue(cst.getPhoneNumber() == null ? "" : cst.getPhoneNumber());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignTitle() == null ? "" : cst.getDesignTitle());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignDescription() == null ? "" : cst.getDesignDescription());
|
||||
r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoDuration() == null ? "" : cst.getVideoDuration().toString());
|
||||
if (cst.getVideoSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double vMb = cst.getVideoSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", vMb));
|
||||
}
|
||||
if (cst.getPdfSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
double pMb = cst.getPdfSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", pMb));
|
||||
}
|
||||
r.createCell(ci++).setCellValue(cst.getPortfolioUrl() == null ? "" : cst.getPortfolioUrl());
|
||||
r.createCell(ci++).setCellValue(cst.getCreatedAt() == null ? "" : cst.getCreatedAt().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getUpdatedAt() == null ? "" : cst.getUpdatedAt().toString());
|
||||
}
|
||||
|
||||
workbook.write(fos);
|
||||
fos.flush();
|
||||
log.info("Exported contestants to local file: {}", outPath.toString());
|
||||
} catch (IOException e) {
|
||||
log.error("save contestants to local failed", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContestantDTO getContestantByID(String id) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("id is required.");
|
||||
}
|
||||
Contestant existing = contestantMapper.selectById(id);
|
||||
if (existing == null) {
|
||||
return null;
|
||||
}
|
||||
ContestantDTO dto = new ContestantDTO();
|
||||
// dto.setEmail(existing.getEmail());
|
||||
dto.setFirstName(existing.getFirstName());
|
||||
dto.setLastName(existing.getLastName());
|
||||
dto.setGender(existing.getGender());
|
||||
dto.setOccupation(existing.getOccupation());
|
||||
dto.setAge(existing.getAge());
|
||||
dto.setCountryRegionCity(existing.getCountryRegionCity());
|
||||
dto.setPhoneNumber(existing.getPhoneNumber());
|
||||
dto.setDesignTitle(existing.getDesignTitle());
|
||||
dto.setDesignDescription(existing.getDesignDescription());
|
||||
dto.setPdfPath(existing.getPdfPath());
|
||||
dto.setVideoPath(existing.getVideoPath());
|
||||
dto.setVideoDuration(existing.getVideoDuration());
|
||||
dto.setPdfSize(existing.getPdfSize());
|
||||
dto.setVideoSize(existing.getVideoSize());
|
||||
dto.setPortfolioUrl(existing.getPortfolioUrl());
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否符合申请要求,发送验证码
|
||||
* @param email AiDA邮箱
|
||||
*/
|
||||
public void checkEmail(String email) {
|
||||
List<Integer> validRole = Arrays.asList(1, 2, 7, 8);
|
||||
// 1. 验证邮箱在aida中有无账号
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda().eq(Account::getUserEmail, email);
|
||||
List<Account> accounts = accountMapper.selectList(queryWrapper);
|
||||
if (accounts.isEmpty()) {
|
||||
throw new BusinessException("Please register and subscribe to AiDA, then resubmit your application.");
|
||||
}
|
||||
|
||||
// 2. 验证账号是否是付费用户(如果首次提交是,但是修改的时候已经不是了,how?不允许修改吗)
|
||||
if (validRole.contains(accounts.getFirst().getSystemUser())) {
|
||||
String randomVerifyCode = RandomsUtil.generateVerifyCode(100000L, 999999L);
|
||||
LocalCacheUtils.setVerifyCodeCache(
|
||||
AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + "_" + email, randomVerifyCode);
|
||||
SendEmailUtil.send(email, null,
|
||||
SendEmailUtil.LOGIN_TEMPLATE_ID, randomVerifyCode);
|
||||
} else {
|
||||
throw new BusinessException("Please subscribe to AiDA, then resubmit your application.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码是否正确
|
||||
* @param email 邮箱
|
||||
* @param otp 一次性验证码
|
||||
* @return 临时token和之前提交的表单内容
|
||||
*/
|
||||
public CheckOTPVO checkCode(String email, String otp) {
|
||||
String otpCache = LocalCacheUtils.getVerifyCodeCache(AuthenticationOperationTypeEnum.GLOBAL_AWARD.name() + "_" + email);
|
||||
assert otpCache != null;
|
||||
if (otpCache.equals(otp)) {
|
||||
// 1. 生成唯一token
|
||||
String secureToken = UUID.randomUUID().toString().replace("-", "");
|
||||
redisUtil.addToString(tokenCacheKey + email, secureToken, 3 * 24 * 60 * 60L);
|
||||
|
||||
return new CheckOTPVO(secureToken, getContestantByID(email));
|
||||
} else {
|
||||
throw new BusinessException("Verification code is incorrect. Please try again.");
|
||||
}
|
||||
}
|
||||
|
||||
public void checkSecurityToken(String email, String securityToken) {
|
||||
String key = tokenCacheKey + email;
|
||||
if (StringUtils.isBlank(securityToken)) {
|
||||
log.error("security token 缺失");
|
||||
throw new BusinessException("Please complete email verification first.");
|
||||
}
|
||||
|
||||
String tokenCache = redisUtil.getFromString(key);
|
||||
if (StringUtils.isBlank(tokenCache)) {
|
||||
log.error("security token 过期");
|
||||
throw new BusinessException("Email verification has expired. Please verify again.");
|
||||
} else if (!tokenCache.equals(securityToken)){
|
||||
log.error("security token 与缓存不符");
|
||||
throw new BusinessException("Identity verification failed. Please complete email verification first.");
|
||||
}
|
||||
}
|
||||
|
||||
// 发送站内信
|
||||
public void sendSiteMsg(String applicationId, String email) {
|
||||
Long userId;
|
||||
QueryWrapper<Account> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda().eq(Account::getUserEmail, email);
|
||||
List<Account> accounts = accountMapper.selectList(queryWrapper);
|
||||
if (accounts.isEmpty()) {
|
||||
throw new BusinessException("Please register and subscribe to AiDA, then resubmit your application.");
|
||||
}else {
|
||||
userId = accounts.get(0).getId();
|
||||
}
|
||||
PublishSysNotificationDTO sysNotificationDTO = new PublishSysNotificationDTO();
|
||||
Notification notification = new Notification();
|
||||
notification.setType("system");
|
||||
notification.setReceiverId(userId);
|
||||
sysNotificationDTO.setTitle("System Notification 系统通知");
|
||||
// todo
|
||||
sysNotificationDTO.setContent(link + applicationId);
|
||||
notification.setContent(JSON.toJSONString(sysNotificationDTO));
|
||||
notification.setIsRead(0);
|
||||
notification.setCreateTime(LocalDateTime.now());
|
||||
notificationMapper.insert(notification);
|
||||
// 这里推送消息是在接受到视频生成结束后发生的,所以UserContext中没有用户信息
|
||||
messageCenterService.pushMessage("system", userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception {
|
||||
if (minContestantNumber == null || maxContestantNumber == null) {
|
||||
throw new BusinessException("minContestantNumber and maxContestantNumber are required.");
|
||||
}
|
||||
if (minContestantNumber > maxContestantNumber) {
|
||||
throw new BusinessException("minContestantNumber cannot be greater than maxContestantNumber.");
|
||||
}
|
||||
|
||||
// 1. 根据 contestantNumber 范围查询参赛者
|
||||
QueryWrapper<Contestant> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda()
|
||||
.ge(Contestant::getContestantNumber, minContestantNumber)
|
||||
.le(Contestant::getContestantNumber, maxContestantNumber)
|
||||
.orderByAsc(Contestant::getContestantNumber);
|
||||
List<Contestant> contestants = contestantMapper.selectList(queryWrapper);
|
||||
|
||||
if (contestants.isEmpty()) {
|
||||
log.info("No contestants found in range [{}, {}]", minContestantNumber, maxContestantNumber);
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// 2. 在内存中构建 ZIP
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(baos)) {
|
||||
|
||||
for (Contestant contestant : contestants) {
|
||||
Integer contestantNumber = contestant.getContestantNumber();
|
||||
if (contestantNumber == null) {
|
||||
log.warn("Contestant {} has no contestantNumber, skipping", contestant.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
String dirPrefix = contestantNumber + "/";
|
||||
|
||||
// 添加 PDF 文件
|
||||
String pdfPath = contestant.getPdfPath();
|
||||
if (StringUtils.isNotBlank(pdfPath)) {
|
||||
addMinioFileToZip(zos, pdfPath, dirPrefix + "design.pdf");
|
||||
}
|
||||
|
||||
// 添加视频文件
|
||||
String videoPath = contestant.getVideoPath();
|
||||
if (StringUtils.isNotBlank(videoPath)) {
|
||||
String fileName = videoPath.contains("/") ?
|
||||
videoPath.substring(videoPath.lastIndexOf("/") + 1) : "video.mp4";
|
||||
addMinioFileToZip(zos, videoPath, dirPrefix + fileName);
|
||||
}
|
||||
|
||||
// 添加参赛者信息 txt 文件
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("=== Contestant Information ===\n\n");
|
||||
sb.append("ID: ").append(nullSafe(contestant.getId())).append("\n");
|
||||
sb.append("Email: ").append(nullSafe(contestant.getEmail())).append("\n");
|
||||
sb.append("Contestant Number: ").append(contestantNumber).append("\n");
|
||||
sb.append("First Name: ").append(nullSafe(contestant.getFirstName())).append("\n");
|
||||
sb.append("Last Name: ").append(nullSafe(contestant.getLastName())).append("\n");
|
||||
sb.append("Gender: ").append(nullSafe(contestant.getGender())).append("\n");
|
||||
sb.append("Occupation: ").append(nullSafe(contestant.getOccupation())).append("\n");
|
||||
sb.append("Age: ").append(contestant.getAge() != null ? contestant.getAge() : "N/A").append("\n");
|
||||
sb.append("Country/Region/City: ").append(nullSafe(contestant.getCountryRegionCity())).append("\n");
|
||||
sb.append("Phone Number: ").append(nullSafe(contestant.getPhoneNumber())).append("\n");
|
||||
sb.append("Design Title: ").append(nullSafe(contestant.getDesignTitle())).append("\n");
|
||||
sb.append("Design Description: ").append(nullSafe(contestant.getDesignDescription())).append("\n");
|
||||
sb.append("PDF Path: ").append(nullSafe(pdfPath)).append("\n");
|
||||
sb.append("PDF Size (bytes): ").append(contestant.getPdfSize() != null ? contestant.getPdfSize() : "N/A").append("\n");
|
||||
sb.append("Video Path: ").append(nullSafe(videoPath)).append("\n");
|
||||
sb.append("Video Duration (seconds): ").append(contestant.getVideoDuration() != null ? contestant.getVideoDuration() : "N/A").append("\n");
|
||||
sb.append("Video Size (bytes): ").append(contestant.getVideoSize() != null ? contestant.getVideoSize() : "N/A").append("\n");
|
||||
sb.append("Portfolio URL: ").append(nullSafe(contestant.getPortfolioUrl())).append("\n");
|
||||
sb.append("Created At: ").append(contestant.getCreatedAt() != null ? contestant.getCreatedAt() : "N/A").append("\n");
|
||||
sb.append("Updated At: ").append(contestant.getUpdatedAt() != null ? contestant.getUpdatedAt() : "N/A").append("\n");
|
||||
|
||||
ZipEntry infoEntry = new ZipEntry(dirPrefix + "contestant_info.txt");
|
||||
zos.putNextEntry(infoEntry);
|
||||
zos.write(sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
zos.closeEntry();
|
||||
log.info("Added contestant {} info to zip", contestantNumber);
|
||||
}
|
||||
|
||||
zos.finish();
|
||||
log.info("ZIP built for {} contestants, size: {} bytes", contestants.size(), baos.size());
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 MinIO 文件流式写入 ZIP,不落盘
|
||||
* @param zos ZIP 输出流
|
||||
* @param minioPath MinIO 路径(格式: bucketName/objectPath)
|
||||
* @param entryName ZIP 条目名称
|
||||
*/
|
||||
private void addMinioFileToZip(java.util.zip.ZipOutputStream zos, String minioPath, String entryName) {
|
||||
if (StringUtils.isBlank(minioPath)) {
|
||||
return;
|
||||
}
|
||||
int index = minioPath.indexOf("/");
|
||||
if (index == -1) {
|
||||
log.warn("Invalid MinIO path: {}", minioPath);
|
||||
return;
|
||||
}
|
||||
String bucketName = minioPath.substring(0, index);
|
||||
String objectName = minioPath.substring(index + 1);
|
||||
|
||||
try (InputStream in = minioUtil.download(bucketName, objectName)) {
|
||||
ZipEntry entry = new ZipEntry(entryName);
|
||||
zos.putNextEntry(entry);
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
zos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
zos.closeEntry();
|
||||
log.info("Added {} to zip ({} bytes)", entryName, entry.getSize());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add {} to zip: {}", entryName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContestantCountVO getContestantCount() {
|
||||
long count = contestantMapper.selectCount(null);
|
||||
Integer maxContestantNumber = null;
|
||||
QueryWrapper<Contestant> qMax = new QueryWrapper<>();
|
||||
qMax.isNotNull("contestant_number");
|
||||
qMax.orderByDesc("contestant_number");
|
||||
qMax.last("LIMIT 1");
|
||||
Contestant last = contestantMapper.selectOne(qMax);
|
||||
if (last != null) {
|
||||
maxContestantNumber = last.getContestantNumber();
|
||||
}
|
||||
return ContestantCountVO.builder()
|
||||
.count(count)
|
||||
.maxContestantNumber(maxContestantNumber)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String nullSafe(String value) {
|
||||
return value != null ? value : "N/A";
|
||||
}
|
||||
|
||||
private static final String RAW_VISIT_COUNT_KEY = "GLOBAL_AWARD:visit:raw";
|
||||
private static final String UNIQUE_VISIT_SET_KEY = "GLOBAL_AWARD:visit:unique";
|
||||
private static final String SESSION_VISIT_KEY_PREFIX = "GLOBAL_AWARD:visit:session:";
|
||||
private static final long SESSION_DEDUP_SECONDS = 5L;
|
||||
|
||||
@Override
|
||||
public void recordPageVisit(String sessionId) {
|
||||
redisUtil.increaseCount(RAW_VISIT_COUNT_KEY);
|
||||
|
||||
if (StringUtils.isNotBlank(sessionId)) {
|
||||
String sessionKey = SESSION_VISIT_KEY_PREFIX + sessionId;
|
||||
if (!redisUtil.hasKey(sessionKey)) {
|
||||
redisUtil.increaseCount(UNIQUE_VISIT_SET_KEY);
|
||||
redisUtil.addToString(sessionKey, "1", SESSION_DEDUP_SECONDS);
|
||||
}
|
||||
} else {
|
||||
redisUtil.increaseCount(UNIQUE_VISIT_SET_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageVisitCountVO getPageVisitCount() {
|
||||
Long raw = redisUtil.getIncrementCount(RAW_VISIT_COUNT_KEY);
|
||||
Long unique = redisUtil.getIncrementCount(UNIQUE_VISIT_SET_KEY);
|
||||
return PageVisitCountVO.builder()
|
||||
.rawVisitCount(raw != null ? raw : 0L)
|
||||
.uniqueVisitCount(unique != null ? unique : 0L)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import io.netty.util.internal.StringUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -53,7 +54,11 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
private final PythonTAllInfoService pythonTAllInfoService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public LibraryModelPointVO saveOrEditTemplatePoint(LibraryModelPointDTO libraryModelPointDTO) {
|
||||
// 参数校验
|
||||
validateInputParams(libraryModelPointDTO);
|
||||
|
||||
LibraryModelPointVO libraryModelPointVO = CopyUtil.copyObject(libraryModelPointDTO, LibraryModelPointVO.class);
|
||||
|
||||
// 不管是保存还是另存为,都需要传模特的libraryId
|
||||
@@ -71,7 +76,8 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
// 更新模特图片
|
||||
if (flag) {
|
||||
libModel.setUrl(url);
|
||||
libModel.setMd5(MD5Utils.encryptFile(minioUtil.getPreSignedUrl(url, CommonConstant.MINIO_IMAGE_EXPIRE_TIME), false));
|
||||
String preSignedUrl = minioUtil.getPreSignedUrl(url, CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
|
||||
libModel.setMd5(MD5Utils.encryptFile(preSignedUrl, false));
|
||||
List<Integer> imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(url);
|
||||
libModel.setWidth(imagesWidthAndHeight.get(0));
|
||||
libModel.setHigh(imagesWidthAndHeight.get(1));
|
||||
@@ -104,25 +110,10 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
|
||||
} else {
|
||||
// 不覆盖,即另存为
|
||||
// 新增模特library信息
|
||||
Library saveAsModel = new Library();
|
||||
saveAsModel.setAccountId(libModel.getAccountId());
|
||||
saveAsModel.setLevel1Type(libModel.getLevel1Type());
|
||||
saveAsModel.setLevel2Type(libModel.getLevel2Type());
|
||||
String ageGroup = StringUtil.isNullOrEmpty(libModel.getLevel3Type()) ? "Adult" : libModel.getLevel3Type();
|
||||
saveAsModel.setLevel3Type(ageGroup);
|
||||
saveAsModel.setName(DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD));
|
||||
saveAsModel.setUrl(url);
|
||||
saveAsModel.setMd5(MD5Utils.encryptFile(minioUtil.getPreSignedUrl(url, CommonConstant.MINIO_IMAGE_EXPIRE_TIME), false));
|
||||
List<Integer> imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(url);
|
||||
saveAsModel.setWidth(imagesWidthAndHeight.get(0));
|
||||
saveAsModel.setHigh(imagesWidthAndHeight.get(1));
|
||||
saveAsModel.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
||||
libraryService.save(saveAsModel);
|
||||
// 更新新的模特在library中的id,用于后面新建模特点位信息用
|
||||
libraryModelPointDTO.setLibraryId(saveAsModel.getId());
|
||||
Library saveAsModel = createNewLibraryCopy(libModel, libraryModelPointDTO);
|
||||
|
||||
// 新增模特点位信息
|
||||
libraryModelPointDTO.setLibraryId(saveAsModel.getId()); // 更新libraryId为新创建的模型ID
|
||||
LibraryModelPoint libraryModelPoint = resolvePoint(libraryModelPointDTO);
|
||||
libraryModelPoint.setModelType("Library");
|
||||
libraryModelPoint.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
||||
@@ -130,22 +121,50 @@ public class LibraryModelPointServiceImpl extends ServiceImpl<LibraryModelPointM
|
||||
libraryModelPointVO.setTemplateId(libraryModelPoint.getId());
|
||||
libraryModelPointVO.setRelationId(libraryModelPoint.getRelationId());
|
||||
}
|
||||
//编辑
|
||||
/*if (!StringUtils.isEmpty(libraryModelPointDTO.getModelSex())) {
|
||||
Library byId = libraryService.getById(libraryModelPointDTO.getLibraryId());
|
||||
if (!byId.getLevel2Type().equals(libraryModelPointDTO.getModelSex())) {
|
||||
if (byId.getLevel2Type().equals(Sex.FEMALE.getValue())) {
|
||||
libraryService.checkModel(Sex.FEMALE.getValue(), Collections.singletonList(byId.getId()), 1);
|
||||
}else {
|
||||
libraryService.checkModel(Sex.MALE.getValue(), Collections.singletonList(byId.getId()), 1);
|
||||
}
|
||||
byId.setLevel2Type(libraryModelPointDTO.getModelSex());
|
||||
libraryService.updateById(byId);
|
||||
}
|
||||
}*/
|
||||
return libraryModelPointVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证输入参数
|
||||
*/
|
||||
private void validateInputParams(LibraryModelPointDTO libraryModelPointDTO) {
|
||||
if (libraryModelPointDTO == null) {
|
||||
throw new BusinessException("libraryModelPointDTO cannot be null");
|
||||
}
|
||||
if (libraryModelPointDTO.getLibraryId() == null || libraryModelPointDTO.getLibraryId() <= 0) {
|
||||
throw new BusinessException("libraryId is required");
|
||||
}
|
||||
if (StringUtils.isEmpty(libraryModelPointDTO.getModelPath())) {
|
||||
throw new BusinessException("modelPath is required");
|
||||
}
|
||||
if (StringUtils.isEmpty(libraryModelPointDTO.getTimeZone())) {
|
||||
throw new BusinessException("timeZone is required");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新的库模型副本
|
||||
*/
|
||||
private Library createNewLibraryCopy(Library originalModel, LibraryModelPointDTO libraryModelPointDTO) {
|
||||
// 新增模特library信息
|
||||
Library saveAsModel = new Library();
|
||||
saveAsModel.setAccountId(originalModel.getAccountId());
|
||||
saveAsModel.setLevel1Type(originalModel.getLevel1Type());
|
||||
saveAsModel.setLevel2Type(originalModel.getLevel2Type());
|
||||
String ageGroup = StringUtil.isNullOrEmpty(originalModel.getLevel3Type()) ? "Adult" : originalModel.getLevel3Type();
|
||||
saveAsModel.setLevel3Type(ageGroup);
|
||||
saveAsModel.setName(DateUtil.dateToStr(new Date(), DateUtil.YYYY_MM_DD));
|
||||
saveAsModel.setUrl(libraryModelPointDTO.getModelPath());
|
||||
String preSignedUrl = minioUtil.getPreSignedUrl(libraryModelPointDTO.getModelPath(), CommonConstant.MINIO_IMAGE_EXPIRE_TIME);
|
||||
saveAsModel.setMd5(MD5Utils.encryptFile(preSignedUrl, false));
|
||||
List<Integer> imagesWidthAndHeight = minioUtil.getImagesWidthAndHeight(libraryModelPointDTO.getModelPath());
|
||||
saveAsModel.setWidth(imagesWidthAndHeight.get(0));
|
||||
saveAsModel.setHigh(imagesWidthAndHeight.get(1));
|
||||
saveAsModel.setCreateDate(DateUtil.getByTimeZone(libraryModelPointDTO.getTimeZone()));
|
||||
libraryService.save(saveAsModel);
|
||||
return saveAsModel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LibraryModelPointVO saveOrEditTemplatePointOld(LibraryModelPointDTO libraryModelPointDTO) {
|
||||
// Library library = libraryService.getById(libraryModelPointDTO.getLibraryId());
|
||||
|
||||
@@ -194,6 +194,8 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
if (!type.equals("system")) {
|
||||
// 个人未读消息
|
||||
count = getUnreadCountByType(type, receiverId);
|
||||
} else if (Objects.isNull(receiverId)) {
|
||||
count = 1L;
|
||||
} else {
|
||||
// 系统未读消息
|
||||
count = getUnreadSystemNotification(receiverId);
|
||||
@@ -253,12 +255,14 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
// 计算总的系统通知数量
|
||||
QueryWrapper<Notification> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda().eq(Notification::getType, "system")
|
||||
.gt(Notification::getCreateTime, account.getCreateDate())
|
||||
.and(wrapper -> wrapper
|
||||
.isNull(Notification::getReceiverId)
|
||||
.or()
|
||||
.eq(Notification::getReceiverId, receiverId)
|
||||
);
|
||||
if (Objects.nonNull(account)) {
|
||||
queryWrapper.lambda().gt(Notification::getCreateTime, account.getCreateDate());
|
||||
}
|
||||
Long totalSysCount = baseMapper.selectCount(queryWrapper);
|
||||
|
||||
// 计算单个用户读了多少条系统数据
|
||||
|
||||
@@ -31,6 +31,7 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
@@ -90,7 +91,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
}
|
||||
|
||||
public OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
||||
HttpServletRequest request, byte autoRenewal) {
|
||||
HttpServletRequest request) {
|
||||
|
||||
//获取商品信息
|
||||
// Product product = productMapper.selectById(amount);
|
||||
@@ -276,10 +277,11 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
|
||||
public void updateTotalFeeByOrderNo(String orderNo) {
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("order_no", orderNo);
|
||||
qw.eq("order_no", orderNo).in("trade_state", Arrays.asList("paid", "COMPLETED", ""));
|
||||
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qw);
|
||||
Float sum = paymentInfos.stream()
|
||||
.map(PaymentInfo::getPayerTotal)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(0f, Float::sum);
|
||||
|
||||
baseMapper.update(
|
||||
|
||||
@@ -258,6 +258,11 @@ public class PanToneServiceImpl extends ServiceImpl<PanToneMapper, PanTone> impl
|
||||
d.setH(getRgbByHsvBatchDTO.getH());
|
||||
d.setS(getRgbByHsvBatchDTO.getS());
|
||||
d.setV(getRgbByHsvBatchDTO.getV());
|
||||
// 不使用数据库中存储的RGB值,使用通过hsv计算得到的RGB值
|
||||
int[] rgb = PantoneUtils.hsvToRgb(d.getH(), d.getS(), d.getV());
|
||||
d.setR(rgb[0]);
|
||||
d.setG(rgb[1]);
|
||||
d.setB(rgb[2]);
|
||||
}
|
||||
});
|
||||
Map<Integer, PantoneVO> valueToPantoneVo = templateResposne.stream().collect(Collectors.toMap(
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.ai.da.service.impl;
|
||||
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.common.enums.PayTypeEnum;
|
||||
import com.ai.da.common.enums.PaymentInfoType;
|
||||
import com.ai.da.common.response.PageBaseResponse;
|
||||
import com.ai.da.common.utils.SpringUtils;
|
||||
import com.ai.da.mapper.primary.PaymentInfoMapper;
|
||||
import com.ai.da.mapper.primary.ProductCouponsMapper;
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.PaymentInfo;
|
||||
import com.ai.da.mapper.primary.entity.ProductCoupons;
|
||||
@@ -20,13 +22,17 @@ import com.google.gson.Gson;
|
||||
import com.paypal.orders.Order;
|
||||
import com.stripe.Stripe;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.Subscription;
|
||||
import com.stripe.model.*;
|
||||
import com.stripe.model.checkout.Session;
|
||||
import com.stripe.net.RequestOptions;
|
||||
import com.stripe.param.InvoicePaymentListParams;
|
||||
import com.stripe.param.InvoiceRetrieveParams;
|
||||
import com.stripe.param.SubscriptionRetrieveParams;
|
||||
import com.stripe.param.checkout.SessionRetrieveParams;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -49,6 +55,9 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
|
||||
@Resource
|
||||
private ProductCouponsMapper productCouponsMapper;
|
||||
|
||||
/**
|
||||
* 记录支付日志:微信支付
|
||||
* @param plainText
|
||||
@@ -194,38 +203,199 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
baseMapper.insert(paymentInfo);
|
||||
}
|
||||
|
||||
public void createOrUpdatePaymentInfoForStripe(Session session){
|
||||
String orderId = session.getMetadata().get("orderId");
|
||||
String status = session.getStatus();
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
/**
|
||||
* 为 Stripe Checkout Session 创建支付记录
|
||||
*
|
||||
* 策略:根据 session.getMode() 分流:
|
||||
* - mode=subscription:直接获取 session.getInvoice(),委托给 Invoice 版本(最完整)
|
||||
* - mode=payment:从 session.getPaymentIntentObject() 获取支付信息,兼容无 Invoice 场景
|
||||
*
|
||||
* @param session Stripe Checkout Session
|
||||
*/
|
||||
public void createOrUpdatePaymentInfoForStripe(Session session) {
|
||||
Stripe.apiKey = privateKey;
|
||||
|
||||
String sessionId = session.getId();
|
||||
String orderNo = session.getMetadata().get("orderId");
|
||||
String mode = session.getMode();
|
||||
String type = PaymentInfoType.CREDIT.getType();
|
||||
|
||||
// 从 Session 的 PaymentIntent 获取支付方式信息(两种 mode 都适用)
|
||||
Map<String, String> paymentMethodInfo = handlePaymentMethodBySession(session, mode);
|
||||
|
||||
String invoiceId = session.getInvoice();
|
||||
Invoice invoice = null;
|
||||
if (!StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
try {
|
||||
invoice = Invoice.retrieve(invoiceId);
|
||||
} catch (StripeException e) {
|
||||
log.warn("[createOrUpdatePaymentInfoForStripe(Session)] 订阅模式获取 Invoice 失败,降级为 payment 模式处理,sessionId={},error={}",
|
||||
sessionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
// subscription mode:获取 Invoice,委托给 Invoice 方法(传入已获取的 paymentMethodInfo)
|
||||
if ("subscription".equals(mode)) {
|
||||
if (invoice != null) {
|
||||
createOrUpdatePaymentInfoForStripe(invoice, paymentMethodInfo, session.getDiscounts());
|
||||
log.info("[createOrUpdatePaymentInfoForStripe(Session)] subscription 模式通过 Invoice 创建支付记录,invoiceId={}", invoiceId);
|
||||
return;
|
||||
}
|
||||
type = PaymentInfoType.NEW.getType();
|
||||
}
|
||||
|
||||
|
||||
// payment mode / 降级:使用 session 自有字段创建支付记录
|
||||
String status = session.getPaymentStatus();
|
||||
Long amountTotal = session.getAmountTotal();
|
||||
// stripe 的支付金额单位是分
|
||||
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||
|
||||
PaymentInfo paymentInfo = new PaymentInfo();
|
||||
paymentInfo.setOrderNo(orderId);
|
||||
paymentInfo.setOrderNo(orderNo);
|
||||
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
||||
paymentInfo.setTransactionId(sessionId);
|
||||
paymentInfo.setTransactionId(invoiceId);
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setPayerTotal(divide);
|
||||
Gson gson = new Gson();
|
||||
String json = gson.toJson(session);
|
||||
paymentInfo.setContent(json);
|
||||
// 获取订单信息
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId);
|
||||
if (!Objects.isNull(orderByOrderNo)){
|
||||
paymentInfo.setContent(gson.toJson(session));
|
||||
paymentInfo.setType(type);
|
||||
paymentInfo.setNotified(0);
|
||||
paymentInfo.setPaymentMethod(paymentMethodInfo.getOrDefault("paymentMethod", "N/A"));
|
||||
paymentInfo.setLast4(paymentMethodInfo.getOrDefault("last4", "N/A"));
|
||||
paymentInfo.setHostedInvoiceUrl(invoice == null ? null : invoice.getHostedInvoiceUrl());
|
||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (!Objects.isNull(orderByOrderNo)) {
|
||||
paymentInfo.setCountry(orderByOrderNo.getCountry());
|
||||
paymentInfo.setCity(orderByOrderNo.getCity());
|
||||
paymentInfo.setIpAddress(orderByOrderNo.getIpAddress());
|
||||
}
|
||||
|
||||
baseMapper.insert(paymentInfo);
|
||||
log.info("[createOrUpdatePaymentInfoForStripe(Session)] payment 模式创建支付记录,sessionId={},orderNo={}", sessionId, orderNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一获取支付方式信息
|
||||
* @param sessionId 可选的 sessionId
|
||||
* @param subscriptionId 可选的 subscriptionId
|
||||
* @return paymentMethodInfo Map,包含 paymentMethod 和 last4
|
||||
*/
|
||||
public Map<String, String> getPaymentMethodInfo(String sessionId, String subscriptionId) {
|
||||
PaymentMethod paymentMethod = null;
|
||||
|
||||
if (!StringUtil.isNullOrEmpty(sessionId)) {
|
||||
paymentMethod = getPaymentMethodBySessionId(sessionId);
|
||||
} else if (!StringUtil.isNullOrEmpty(subscriptionId)) {
|
||||
paymentMethod = getPaymentMethodBySubscriptionId(subscriptionId);
|
||||
}
|
||||
|
||||
return getPaymentMethodMap(paymentMethod);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Session 获取支付方式信息
|
||||
* @param session Stripe Checkout Session
|
||||
* @param mode session 模式:subscription 或 payment
|
||||
* @return paymentMethodInfo Map
|
||||
*/
|
||||
public Map<String, String> handlePaymentMethodBySession(Session session, String mode) {
|
||||
PaymentMethod paymentMethod;
|
||||
|
||||
if ("subscription".equals(mode)) {
|
||||
String subscriptionId = session.getSubscription();
|
||||
paymentMethod = getPaymentMethodBySubscriptionId(subscriptionId);
|
||||
} else {
|
||||
paymentMethod = getPaymentMethodBySessionId(session.getId());
|
||||
}
|
||||
|
||||
return getPaymentMethodMap(paymentMethod);
|
||||
}
|
||||
|
||||
public String getPromotionCodeByPromotionCodeId(String promotionCodeId) {
|
||||
if (StringUtil.isNullOrEmpty(promotionCodeId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
ProductCoupons promotionCode = productCouponsMapper.selectOne(new QueryWrapper<ProductCoupons>().lambda().eq(ProductCoupons::getPromotionCodeId, promotionCodeId));
|
||||
if (promotionCode == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return promotionCode.getPromotionCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 sessionId 获取 PaymentMethod
|
||||
* @param sessionId Stripe Session ID
|
||||
* @return PaymentMethod 对象
|
||||
*/
|
||||
private PaymentMethod getPaymentMethodBySessionId(String sessionId) {
|
||||
Stripe.apiKey = privateKey;
|
||||
SessionRetrieveParams params = SessionRetrieveParams.builder()
|
||||
.addExpand("payment_intent")
|
||||
.addExpand("payment_intent.payment_method")
|
||||
.build();
|
||||
|
||||
Session fullSession;
|
||||
try {
|
||||
fullSession = Session.retrieve(sessionId, params, RequestOptions.builder().build());
|
||||
} catch (StripeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
PaymentIntent paymentIntent = fullSession.getPaymentIntentObject();
|
||||
return paymentIntent != null ? paymentIntent.getPaymentMethodObject() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 subscriptionId 获取 PaymentMethod
|
||||
* @param subscriptionId Stripe Subscription ID
|
||||
* @return PaymentMethod 对象
|
||||
*/
|
||||
public PaymentMethod getPaymentMethodBySubscriptionId(String subscriptionId) {
|
||||
Stripe.apiKey = privateKey;
|
||||
SubscriptionRetrieveParams params = SubscriptionRetrieveParams.builder()
|
||||
.addExpand("default_payment_method")
|
||||
.build();
|
||||
try {
|
||||
Subscription subscription = Subscription.retrieve(subscriptionId, params, RequestOptions.builder().build());
|
||||
return subscription.getDefaultPaymentMethodObject();
|
||||
} catch (StripeException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private static Map<String, String> getPaymentMethodMap(PaymentMethod paymentMethod) {
|
||||
Map<String, String> paymentMethodInfo = new HashMap<>();
|
||||
if (paymentMethod != null && paymentMethod.getCard() != null) {
|
||||
String brand = paymentMethod.getCard().getBrand();
|
||||
brand = brand.substring(0, 1).toUpperCase() + brand.substring(1);
|
||||
paymentMethodInfo.put("paymentMethod", brand + " " + paymentMethod.getCard().getFunding() + " Card");
|
||||
paymentMethodInfo.put("last4", paymentMethod.getCard().getLast4());
|
||||
} else if (paymentMethod != null) {
|
||||
paymentMethodInfo.put("paymentMethod", StringUtils.capitalize(paymentMethod.getType()));
|
||||
paymentMethodInfo.put("last4", "N/A");
|
||||
} else {
|
||||
paymentMethodInfo.put("paymentMethod", "N/A");
|
||||
paymentMethodInfo.put("last4", "N/A");
|
||||
}
|
||||
return paymentMethodInfo;
|
||||
}
|
||||
|
||||
@Value("${stripe.private-key}")
|
||||
private String privateKey;
|
||||
|
||||
/**
|
||||
* 为 Stripe Invoice 创建或更新支付记录
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @param paymentMethodInfo 外部传入的支付方式信息(如从 Session 传入),优先使用,为空时内部重新获取
|
||||
* @return PaymentInfo 支付记录
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice){
|
||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice, Map<String, String> paymentMethodInfo, List<Session.Discount> discounts) {
|
||||
Stripe.apiKey = privateKey;
|
||||
StripeService stripeService = SpringUtils.getBean(StripeService.class);
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
@@ -235,29 +405,61 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
qw.eq("transaction_id", invoiceId);
|
||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||
String status = invoice.getStatus();
|
||||
// 判断是否有优惠码
|
||||
// 判断是否有优惠码 续订不会使用优惠码,故取消这部分代码
|
||||
// Stripe SDK 32.0.0: invoice.getDiscount() 已移除,使用 invoice.getDiscountObject().get() 替代
|
||||
String promotionCode = null;
|
||||
if (Objects.nonNull(invoice.getDiscount()) && !StringUtil.isNullOrEmpty(invoice.getDiscount().getPromotionCode())){
|
||||
ProductCoupons productCoupon = stripeService.getProductCoupon(null, invoice.getDiscount().getPromotionCode());
|
||||
promotionCode = productCoupon.getPromotionCode();
|
||||
if (!CollectionUtils.isEmpty(discounts)) {
|
||||
promotionCode = getPromotionCodeByPromotionCodeId(discounts.getFirst().getPromotionCode());
|
||||
}
|
||||
// 判断当前支付是否已经被记录,确保同一个支付不会被重复记录
|
||||
if (Objects.isNull(paymentInfo)){
|
||||
String orderNo;
|
||||
String orderNo = null;
|
||||
String billingReason = invoice.getBillingReason();
|
||||
String paymentIntentIdForCharge = null;
|
||||
try {
|
||||
if (invoice.getBillingReason().equals("manual")){
|
||||
if ("manual".equals(billingReason)){
|
||||
// 手动创建的发票,针对one-time支付
|
||||
// orderNo = invoice.getLines().getData().get(0).getPrice().getMetadata().get("orderId");
|
||||
// 当支付失败时,chargeId为空
|
||||
String chargeId = invoice.getCharge();
|
||||
orderNo = Charge.retrieve(chargeId).getDescription().replace("AiDA - ", "");
|
||||
}else {
|
||||
String subscriptionId = invoice.getSubscription();
|
||||
// 从subscription中获取orderNo
|
||||
orderNo = Subscription.retrieve(subscriptionId).getDescription().replace("AiDA - ", "");
|
||||
// 获取 PaymentIntent 用于后续获取 chargeId 和支付方式
|
||||
paymentIntentIdForCharge = getPaymentIntentByInvoice(invoice);
|
||||
if (!StringUtil.isNullOrEmpty(paymentIntentIdForCharge)) {
|
||||
PaymentIntent paymentIntent = PaymentIntent.retrieve(paymentIntentIdForCharge);
|
||||
String chargeId = paymentIntent.getLatestCharge();
|
||||
if (!StringUtil.isNullOrEmpty(chargeId)) {
|
||||
Charge charge = Charge.retrieve(chargeId);
|
||||
String description = charge.getDescription();
|
||||
orderNo = description != null ? description.replace("AiDA - ", "") : null;
|
||||
}
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(orderNo)) {
|
||||
orderNo = extractOrderNoFromInvoiceLines(invoice);
|
||||
}
|
||||
} else {
|
||||
// Stripe SDK 32.0.0: invoice.getSubscription() 已移除
|
||||
// 方案A:直接从 invoice.getParent().getSubscriptionDetails().getMetadata() 获取 orderId(SDK 32.0.0 新方式)
|
||||
String orderNoFromParent = getOrderNoFromInvoiceParent(invoice);
|
||||
if (!StringUtil.isNullOrEmpty(orderNoFromParent)) {
|
||||
orderNo = orderNoFromParent;
|
||||
log.info("[createOrUpdatePaymentInfoForStripe] 从 invoice.getParent().getSubscriptionDetails() 获取到 orderNo={}", orderNo);
|
||||
} else {
|
||||
// 方案B:从 subscription 获取 orderNo
|
||||
String subscriptionId = getSubscriptionByInvoice(invoice);
|
||||
if (!StringUtil.isNullOrEmpty(subscriptionId)) {
|
||||
try {
|
||||
Subscription subscription = Subscription.retrieve(subscriptionId);
|
||||
orderNo = getOrderNoBySubscription(subscription);
|
||||
} catch (StripeException e) {
|
||||
log.warn("[createOrUpdatePaymentInfoForStripe] 获取 Subscription 失败,subscriptionId={}, error={}", subscriptionId, e.getMessage());
|
||||
}
|
||||
}
|
||||
// 方案C:备用方案,从 invoice metadata 获取
|
||||
if (StringUtil.isNullOrEmpty(orderNo)) {
|
||||
orderNo = extractOrderNoFromInvoiceMetadata(invoice);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
throw new RuntimeException(e);
|
||||
log.error("[createOrUpdatePaymentInfoForStripe] 获取订单号失败,invoiceId={}, error={}", invoiceId, e.getMessage());
|
||||
throw new RuntimeException("Failed to retrieve orderNo from invoice: " + invoiceId, e);
|
||||
}
|
||||
Long amountTotal;
|
||||
if (status.equals("paid")){
|
||||
@@ -268,11 +470,9 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
|
||||
// stripe 的支付金额单位是分,在我们数据库中金额单位为 元
|
||||
Float divide = new BigDecimal(amountTotal).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||
String type = invoice.getBillingReason().equals("subscription_create") ? "new" :
|
||||
invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason();
|
||||
String type = invoice.getBillingReason().equals("subscription_create") ? PaymentInfoType.NEW.getType() :
|
||||
invoice.getBillingReason().equals("subscription_cycle") ? PaymentInfoType.RENEWAL.getType() : invoice.getBillingReason();
|
||||
|
||||
// 获取支付方式
|
||||
Map<String, String> paymentMethod = stripeService.getPaymentMethodByInvoiceId(invoiceId);
|
||||
// 获取订单信息
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
|
||||
@@ -288,8 +488,8 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
paymentInfo.setContent(json);
|
||||
paymentInfo.setType(type);
|
||||
paymentInfo.setNotified(0);
|
||||
paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod"));
|
||||
paymentInfo.setLast4(paymentMethod.get("last4"));
|
||||
paymentInfo.setPaymentMethod(paymentMethodInfo.get("paymentMethod"));
|
||||
paymentInfo.setLast4(paymentMethodInfo.get("last4"));
|
||||
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
|
||||
paymentInfo.setPromotionCode(promotionCode);
|
||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||
@@ -300,87 +500,85 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
}
|
||||
int row = baseMapper.insertIgnore(paymentInfo);
|
||||
log.info("Payment Info insert affect rows:{}", row);
|
||||
}else {
|
||||
orderInfoService.updateTotalFeeByOrderNo(orderNo);
|
||||
} else {
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setPromotionCode(promotionCode);
|
||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(paymentInfo);
|
||||
}
|
||||
|
||||
return paymentInfo;
|
||||
}
|
||||
|
||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge){
|
||||
Stripe.apiKey = privateKey;
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
// todo 首次支付失败,没有invoiceId,所以如果这个order之后成功支付后,会有多条paymentInfo 是否需要优化??
|
||||
qw.eq("transaction_id", charge.getInvoice());
|
||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||
Charge.PaymentMethodDetails paymentMethodDetails = charge.getPaymentMethodDetails();
|
||||
String paymentMethod;
|
||||
String last4 = "N/A";
|
||||
switch (paymentMethodDetails.getType()){
|
||||
case "alipay":
|
||||
paymentMethod = "Alipay";
|
||||
break;
|
||||
case "bancontact":
|
||||
paymentMethod = "BanContact";
|
||||
break;
|
||||
case "card":
|
||||
Charge.PaymentMethodDetails.Card card = paymentMethodDetails.getCard();
|
||||
String brand = card.getBrand();
|
||||
brand = brand.substring(0, 1).toUpperCase() + brand.substring(1);
|
||||
paymentMethod = brand + " " + card.getFunding() + "card";
|
||||
last4 = card.getLast4();
|
||||
break;
|
||||
case "eps":
|
||||
Charge.PaymentMethodDetails.Eps eps = paymentMethodDetails.getEps();
|
||||
paymentMethod = eps.getBank();
|
||||
break;
|
||||
case "giropay":
|
||||
paymentMethod = "GiroPay";
|
||||
break;
|
||||
case "ideal":
|
||||
Charge.PaymentMethodDetails.Ideal ideal = paymentMethodDetails.getIdeal();
|
||||
paymentMethod = ideal.getBank();
|
||||
break;
|
||||
case "link":
|
||||
paymentMethod = "Link";
|
||||
break;
|
||||
default:
|
||||
paymentMethod = "N/A";
|
||||
}
|
||||
if (Objects.isNull(paymentInfo)){
|
||||
Stripe.apiKey = privateKey;
|
||||
|
||||
String orderNo = charge.getDescription().replace("AiDA - ", "");
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
Float divide = new BigDecimal(charge.getAmount()).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||
paymentInfo = new PaymentInfo();
|
||||
paymentInfo.setOrderNo(orderNo);
|
||||
paymentInfo.setTransactionId(charge.getInvoice());
|
||||
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
||||
paymentInfo.setTradeState(charge.getStatus());
|
||||
paymentInfo.setPayerTotal(divide);
|
||||
paymentInfo.setNotified(0);
|
||||
paymentInfo.setPaymentMethod(paymentMethod);
|
||||
paymentInfo.setLast4(last4);
|
||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||
if (!Objects.isNull(orderByOrderNo)){
|
||||
paymentInfo.setCountry(orderByOrderNo.getCountry());
|
||||
paymentInfo.setCity(orderByOrderNo.getCity());
|
||||
paymentInfo.setIpAddress(orderByOrderNo.getIpAddress());
|
||||
private String getPaymentIntentByInvoice(Invoice invoice) {
|
||||
// 从 invoice.getPayments() 获取(适用于已支付完成的 Invoice)
|
||||
// SDK 32.0.0: invoice.getPayments() 可能为 null,需逐层判空
|
||||
try {
|
||||
InvoicePaymentCollection payments = invoice.getPayments();
|
||||
if (payments != null) {
|
||||
List<InvoicePayment> invoicePayments = payments.getData();
|
||||
if (invoicePayments != null && !invoicePayments.isEmpty()) {
|
||||
InvoicePayment firstPayment = invoicePayments.getFirst();
|
||||
if (firstPayment != null) {
|
||||
InvoicePayment.Payment payment = firstPayment.getPayment();
|
||||
if (payment != null) {
|
||||
PaymentIntent paymentIntent = payment.getPaymentIntentObject();
|
||||
if (paymentIntent != null) {
|
||||
return paymentIntent.getId();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
int row = baseMapper.insertIgnore(paymentInfo);
|
||||
log.info("Payment Info insert affect rows:{}", row);
|
||||
}else {
|
||||
paymentInfo.setTradeState(charge.getStatus());
|
||||
paymentInfo.setPaymentMethod(paymentMethod);
|
||||
paymentInfo.setLast4(last4);
|
||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(paymentInfo);
|
||||
} catch (Exception e) {
|
||||
log.warn("[getPaymentIntentByInvoice] 获取 PaymentIntent 失败,invoiceId={},error={}", invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String getPromotionCodeByInvoice(Invoice invoice) throws StripeException {
|
||||
// 1. 检索 Invoice 并展开 discounts 字段
|
||||
InvoiceRetrieveParams params = InvoiceRetrieveParams.builder()
|
||||
.addExpand("discounts") // 展开折扣数组
|
||||
.build();
|
||||
|
||||
invoice = Invoice.retrieve(invoice.getId(), params, null);
|
||||
|
||||
// 2. 获取折扣列表(注意:Invoice.Discount 不是 List<Discount>)
|
||||
List<Discount> invoiceDiscounts = invoice.getDiscountObjects();
|
||||
|
||||
if (invoiceDiscounts == null || invoiceDiscounts.isEmpty()) {
|
||||
log.info("No discounts applied to this invoice");
|
||||
return null;
|
||||
}
|
||||
|
||||
return paymentInfo;
|
||||
// 3. 遍历每个折扣(通常只有一个)
|
||||
for (Discount discount : invoiceDiscounts) {
|
||||
// // 获取 source 对象(包含优惠券信息)
|
||||
// Discount.Source source = discount.getSource();
|
||||
//
|
||||
// if (source != null && "coupon".equals(source.getType())) {
|
||||
// // source.coupon 可能是 ID 字符串,也可能是已展开的 Coupon 对象
|
||||
// Object couponObj = source.getCoupon();
|
||||
//
|
||||
// if (couponObj instanceof String) {
|
||||
// String couponId = (String) couponObj;
|
||||
// // 需要通过 ID 单独检索 Coupon
|
||||
// Coupon coupon = Coupon.retrieve(couponId);
|
||||
// System.out.println("Coupon ID: " + coupon.getId());
|
||||
// System.out.println("Coupon name: " + coupon.getName());
|
||||
// } else if (couponObj instanceof Coupon) {
|
||||
// Coupon coupon = (Coupon) couponObj;
|
||||
// System.out.println("Coupon ID: " + coupon.getId());
|
||||
// }
|
||||
// }
|
||||
// // 获取其他折扣信息
|
||||
// Long start = discount.getStart(); // 折扣开始时间
|
||||
// Long end = discount.getEnd(); // 折扣结束时间(可能为 null)
|
||||
return discount.getPromotionCode(); // 关联的促销码 ID
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -439,26 +637,234 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
return baseMapper.selectPaidPaymentsByAccountAndPromotion(accountId, promCode);
|
||||
}
|
||||
|
||||
public PaymentInfo updatePaymentRefundStatus(Charge charge){
|
||||
// 判断当前退款是部分退款还是全部退款
|
||||
|
||||
/**
|
||||
* 通过 chargeId 更新支付记录的退款状态
|
||||
* 从 charge 获取关联的 invoiceId,再更新 paymentInfo
|
||||
*
|
||||
* @param charge Stripe Charge 对象
|
||||
* @param status 新的交易状态
|
||||
*/
|
||||
@Override
|
||||
public void updatePaymentRefundStatusByChargeId(Charge charge, String status) {
|
||||
if (charge == null) {
|
||||
log.warn("[updatePaymentRefundStatusByChargeId] charge 为空,跳过");
|
||||
return;
|
||||
}
|
||||
String chargeId = charge.getId();
|
||||
Stripe.apiKey = privateKey;
|
||||
String invoiceId = extractInvoiceIdFromCharge(charge);
|
||||
if (!StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
updatePaymentRefundStatusByInvoiceId(invoiceId, status);
|
||||
} else {
|
||||
log.warn("[updatePaymentRefundStatusByChargeId] 无法从 charge 获取 invoiceId,chargeId={}", chargeId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 invoiceId 更新 paymentInfo 表的退款状态
|
||||
*
|
||||
* @param invoiceId Stripe Invoice ID(对应 paymentInfo.transactionId)
|
||||
* @param status 新的交易状态
|
||||
*/
|
||||
@Override
|
||||
public void updatePaymentRefundStatusByInvoiceId(String invoiceId, String status) {
|
||||
if (StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
log.warn("[updatePaymentRefundStatusByInvoiceId] invoiceId 为空,跳过");
|
||||
return;
|
||||
}
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("transaction_id", charge.getInvoice());
|
||||
qw.eq("transaction_id", invoiceId);
|
||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||
if (Objects.nonNull(paymentInfo)){
|
||||
String status ;
|
||||
if (Objects.equals(charge.getAmount(), charge.getAmountRefunded())){
|
||||
status = "Refunded";
|
||||
}else if (charge.getAmount() > charge.getAmountRefunded()){
|
||||
status = "Partial refund";
|
||||
}else {
|
||||
status = "Refund Exception";
|
||||
log.warn("{}, 退款金额高于付款金额, ChargeId为:{}", status, charge.getId());
|
||||
}
|
||||
if (!paymentInfo.getTradeState().equals(status)){
|
||||
if (Objects.nonNull(paymentInfo)) {
|
||||
if (!paymentInfo.getTradeState().equals(status)) {
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(paymentInfo);
|
||||
log.info("[updatePaymentRefundStatusByInvoiceId] 支付记录状态已更新,invoiceId={},status={}", invoiceId, status);
|
||||
}
|
||||
} else {
|
||||
log.warn("[updatePaymentRefundStatusByInvoiceId] 未找到对应的支付记录,invoiceId={}", invoiceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Charge 中提取 invoiceId
|
||||
* Stripe SDK 32.0.0 (API 2026-03-25.dahlia):
|
||||
* - Charge 没有 invoice 字段(charge.getInvoice() 在新版本中不可用)
|
||||
* - Charge 有 payment_intent 字段(可展开)
|
||||
* 路径: Charge → payment_intent → InvoicePayment.list(payment.payment_intent=xxx) → invoice
|
||||
*
|
||||
* @param charge Stripe Charge
|
||||
* @return invoiceId 或 null
|
||||
*/
|
||||
private String extractInvoiceIdFromCharge(Charge charge) {
|
||||
if (charge == null) {
|
||||
return null;
|
||||
}
|
||||
// 方案1:从 charge.metadata 中获取(如果存储了相关信息)
|
||||
Map<String, String> metadata = charge.getMetadata();
|
||||
if (metadata != null && metadata.containsKey("invoiceId")) {
|
||||
return metadata.get("invoiceId");
|
||||
}
|
||||
|
||||
// 方案2:路径 Charge → payment_intent → InvoicePayment → invoice
|
||||
String paymentIntentId = charge.getPaymentIntent();
|
||||
if (!StringUtil.isNullOrEmpty(paymentIntentId)) {
|
||||
return extractInvoiceIdFromPaymentIntentById(paymentIntentId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 PaymentIntentId 查找关联的 invoiceId
|
||||
* 路径: PaymentIntent → InvoicePayment.list(payment.payment_intent=xxx) → invoice
|
||||
* SDK 32.0.0 InvoicePayment.list() 支持 payment.payment_intent 过滤参数
|
||||
*
|
||||
* @param paymentIntentId Stripe PaymentIntent ID
|
||||
* @return invoiceId 或 null
|
||||
*/
|
||||
private String extractInvoiceIdFromPaymentIntentById(String paymentIntentId) {
|
||||
if (StringUtil.isNullOrEmpty(paymentIntentId)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
InvoicePaymentListParams params = InvoicePaymentListParams.builder()
|
||||
.setPayment(
|
||||
InvoicePaymentListParams.Payment.builder()
|
||||
.setPaymentIntent(paymentIntentId)
|
||||
.setType(InvoicePaymentListParams.Payment.Type.PAYMENT_INTENT)
|
||||
.build()
|
||||
)
|
||||
.setLimit(1L)
|
||||
.build();
|
||||
InvoicePaymentCollection payments = InvoicePayment.list(params);
|
||||
if (payments != null && payments.getData() != null && !payments.getData().isEmpty()) {
|
||||
InvoicePayment payment = payments.getData().get(0);
|
||||
String invoiceId = payment.getInvoice();
|
||||
if (!StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
return invoiceId;
|
||||
}
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
log.warn("[extractInvoiceIdFromPaymentIntentById] 通过 InvoicePayment.list 查找 invoice 失败,paymentIntentId={}, error={}",
|
||||
paymentIntentId, e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice lines 中提取订单号
|
||||
* Stripe SDK 32.0.0: 兼容处理
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String extractOrderNoFromInvoiceLines(Invoice invoice) {
|
||||
try {
|
||||
List<InvoiceLineItem> lines = invoice.getLines().getData();
|
||||
if (lines != null && !lines.isEmpty()) {
|
||||
InvoiceLineItem firstLine = lines.getFirst();
|
||||
// 尝试从 line metadata 获取
|
||||
Map<String, String> lineMetadata = firstLine.getMetadata();
|
||||
if (lineMetadata != null && lineMetadata.containsKey("orderId")) {
|
||||
return lineMetadata.get("orderId");
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[extractOrderNoFromInvoiceLines] 提取订单号失败,invoiceId={}, error={}",
|
||||
invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice metadata 中提取订单号
|
||||
* Stripe SDK 32.0.0: 兼容处理
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String extractOrderNoFromInvoiceMetadata(Invoice invoice) {
|
||||
Map<String, String> metadata = invoice.getMetadata();
|
||||
if (metadata != null && metadata.containsKey("orderId")) {
|
||||
return metadata.get("orderId");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice 中获取 subscriptionId
|
||||
* Stripe SDK 32.0.0: 使用 invoice.getParent().getSubscriptionDetails().getSubscription() 替代已移除的 invoice.getSubscription()
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return subscriptionId 或 null
|
||||
*/
|
||||
private String getSubscriptionByInvoice(Invoice invoice) {
|
||||
try {
|
||||
Invoice.Parent parent = invoice.getParent();
|
||||
if (parent != null && "subscription_details".equals(parent.getType())) {
|
||||
Invoice.Parent.SubscriptionDetails subscriptionDetails = parent.getSubscriptionDetails();
|
||||
if (subscriptionDetails != null) {
|
||||
return subscriptionDetails.getSubscription();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[getSubscriptionByInvoice] 从 invoice.getParent() 获取 subscriptionId 失败,invoiceId={}, error={}",
|
||||
invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Invoice.getParent().getSubscriptionDetails().getMetadata() 直接获取 orderNo
|
||||
* Stripe SDK 32.0.0: 这是获取订阅关联的 orderNo 的推荐方式
|
||||
* 当通过 Checkout Session 创建订阅时,metadata 会自动传递到 Subscription,再传递到 Invoice
|
||||
*
|
||||
* @param invoice Stripe Invoice
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String getOrderNoFromInvoiceParent(Invoice invoice) {
|
||||
try {
|
||||
Invoice.Parent parent = invoice.getParent();
|
||||
if (parent != null && "subscription_details".equals(parent.getType())) {
|
||||
Invoice.Parent.SubscriptionDetails subscriptionDetails = parent.getSubscriptionDetails();
|
||||
if (subscriptionDetails != null) {
|
||||
// SDK 32.0.0: subscriptionDetails.getMetadata() 可以直接获取 subscription 创建时设置的 metadata
|
||||
Map<String, String> subscriptionMetadata = subscriptionDetails.getMetadata();
|
||||
if (subscriptionMetadata != null && subscriptionMetadata.containsKey("orderId")) {
|
||||
return subscriptionMetadata.get("orderId");
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[getOrderNoFromInvoiceParent] 从 invoice.getParent().getSubscriptionDetails().getMetadata() 获取 orderNo 失败,invoiceId={}, error={}",
|
||||
invoice.getId(), e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Subscription 中获取 orderNo
|
||||
* 优先从 subscription metadata 获取,其次从 description 中获取
|
||||
*
|
||||
* @param subscription Stripe Subscription
|
||||
* @return orderNo 或 null
|
||||
*/
|
||||
private String getOrderNoBySubscription(Subscription subscription) {
|
||||
if (subscription == null) {
|
||||
return null;
|
||||
}
|
||||
// 方案1:从 subscription metadata 获取(SDK 32.0.0 推荐方式)
|
||||
Map<String, String> metadata = subscription.getMetadata();
|
||||
if (metadata != null && metadata.containsKey("orderId")) {
|
||||
return metadata.get("orderId");
|
||||
}
|
||||
// 方案2:从 description 获取(旧方式,保持兼容)
|
||||
String description = subscription.getDescription();
|
||||
if (!StringUtil.isNullOrEmpty(description) && description.startsWith("AiDA - ")) {
|
||||
return description.replace("AiDA - ", "");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,11 @@ public class RabbitMQServiceImpl implements RabbitMQService {
|
||||
mqPublisher.sendGenerateMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMessageToGenerateResult(String message) {
|
||||
mqPublisher.sendGenerateResultMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishMessageToSR(String message) {
|
||||
mqPublisher.sendSRMessage(message);
|
||||
|
||||
@@ -1,31 +1,59 @@
|
||||
package com.ai.da.service.impl;
|
||||
|
||||
import com.ai.da.common.enums.CreditsEventsEnum;
|
||||
import com.ai.da.common.enums.OrderStatusEnum;
|
||||
import com.ai.da.common.enums.ProductEnum;
|
||||
import com.ai.da.common.utils.OrderNoUtils;
|
||||
import com.ai.da.mapper.primary.AccountMapper;
|
||||
import com.ai.da.mapper.primary.RefundInfoMapper;
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.Account;
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.RefundInfo;
|
||||
import com.ai.da.service.OrderInfoService;
|
||||
import com.ai.da.service.RefundInfoService;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.*;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.google.gson.Gson;
|
||||
import com.stripe.Stripe;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Refund;
|
||||
import com.stripe.exception.StripeException;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundInfo> implements RefundInfoService {
|
||||
|
||||
@Value("${stripe.private-key}")
|
||||
private String privateKey;
|
||||
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
@Resource
|
||||
private CreditsService creditsService;
|
||||
@Resource
|
||||
private AccountService accountService;
|
||||
@Resource
|
||||
private PaymentInfoService paymentInfoService;
|
||||
@Resource
|
||||
private AccountMapper accountMapper;
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
|
||||
/**
|
||||
* 根据订单号创建退款订单
|
||||
@@ -217,18 +245,275 @@ public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundI
|
||||
}
|
||||
|
||||
public RefundInfo updateRefundForStripe(Charge charge){
|
||||
List<RefundInfo> refundInfoList = getByChargeId(charge.getId());
|
||||
if (!refundInfoList.isEmpty()){
|
||||
RefundInfo refundInfo = refundInfoList.get(refundInfoList.size() - 1);
|
||||
if (StringUtil.isNullOrEmpty(refundInfo.getOrderNo())){
|
||||
String orderNo = charge.getDescription().replace("AiDA - ", "");
|
||||
refundInfo.setOrderNo(orderNo);
|
||||
refundInfo.setTotalFee(charge.getAmount() / 100f);
|
||||
refundInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(refundInfo);
|
||||
return refundInfo;
|
||||
String chargeId = charge.getId();
|
||||
List<RefundInfo> refundInfoList = getByChargeId(chargeId);
|
||||
if (refundInfoList.isEmpty()){
|
||||
return null;
|
||||
}
|
||||
RefundInfo refundInfo = refundInfoList.get(refundInfoList.size() - 1);
|
||||
if (StringUtil.isNullOrEmpty(refundInfo.getOrderNo())){
|
||||
String orderNo = charge.getDescription() != null ? charge.getDescription().replace("AiDA - ", "") : null;
|
||||
if (StringUtil.isNullOrEmpty(orderNo)){
|
||||
return null;
|
||||
}
|
||||
refundInfo.setOrderNo(orderNo);
|
||||
refundInfo.setTotalFee(charge.getAmount() / 100f);
|
||||
baseMapper.updateById(refundInfo);
|
||||
}
|
||||
|
||||
// 处理退款成功后的业务逻辑
|
||||
// 判断是否为全额退款(amount == amountRefunded)
|
||||
if (charge.getAmount() != null && charge.getAmountRefunded() != null
|
||||
&& charge.getAmount().equals(charge.getAmountRefunded())) {
|
||||
handleRefundSuccess(refundInfo);
|
||||
}
|
||||
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理全额退款成功后的业务逻辑
|
||||
* 根据订单类型执行不同操作:
|
||||
* - 积分购买订单:扣减 t_account.credits,并在 t_credits_detail 添加变动记录
|
||||
* - 订阅订单:扣减 t_account.credits,根据订阅类型在 t_credits_detail 添加变动记录,
|
||||
* 并将 t_account.valid_start_time 设置为 t_subscription_info.current_period_start
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void handleRefundSuccess(RefundInfo refundInfo) {
|
||||
String orderNo = refundInfo.getOrderNo();
|
||||
if (StringUtil.isNullOrEmpty(orderNo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (orderInfo == null) {
|
||||
log.warn("[handleFullRefundSuccess] 未找到订单,跳过,orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
String title = orderInfo.getTitle();
|
||||
Long accountId = orderInfo.getAccountId();
|
||||
Account account = accountMapper.selectById(accountId);
|
||||
|
||||
// 判断订单类型
|
||||
if (title != null && title.startsWith("积分购买")) {
|
||||
// 积分购买订单退款
|
||||
handleCreditsPurchaseRefund(orderNo, orderInfo, account);
|
||||
} else {
|
||||
// 订阅订单退款
|
||||
handleSubscriptionRefund(orderNo, orderInfo, account);
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.REFUND_SUCCESS);
|
||||
log.info("[RefundInfoService] 退款成功,订单状态已更新,orderNo={}", orderNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理积分购买订单的退款
|
||||
* 扣减 t_account.credits,在 t_credits_detail 添加变动记录
|
||||
*/
|
||||
private void handleCreditsPurchaseRefund(String orderNo, OrderInfo orderInfo, Account account) {
|
||||
Long accountId = orderInfo.getAccountId();
|
||||
// 根据购买金额 / 单价计算积分数量
|
||||
float creditsToRefund = orderInfo.getTotalFee() / Float.parseFloat(CreditsEventsEnum.PRICE.getValue());
|
||||
int creditsQty = (int) creditsToRefund;
|
||||
|
||||
BigDecimal existingCredits = account.getCredits();
|
||||
BigDecimal refundCredits = new BigDecimal(CreditsEventsEnum.BUY_CREDITS.getValue())
|
||||
.multiply(new BigDecimal(creditsQty));
|
||||
BigDecimal newCredits = existingCredits.subtract(refundCredits);
|
||||
if (newCredits.compareTo(BigDecimal.ZERO) < 0) {
|
||||
newCredits = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 更新 t_account.credits
|
||||
accountService.updateCreditsAndEndTime(account, newCredits.toString(), null, null);
|
||||
|
||||
// 更新 t_credits_detail
|
||||
creditsService.insertToCreditsDetail(
|
||||
accountId,
|
||||
CreditsEventsEnum.REFUND.getName() + "--Stripe",
|
||||
refundCredits.toString(),
|
||||
"negative",
|
||||
orderNo
|
||||
);
|
||||
|
||||
log.info("[handleCreditsPurchaseRefund] 积分购买退款完成,orderNo={},accountId={},creditsRefunded={}",
|
||||
orderNo, accountId, refundCredits);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理订阅订单的退款
|
||||
* 扣减 t_account.credits,根据订阅类型在 t_credits_detail 添加变动记录,
|
||||
* 并将 t_account.valid_start_time 设置为 t_subscription_info.current_period_start
|
||||
*/
|
||||
private void handleSubscriptionRefund(String orderNo, OrderInfo orderInfo, Account account) {
|
||||
Long accountId = orderInfo.getAccountId();
|
||||
String title = orderInfo.getTitle();
|
||||
|
||||
// 根据 orderInfo.title 在 ProductEnum 中匹配订阅类型
|
||||
ProductEnum productEnum = ProductEnum.getByName(title);
|
||||
if (productEnum == null) {
|
||||
log.warn("[handleSubscriptionRefund] 无法匹配订阅类型,跳过积分扣减,orderNo={},title={}", orderNo, title);
|
||||
return;
|
||||
}
|
||||
|
||||
// 扣减对应订阅类型的积分
|
||||
BigDecimal existingCredits = account.getCredits();
|
||||
BigDecimal refundCredits = new BigDecimal(productEnum.getCredits());
|
||||
BigDecimal newCredits = existingCredits.subtract(refundCredits);
|
||||
if (newCredits.compareTo(BigDecimal.ZERO) < 0) {
|
||||
newCredits = BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
// 根据 orderNo 查询 t_subscription_info,将 t_account.valid_start_time 设置为 current_period_start
|
||||
List<SubscriptionInfo> subList = subscriptionInfoMapper.selectList(
|
||||
new QueryWrapper<SubscriptionInfo>().eq("order_no", orderNo)
|
||||
);
|
||||
if (!subList.isEmpty()) {
|
||||
SubscriptionInfo subscriptionInfo = subList.getFirst();
|
||||
|
||||
if (subscriptionInfo.getStatus().equals("active")) {
|
||||
stripeSubscriptionService.cancelSubscription(subscriptionInfo.getSubscriptionId(), "Refunded", accountId);
|
||||
}
|
||||
|
||||
Long periodStart = subscriptionInfo.getCurrentPeriodStart();
|
||||
if (periodStart != null) {
|
||||
account.setSystemUser(0);
|
||||
account.setValidEndTime(periodStart * 1000);
|
||||
account.setCredits(newCredits);
|
||||
account.setUpdateDate(new java.util.Date());
|
||||
accountMapper.updateById(account);
|
||||
log.info("[handleSubscriptionRefund] 已将 valid_start_time 设置为 current_period_start,orderNo={},periodStart={}",
|
||||
orderNo, periodStart);
|
||||
}
|
||||
} else {
|
||||
log.warn("[handleSubscriptionRefund] 未找到订阅记录,跳过 valid_start_time 更新,orderNo={}", orderNo);
|
||||
}
|
||||
|
||||
// 在 t_credits_detail 添加变动记录,systemUser 设为 0
|
||||
creditsService.insertToCreditsDetail(
|
||||
accountId,
|
||||
CreditsEventsEnum.REFUND.getName() + "--Stripe",
|
||||
refundCredits.toString(),
|
||||
"negative",
|
||||
orderNo
|
||||
);
|
||||
|
||||
log.info("[handleSubscriptionRefund] 订阅退款完成,orderNo={},accountId={},creditsRefunded={}",
|
||||
orderNo, accountId, refundCredits);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* refund.created 事件处理
|
||||
* 在 t_refund_info 表中创建退款记录
|
||||
*/
|
||||
@Override
|
||||
public RefundInfo handleRefundCreated(Refund refund) {
|
||||
String refundId = refund.getId();
|
||||
RefundInfo existing = getByRefundId(refundId);
|
||||
if (existing != null) {
|
||||
log.info("[handleRefundCreated] 退款记录已存在,跳过创建,refundId={}", refundId);
|
||||
return existing;
|
||||
}
|
||||
|
||||
RefundInfo refundInfo = new RefundInfo();
|
||||
refundInfo.setRefundId(refundId);
|
||||
refundInfo.setRefundNo(OrderNoUtils.getRefundNo());
|
||||
refundInfo.setChargeId(refund.getCharge());
|
||||
refundInfo.setRefund(refund.getAmount() / 100f);
|
||||
refundInfo.setReason(refund.getReason());
|
||||
refundInfo.setRefundStatus("pending");
|
||||
refundInfo.setCreateTime(LocalDateTime.now());
|
||||
|
||||
baseMapper.insert(refundInfo);
|
||||
log.info("[handleRefundCreated] 退款记录已创建,refundId={},chargeId={}", refundId, refund.getCharge());
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* refund.updated (status=succeeded) 事件处理
|
||||
* 找到该笔退款对应的 invoice,从而修改 paymentInfo 表中 transactionId 为 invoiceId 的记录,将状态改为 refunded
|
||||
*/
|
||||
@Override
|
||||
public RefundInfo handleRefundSucceeded(Refund refund) {
|
||||
String refundId = refund.getId();
|
||||
RefundInfo refundInfo = getByRefundId(refundId);
|
||||
if (refundInfo == null) {
|
||||
log.warn("[handleRefundSucceeded] 未找到退款记录,先创建,refundId={}", refundId);
|
||||
refundInfo = handleRefundCreated(refund);
|
||||
}
|
||||
|
||||
// 获取 charge 并从中提取 orderNo
|
||||
String chargeId = refund.getCharge();
|
||||
Charge charge = null;
|
||||
if (!StringUtil.isNullOrEmpty(chargeId)) {
|
||||
Stripe.apiKey = privateKey;
|
||||
try {
|
||||
charge = Charge.retrieve(chargeId);
|
||||
String description = charge.getDescription();
|
||||
String orderNo = description != null ? description.replace("AiDA - ", "") : null;
|
||||
if (!StringUtil.isNullOrEmpty(orderNo) && !orderNo.equals(refundInfo.getOrderNo())) {
|
||||
if (!"succeeded".equals(refundInfo.getRefundStatus())) {
|
||||
refundInfo.setRefundStatus("succeeded");
|
||||
}
|
||||
refundInfo.setOrderNo(orderNo);
|
||||
refundInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(refundInfo);
|
||||
log.info("[handleRefundSucceeded] 从 charge 中提取并更新 orderNo,refundId={},orderNo={}", refundId, orderNo);
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
log.error("[handleRefundSucceeded] 获取 charge 失败,chargeId={},error={}", chargeId, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
// 通过 charge 更新 paymentInfo 状态为 refunded
|
||||
if (charge != null) {
|
||||
paymentInfoService.updatePaymentRefundStatusByChargeId(charge, "Refunded");
|
||||
}
|
||||
|
||||
// 如果是全额退款,执行后续业务逻辑
|
||||
if (!StringUtil.isNullOrEmpty(refundInfo.getOrderNo())) {
|
||||
handleRefundSuccess(refundInfo);
|
||||
}
|
||||
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* refund.failed 事件处理
|
||||
* 修改 t_refund_info 表状态,同时邮件通知商家
|
||||
*/
|
||||
@Override
|
||||
public RefundInfo handleRefundFailed(Refund refund) {
|
||||
String refundId = refund.getId();
|
||||
RefundInfo refundInfo = getByRefundId(refundId);
|
||||
if (refundInfo == null) {
|
||||
log.warn("[handleRefundFailed] 未找到退款记录,先创建,refundId={}", refundId);
|
||||
refundInfo = handleRefundCreated(refund);
|
||||
}
|
||||
|
||||
if (!"failed".equals(refundInfo.getRefundStatus())) {
|
||||
refundInfo.setRefundStatus("failed");
|
||||
refundInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(refundInfo);
|
||||
log.info("[handleRefundFailed] 退款状态已更新为 failed,refundId={}", refundId);
|
||||
}
|
||||
|
||||
// 发送退款失败邮件通知商家
|
||||
try {
|
||||
String reason = refund.getFailureReason();
|
||||
String orderNo = refundInfo.getOrderNo() != null ? refundInfo.getOrderNo() : "";
|
||||
String amount = String.valueOf(refundInfo.getRefund());
|
||||
// SendEmailUtil.sendRefundFailedNotification(refundId, reason, orderNo, amount);
|
||||
log.info("[handleRefundFailed] 已发送退款失败通知邮件,refundId={}", refundId);
|
||||
} catch (Exception e) {
|
||||
log.error("[handleRefundFailed] 发送退款失败通知邮件异常,refundId={},error={}", refundId, e.getMessage(), e);
|
||||
}
|
||||
|
||||
return refundInfo;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,22 +23,18 @@ import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.google.gson.Gson;
|
||||
import com.stripe.Stripe;
|
||||
import com.stripe.exception.InvalidRequestException;
|
||||
import com.stripe.exception.SignatureVerificationException;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.stripe.model.*;
|
||||
import com.stripe.model.Product;
|
||||
import com.stripe.model.checkout.Session;
|
||||
import com.stripe.net.Webhook;
|
||||
import com.stripe.param.*;
|
||||
import com.stripe.param.checkout.SessionCreateParams;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -46,14 +42,16 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
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")
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -62,14 +60,10 @@ public class StripeServiceImpl implements StripeService {
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
@Resource
|
||||
private PayPalCheckoutService payPalCheckoutService;
|
||||
@Resource
|
||||
private PaymentInfoService paymentInfoService;
|
||||
@Resource
|
||||
private CreditsService creditsService;
|
||||
@Resource
|
||||
private RefundInfoService refundInfoService;
|
||||
@Resource
|
||||
private AccountService accountService;
|
||||
@Resource
|
||||
private AccountMapper accountMapper;
|
||||
@@ -81,6 +75,8 @@ public class StripeServiceImpl implements StripeService {
|
||||
private ProductCouponsMapper productCouponsMapper;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
@Resource
|
||||
private StripeWebhookService stripeWebhookService;
|
||||
|
||||
@Value("${stripe.private-key}")
|
||||
private String privateKey;
|
||||
@@ -109,6 +105,7 @@ public class StripeServiceImpl implements StripeService {
|
||||
productPurchaseDTO.setAutoRenewal(false);
|
||||
break;
|
||||
case "Subscription":
|
||||
productPurchaseDTO.setAutoRenewal(true);
|
||||
switch (productPurchaseDTO.getSubscribeType()){
|
||||
case "Month":
|
||||
productEnum = ProductEnum.MonthlySubscription;
|
||||
@@ -144,16 +141,13 @@ public class StripeServiceImpl implements StripeService {
|
||||
}
|
||||
log.info("生成订单");
|
||||
String payType;
|
||||
byte autoRenewal;
|
||||
if (productPurchaseDTO.getAutoRenewal()){
|
||||
payType = "recurring";
|
||||
autoRenewal = 1;
|
||||
}else {
|
||||
payType = "one_time";
|
||||
autoRenewal = 0;
|
||||
}
|
||||
OrderInfo orderInfo = orderInfoService.createOrderByProductId(productPurchaseDTO.getQuantity(),
|
||||
PayTypeEnum.STRIPE.getType(), productEnum, request, autoRenewal);
|
||||
PayTypeEnum.STRIPE.getType(), productEnum, request);
|
||||
|
||||
try {
|
||||
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.
|
||||
if (payType.equals("recurring")){
|
||||
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 {
|
||||
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
|
||||
sessionBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(Boolean.TRUE).build());
|
||||
}
|
||||
@@ -189,7 +193,8 @@ public class StripeServiceImpl implements StripeService {
|
||||
.setQuantity((long) productPurchaseDTO.getQuantity())
|
||||
.setPrice(priceId)
|
||||
.build());
|
||||
sessionBuilder.putMetadata("orderId", orderId); //通过订单号关联用于检索支付信息(可选)
|
||||
// 将 orderId 写入 metadata,Stripe Checkout 会自动传递给关联的 PaymentIntent/Subscription
|
||||
sessionBuilder.putMetadata("orderId", orderId);
|
||||
|
||||
Session session = Session.create(sessionBuilder.build());
|
||||
List<String> paymentMethodTypes = session.getPaymentMethodTypes();
|
||||
@@ -276,422 +281,9 @@ public class StripeServiceImpl implements StripeService {
|
||||
return Price.create(priceCreateParams.build());
|
||||
}
|
||||
|
||||
@Resource
|
||||
private EmailService emailService;
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean notify(HttpServletRequest request) {
|
||||
log.info("stripe异步通知进行中");
|
||||
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;
|
||||
}
|
||||
return stripeWebhookService.notify(request);
|
||||
}
|
||||
|
||||
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) {
|
||||
Stripe.apiKey = privateKey;
|
||||
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) {
|
||||
Stripe.apiKey = privateKey;
|
||||
// 1、通过orderNo 查询sessionId
|
||||
// todo transactionId不再是sessionId而是invoiceId,所以这里需要更新
|
||||
PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0);
|
||||
List<PaymentInfo> paymentInfos = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC");
|
||||
if (paymentInfos == null || paymentInfos.isEmpty()) {
|
||||
log.warn("核实订单未找到 ===> {}", orderNo);
|
||||
return;
|
||||
}
|
||||
PaymentInfo paymentInfo = paymentInfos.get(0);
|
||||
String transactionId = paymentInfo.getTransactionId();
|
||||
if (transactionId == null) {
|
||||
log.warn("核实订单 transactionId 为空 ===> {}", orderNo);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Session session = Session.retrieve(paymentInfo.getTransactionId());
|
||||
if (Objects.isNull(session)) {
|
||||
log.warn("核实订单未创建 ===> {}", orderNo);
|
||||
return;
|
||||
} else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) {
|
||||
// 订单未支付 || 订单过期 ---> 均设置为超时未支付
|
||||
Session session = Session.retrieve(transactionId);
|
||||
String status = session.getStatus();
|
||||
if ("open".equals(status) || "expired".equals(status)) {
|
||||
log.info("订单超时未支付 ===> {}", orderNo);
|
||||
//更新本地订单状态
|
||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED);
|
||||
paymentInfoService.updatePaymentStatusById(paymentInfo.getId(),
|
||||
session.getStatus(),
|
||||
new Gson().toJson(session));
|
||||
} else if (session.getStatus().equals("complete")) {
|
||||
// 订单已完成
|
||||
processOrder(session);
|
||||
status, new Gson().toJson(session));
|
||||
} else if ("complete".equals(status)) {
|
||||
// 订单已完成,通过 Checkout 事件处理(积分/订阅)已在 checkout.session.completed 中处理
|
||||
// 此处仅确保本地订单状态一致
|
||||
String currentStatus = orderInfoService.getOrderByOrderNo(orderNo).getOrderStatus();
|
||||
if (!OrderStatusEnum.SUCCESS.getType().equals(currentStatus)) {
|
||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS);
|
||||
}
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
log.error("根据sessionId获取Stripe Session失败");
|
||||
throw new RuntimeException(e);
|
||||
// transactionId 可能是 invoiceId(Payment Mode),此时无法用 sessionId 查询
|
||||
log.warn("根据 transactionId={} 查询 Stripe Session 失败,可能为 invoiceId,error={}", transactionId, e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public List<Subscription> getSubscription(String username, String userEmail) {
|
||||
@@ -933,84 +402,6 @@ public class StripeServiceImpl implements StripeService {
|
||||
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) {
|
||||
SubscriptionInfo subscriptionInfo;
|
||||
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);
|
||||
|
||||
qwPI.lambda().eq(PaymentInfo::getOrderNo, subscriptionInfo.getOrderNo())
|
||||
.eq(PaymentInfo::getTradeState, "paid")
|
||||
.between(PaymentInfo::getCreateTime, periodStart, periodEnd)
|
||||
.orderByDesc(PaymentInfo::getId);
|
||||
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qwPI);
|
||||
if (paymentInfos.isEmpty()) {
|
||||
log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo);
|
||||
log.info("不发送邮件,原因:【根据order_no:{},查询到的成功的paymentInfos为空】", orderNo);
|
||||
return false;
|
||||
}
|
||||
PaymentInfo paymentInfo = paymentInfos.get(0);
|
||||
@@ -1094,7 +486,8 @@ public class StripeServiceImpl implements StripeService {
|
||||
emailParamsDTO.setEmail(account.getUserEmail());
|
||||
emailParamsDTO.setCountry(paymentInfo.getCountry());
|
||||
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.setQuantity(String.valueOf(1));
|
||||
emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString());
|
||||
@@ -1172,7 +565,7 @@ public class StripeServiceImpl implements StripeService {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){
|
||||
/*public boolean sendRenewalFailEmail(String invoiceId, String subscriptionId, String orderNo){
|
||||
// 1、确认当前订单最后一笔支付为fail
|
||||
// 更新支付信息
|
||||
PaymentInfo paymentInfo;
|
||||
@@ -1232,7 +625,7 @@ public class StripeServiceImpl implements StripeService {
|
||||
payment.setUpdateTime(LocalDateTime.now());
|
||||
paymentInfoMapper.updateById(payment);
|
||||
return true;
|
||||
}
|
||||
}*/
|
||||
|
||||
private void setSubscriptionParams(PaymentInfo paymentInfo, SubscriptionInfo subscriptionInfo, OrderInfo orderByOrderNo,
|
||||
SubscriptionEmailParamsDTO emailParamsDTO, String language) {
|
||||
@@ -1302,7 +695,7 @@ public class StripeServiceImpl implements StripeService {
|
||||
}
|
||||
}*/
|
||||
|
||||
public void checkSubscriptionExpiration(){
|
||||
/*public void checkSubscriptionExpiration(){
|
||||
long epochSecond = Instant.now().getEpochSecond();
|
||||
QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
||||
qw.lt("current_period_end", epochSecond);
|
||||
@@ -1315,13 +708,13 @@ public class StripeServiceImpl implements StripeService {
|
||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
||||
log.info("用户 {} 的订阅 {} 已过期", subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo());
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
// 新建一个订阅 使用不会成功的付款方式(仅供测试使用)
|
||||
public String createSubscriptionTemp(String name, String email){
|
||||
Stripe.apiKey = privateKey;
|
||||
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 paymentMethodCode = "pm_card_mastercard";
|
||||
@@ -1527,9 +920,17 @@ public class StripeServiceImpl implements StripeService {
|
||||
|
||||
public PromotionCode createPromotionCode(String couponId, Long maxRedemption){
|
||||
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()
|
||||
.setCoupon(couponId)
|
||||
.setRestrictions(PromotionCodeCreateParams.Restrictions.builder().build());
|
||||
.setPromotion(promotion); // 使用 setPromotion 而不是 setCoupon
|
||||
|
||||
if (Objects.nonNull(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.QueryWrapper;
|
||||
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.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
@@ -28,6 +29,7 @@ import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -37,11 +39,9 @@ import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.DateTimeParseException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
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.PENDING;
|
||||
|
||||
@@ -78,9 +78,9 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
||||
}
|
||||
|
||||
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)) {
|
||||
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
|
||||
public void updatePlan(UpdateSubscriptionPlanDTO updateDTO) {
|
||||
if (Objects.isNull(updateDTO.getId())) {
|
||||
public void updatePlan(UpdateSubscriptionPlanDTO dto) {
|
||||
if (dto.getId() == null) {
|
||||
throw new BusinessException("id.cannot.be.empty");
|
||||
}
|
||||
|
||||
SubscriptionPlan subscriptionPlan = baseMapper.selectById(updateDTO.getId());
|
||||
if (Objects.isNull(subscriptionPlan)) {
|
||||
SubscriptionPlan plan = baseMapper.selectById(dto.getId());
|
||||
if (plan == null) {
|
||||
throw new BusinessException("unknown.subscription.plan");
|
||||
}
|
||||
|
||||
if (Objects.nonNull(updateDTO.getCurrentPeriodStart()) && !updateDTO.getCurrentPeriodStart().equals(subscriptionPlan.getCurrentPeriodStart())) {
|
||||
subscriptionPlan.setCurrentPeriodStart(updateDTO.getCurrentPeriodStart());
|
||||
}
|
||||
boolean activateToday = false;
|
||||
|
||||
if (Objects.nonNull(updateDTO.getCurrentPeriodEnd()) && !updateDTO.getCurrentPeriodEnd().equals(subscriptionPlan.getCurrentPeriodEnd())) {
|
||||
subscriptionPlan.setCurrentPeriodEnd(updateDTO.getCurrentPeriodEnd());
|
||||
}
|
||||
activateToday = handlePeriodStart(dto, plan);
|
||||
handlePeriodEnd(dto, plan);
|
||||
handleAccountNum(dto, plan);
|
||||
handleCreditLimit(dto, plan);
|
||||
handleBasicInfo(dto, plan);
|
||||
|
||||
if (Objects.nonNull(updateDTO.getAccountNum()) && !updateDTO.getAccountNum().equals(subscriptionPlan.getAccountNum())) {
|
||||
subscriptionPlan.setAccountNum(updateDTO.getAccountNum());
|
||||
}
|
||||
|
||||
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);
|
||||
plan.setUpdateTime(LocalDateTime.now());
|
||||
updateById(plan);
|
||||
|
||||
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
|
||||
@@ -167,6 +342,12 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
||||
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);
|
||||
|
||||
}
|
||||
@@ -221,6 +402,9 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
||||
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)
|
||||
.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");
|
||||
}
|
||||
}
|
||||
@@ -418,18 +602,28 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
||||
return currentPeriodEnd < currentTimestamp;
|
||||
}
|
||||
|
||||
public void activeSubscriptionPlan() {
|
||||
public void activeSubscriptionPlan(Long planId) {
|
||||
log.info("开始执行订阅计划生效检查...");
|
||||
|
||||
// 1. 扫描所有的订阅计划的开始时间currentPeriodStart,找出今天开始生效的计划
|
||||
List<SubscriptionPlan> todayActivePlans = findTodayActivePlans();
|
||||
// 支持按id激活
|
||||
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)) {
|
||||
log.info("今日没有需要生效的订阅计划");
|
||||
return;
|
||||
if (CollectionUtils.isEmpty(todayActivePlans)) {
|
||||
log.info("今日没有需要生效的订阅计划");
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("发现{}个今日生效的订阅计划", todayActivePlans.size());
|
||||
}
|
||||
|
||||
log.info("发现{}个今日生效的订阅计划", todayActivePlans.size());
|
||||
|
||||
// 2. 处理每个今天开始生效的订阅计划
|
||||
for (SubscriptionPlan plan : todayActivePlans) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.ai.da.model.vo.DesignPythonOutfitVO;
|
||||
import com.ai.da.model.vo.TDesignPythonOutfitDetailVO;
|
||||
import com.ai.da.service.ITDesignPythonOutfitDetailService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
@@ -67,6 +68,13 @@ public class TDesignPythonOutfitDetailServiceImpl extends ServiceImpl<TDesignPyt
|
||||
designPythonOutfitVO.setScale(modifyScale(detail.getScale()));
|
||||
designPythonOutfitVO.setOffset(StringUtil.isNullOrEmpty(detail.getOffset()) ? Arrays.asList(0L, 0L) : parseLongList(detail.getOffset()));
|
||||
designPythonOutfitVO.setPriority(Math.abs(detail.getPriority()));
|
||||
if (detail.getTranspose() != null) {
|
||||
List<Integer> transposeList = JSONArray.parseArray(detail.getTranspose(), Integer.class);
|
||||
designPythonOutfitVO.setTranspose(transposeList.stream().mapToInt(Integer::intValue).toArray());
|
||||
} else {
|
||||
designPythonOutfitVO.setTranspose(null);
|
||||
}
|
||||
designPythonOutfitVO.setRotate(detail.getRotate());
|
||||
// designPythonOutfitVO.setOffset(CollectionUtil.isEmpty(offset) ? Arrays.asList(0L, 0L) : offset);
|
||||
|
||||
/*if (!StringUtil.isNullOrEmpty(detail.getImageSize())){
|
||||
|
||||
@@ -905,15 +905,15 @@ public class UserLikeGroupServiceImpl extends ServiceImpl<UserLikeGroupMapper, U
|
||||
}
|
||||
}
|
||||
// 将构建好的结果对象添加到返回列表
|
||||
results.add(magicToolResultVO);
|
||||
// results.add(magicToolResultVO);
|
||||
} else if (Objects.isNull(magicToolResultVO)) {
|
||||
// 如果Redis中没有结果对象,创建执行中状态的结果对象
|
||||
magicToolResultVO = new MagicToolResultVO(taskId, "Executing");
|
||||
results.add(magicToolResultVO);
|
||||
} else {
|
||||
// results.add(magicToolResultVO);
|
||||
}/* else {
|
||||
// 如果Redis中有结果对象但URL为空,直接添加到返回列表
|
||||
results.add(magicToolResultVO);
|
||||
}
|
||||
}*/
|
||||
|
||||
// 收集任务状态用于统计
|
||||
if (!StringUtil.isNullOrEmpty(magicToolResultVO.getStatus())) collect.add(magicToolResultVO.getStatus());
|
||||
@@ -1461,12 +1461,16 @@ public class UserLikeGroupServiceImpl extends ServiceImpl<UserLikeGroupMapper, U
|
||||
if (StringUtil.isNullOrEmpty(fluxResult)) {
|
||||
toProductImageResult.setStatus("Fail");
|
||||
toProductImageResultMapper.updateById(toProductImageResult);
|
||||
sortRank(toProductImageResult);
|
||||
if (toProductImageResult.getIsLike() != null && toProductImageResult.getIsLike() == 1) {
|
||||
sortRank(toProductImageResult);
|
||||
}
|
||||
results.add(new MagicToolResultVO(taskId, "Fail"));
|
||||
} else if (fluxResult.equals("Fail") || fluxResult.equals("Pending")) {
|
||||
toProductImageResult.setStatus(fluxResult);
|
||||
toProductImageResultMapper.updateById(toProductImageResult);
|
||||
sortRank(toProductImageResult);
|
||||
if (fluxResult.equals("Fail") && toProductImageResult.getIsLike() != null && toProductImageResult.getIsLike() == 1) {
|
||||
sortRank(toProductImageResult);
|
||||
}
|
||||
results.add(new MagicToolResultVO(taskId, fluxResult));
|
||||
} else {
|
||||
results.add(processFluxResult(fluxResult, toProductImageResult, taskId, toProductImageRecord.getPrompt()));
|
||||
@@ -2203,10 +2207,14 @@ public class UserLikeGroupServiceImpl extends ServiceImpl<UserLikeGroupMapper, U
|
||||
childCollectionQw.lambda().orderByAsc(CollectionSort::getSort);
|
||||
List<CollectionSort> childSortList = collectionSortMapper.selectList(childCollectionQw);
|
||||
List<AllCollectionVO> childList = new ArrayList<>();
|
||||
// 收集需要删除的失败记录ID,用于后续统一清理并重新排序
|
||||
List<Long> failedSortIds = new ArrayList<>();
|
||||
for (CollectionSort userLikeSort : childSortList) {
|
||||
if (userLikeSort.getRelationType().equals(CollectionType.TO_PRODUCT_IMAGE.getValue())) {
|
||||
ToProductImageResult toProductImageResult = toProductImageResultMapper.selectById(userLikeSort.getRelationId());
|
||||
if (isGenerateTaskFailed(toProductImageResult.getStatus(), toProductImageResult.getCreateTime())) {
|
||||
failedSortIds.add(userLikeSort.getId());
|
||||
log.info("【获取内容】TO_PRODUCT_IMAGE结果失败,relationId={},即将从collection_sort中删除", userLikeSort.getRelationId());
|
||||
continue;
|
||||
}
|
||||
toProductImageResult.setUrl(getMinioUrl(toProductImageResult.getUrl()));
|
||||
@@ -2238,6 +2246,8 @@ public class UserLikeGroupServiceImpl extends ServiceImpl<UserLikeGroupMapper, U
|
||||
} else if (userLikeSort.getRelationType().equals(CollectionType.RELIGHT.getValue())) {
|
||||
ToProductImageResult toProductImageResult = toProductImageResultMapper.selectById(userLikeSort.getRelationId());
|
||||
if (isGenerateTaskFailed(toProductImageResult.getStatus(), toProductImageResult.getCreateTime())) {
|
||||
failedSortIds.add(userLikeSort.getId());
|
||||
log.info("【获取内容】RELIGHT结果失败,relationId={},即将从collection_sort中删除", userLikeSort.getRelationId());
|
||||
continue;
|
||||
}
|
||||
toProductImageResult.setUrl(getMinioUrl(toProductImageResult.getUrl()));
|
||||
@@ -2269,6 +2279,8 @@ public class UserLikeGroupServiceImpl extends ServiceImpl<UserLikeGroupMapper, U
|
||||
} else if (userLikeSort.getRelationType().equals(CollectionType.POSE_TRANSFORM.getValue())) {
|
||||
PoseTransformation item = poseTransformationMapper.selectById(userLikeSort.getRelationId());
|
||||
if (isGenerateTaskFailed(item.getTaskStatus(), item.getCreateTime())) {
|
||||
failedSortIds.add(userLikeSort.getId());
|
||||
log.info("【获取内容】POSE_TRANSFORM结果失败,relationId={},即将从collection_sort中删除", userLikeSort.getRelationId());
|
||||
continue;
|
||||
}
|
||||
PoseTransformationVO poseTransformationVO = new PoseTransformationVO();
|
||||
@@ -2293,6 +2305,114 @@ public class UserLikeGroupServiceImpl extends ServiceImpl<UserLikeGroupMapper, U
|
||||
childList.add(poseTransformationVO);
|
||||
}
|
||||
}
|
||||
// 统一处理失败的记录:从collection_sort表中删除失败的记录,并重新排序
|
||||
if (CollectionUtil.isNotEmpty(failedSortIds)) {
|
||||
Long parentId = collectionSort.getId();
|
||||
Long projectId = projectDTO.getId();
|
||||
log.info("【获取内容】检测到{}条失败记录需要清理,parentId={}, projectId={}", failedSortIds.size(), parentId, projectId);
|
||||
for (Long failedSortId : failedSortIds) {
|
||||
CollectionSort failedRecord = collectionSortMapper.selectById(failedSortId);
|
||||
if (failedRecord != null) {
|
||||
String relationType = failedRecord.getRelationType();
|
||||
Long relationId = failedRecord.getRelationId();
|
||||
collectionSortService.deleteCollectionSort(relationId, relationType, projectId, parentId);
|
||||
log.info("【获取内容】已删除失败记录,relationId={}, relationType={}", relationId, relationType);
|
||||
}
|
||||
}
|
||||
// 重新查询子列表,获取更新后的排序
|
||||
childSortList = collectionSortMapper.selectList(childCollectionQw);
|
||||
// 重新构建childList,使用更新后的sort值
|
||||
childList = new ArrayList<>();
|
||||
for (CollectionSort userLikeSort : childSortList) {
|
||||
if (userLikeSort.getRelationType().equals(CollectionType.TO_PRODUCT_IMAGE.getValue())) {
|
||||
ToProductImageResult toProductImageResult = toProductImageResultMapper.selectById(userLikeSort.getRelationId());
|
||||
if (isGenerateTaskFailed(toProductImageResult.getStatus(), toProductImageResult.getCreateTime())) {
|
||||
continue;
|
||||
}
|
||||
toProductImageResult.setUrl(getMinioUrl(toProductImageResult.getUrl()));
|
||||
ToProductImageResultVO toProductImageResultVO = CopyUtil.copyObject(toProductImageResult, ToProductImageResultVO.class);
|
||||
|
||||
ToProductImageRecord toProductImageRecord = toProductImageRecordMapper.selectById(toProductImageResult.getToProductImageRecordId());
|
||||
if (Objects.isNull(toProductImageRecord)) {
|
||||
continue;
|
||||
}
|
||||
toProductImageResultVO.setPrompt(toProductImageRecord.getPrompt());
|
||||
|
||||
if (toProductImageResultVO.getElementType().equals("ProductElement")) {
|
||||
ToProductElement toProductElement = toProductElementMapper.selectById(toProductImageResultVO.getElementId());
|
||||
toProductImageResultVO.setSourceUrl(getMinioUrl(toProductElement.getUrl()));
|
||||
} else if ((toProductImageResultVO.getElementType().equals("DesignOutfit"))) {
|
||||
TDesignPythonOutfit tDesignPythonOutfit = designPythonOutfitMapper.selectById(toProductImageResultVO.getElementId());
|
||||
toProductImageResultVO.setSourceUrl(getMinioUrl(tDesignPythonOutfit.getDesignUrl()));
|
||||
} else {
|
||||
ToProductImageResult toProductImageResult1 = toProductImageResultMapper.selectById(toProductImageResultVO.getElementId());
|
||||
toProductImageResultVO.setSourceUrl(getMinioUrl(toProductImageResult1.getUrl()));
|
||||
}
|
||||
toProductImageResultVO.setCollectionType(CollectionType.TO_PRODUCT_IMAGE.getValue());
|
||||
toProductImageResultVO.setSort(userLikeSort.getSort());
|
||||
toProductImageResultVO.setUserLikeSortId(userLikeSort.getId());
|
||||
toProductImageResultVO.setRelationType(userLikeSort.getRelationType());
|
||||
toProductImageResultVO.setParentId(userLikeSort.getParentId());
|
||||
childList.add(toProductImageResultVO);
|
||||
} else if (userLikeSort.getRelationType().equals(CollectionType.RELIGHT.getValue())) {
|
||||
ToProductImageResult toProductImageResult = toProductImageResultMapper.selectById(userLikeSort.getRelationId());
|
||||
if (isGenerateTaskFailed(toProductImageResult.getStatus(), toProductImageResult.getCreateTime())) {
|
||||
continue;
|
||||
}
|
||||
toProductImageResult.setUrl(getMinioUrl(toProductImageResult.getUrl()));
|
||||
ToProductImageResultVO toProductImageResultVO = CopyUtil.copyObject(toProductImageResult, ToProductImageResultVO.class);
|
||||
|
||||
ToProductImageRecord toProductImageRecord = toProductImageRecordMapper.selectById(toProductImageResult.getToProductImageRecordId());
|
||||
if (Objects.isNull(toProductImageRecord)) {
|
||||
continue;
|
||||
}
|
||||
toProductImageResultVO.setPrompt(toProductImageRecord.getPrompt());
|
||||
|
||||
if (toProductImageResultVO.getElementType().equals("ProductElement")) {
|
||||
ToProductElement toProductElement = toProductElementMapper.selectById(toProductImageResultVO.getElementId());
|
||||
toProductImageResultVO.setSourceUrl(getMinioUrl(toProductElement.getUrl()));
|
||||
} else if ((toProductImageResultVO.getElementType().equals("DesignOutfit"))) {
|
||||
TDesignPythonOutfit tDesignPythonOutfit = designPythonOutfitMapper.selectById(toProductImageResultVO.getElementId());
|
||||
toProductImageResultVO.setSourceUrl(getMinioUrl(tDesignPythonOutfit.getDesignUrl()));
|
||||
} else {
|
||||
ToProductImageResult toProductImageResult1 = toProductImageResultMapper.selectById(toProductImageResultVO.getElementId());
|
||||
toProductImageResultVO.setSourceUrl(getMinioUrl(toProductImageResult1.getUrl()));
|
||||
}
|
||||
toProductImageResultVO.setCollectionType(CollectionType.RELIGHT.getValue());
|
||||
toProductImageResultVO.setSort(userLikeSort.getSort());
|
||||
toProductImageResultVO.setUserLikeSortId(userLikeSort.getId());
|
||||
toProductImageResultVO.setRelationType(userLikeSort.getRelationType());
|
||||
toProductImageResultVO.setParentId(userLikeSort.getParentId());
|
||||
childList.add(toProductImageResultVO);
|
||||
} else if (userLikeSort.getRelationType().equals(CollectionType.POSE_TRANSFORM.getValue())) {
|
||||
PoseTransformation item = poseTransformationMapper.selectById(userLikeSort.getRelationId());
|
||||
if (isGenerateTaskFailed(item.getTaskStatus(), item.getCreateTime())) {
|
||||
continue;
|
||||
}
|
||||
PoseTransformationVO poseTransformationVO = new PoseTransformationVO();
|
||||
poseTransformationVO.setId(item.getId());
|
||||
poseTransformationVO.setTaskId(item.getUniqueId());
|
||||
poseTransformationVO.setProductImage(getMinioUrl(item.getProductImage()));
|
||||
poseTransformationVO.setLastFrameProductImage(getMinioUrl(item.getLastFrameProductImage()));
|
||||
poseTransformationVO.setPrompt(item.getPrompt());
|
||||
poseTransformationVO.setGifUrl(getMinioUrl(item.getGifUrl()));
|
||||
poseTransformationVO.setVideoUrl(getMinioUrl(item.getVideoUrl()));
|
||||
poseTransformationVO.setFirstFrameUrl(getMinioUrl(item.getFirstFrameUrl()));
|
||||
poseTransformationVO.setIsLiked(item.getIsLiked());
|
||||
poseTransformationVO.setCollectionType(CollectionType.POSE_TRANSFORM.getValue());
|
||||
poseTransformationVO.setSort(userLikeSort.getSort());
|
||||
poseTransformationVO.setUserLikeSortId(userLikeSort.getId());
|
||||
poseTransformationVO.setRelationType(userLikeSort.getRelationType());
|
||||
poseTransformationVO.setResultType(CollectionType.POSE_TRANSFORM.getValue());
|
||||
poseTransformationVO.setParentId(userLikeSort.getParentId());
|
||||
poseTransformationVO.setModelName(item.getModelName());
|
||||
poseTransformationVO.setPoseId(item.getPoseId());
|
||||
poseTransformationVO.setStatus(item.getTaskStatus());
|
||||
childList.add(poseTransformationVO);
|
||||
}
|
||||
}
|
||||
log.info("【获取内容】失败记录清理完成,重新排序后childList.size={}", childList.size());
|
||||
}
|
||||
o.setChildList(childList);
|
||||
|
||||
list.add(o);
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.ai.da.service.stripe.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
|
||||
/**
|
||||
* Stripe Webhook 异步处理线程池配置
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class StripeWebhookAsyncConfig {
|
||||
|
||||
/**
|
||||
* Stripe Webhook 专用线程池
|
||||
*/
|
||||
@Bean(name = "stripeWebhookExecutor")
|
||||
public Executor stripeWebhookExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(4);
|
||||
executor.setMaxPoolSize(8);
|
||||
executor.setQueueCapacity(100);
|
||||
executor.setKeepAliveSeconds(60);
|
||||
executor.setThreadNamePrefix("stripe-webhook-");
|
||||
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.setWaitForTasksToCompleteOnShutdown(true);
|
||||
executor.setAwaitTerminationSeconds(30);
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user