Compare commits
40 Commits
dev/3.1_re
...
dev/3.1_re
| Author | SHA1 | Date | |
|---|---|---|---|
| fe9cc99701 | |||
| 73c366d827 | |||
|
|
85e02a895c | ||
|
|
148bb84f3c | ||
|
|
931eef6f53 | ||
|
|
3d9a6aa9e9 | ||
| 11073690e5 | |||
|
|
921d2d956e | ||
|
|
d700f94f9d | ||
|
|
b277479e73 | ||
|
|
83cbd57dea | ||
|
|
4d3b22de82 | ||
|
|
6b5c2cfec0 | ||
|
|
b676de054a | ||
|
|
4c169ef67e | ||
|
|
f2bce066b6 | ||
|
|
6af442eb15 | ||
|
|
768df55309 | ||
|
|
f351277b73 | ||
|
|
a799162ea4 | ||
|
|
c035eb9d7d | ||
|
|
906a54b3c8 | ||
|
|
643799546b | ||
|
|
f582464cd3 | ||
|
|
b864b393bc | ||
|
|
c03a8762e7 | ||
|
|
cb87ad1099 | ||
|
|
fb229764f8 | ||
|
|
8bec1f842d | ||
|
|
b54bd04cff | ||
|
|
b4ccad6242 | ||
|
|
6068bf7d7d | ||
|
|
d36baf747f | ||
| 7c8f1bee6a | |||
|
|
62bd145e2c | ||
|
|
23716984cc | ||
|
|
d0b8b8d674 | ||
|
|
92e7dbf258 | ||
|
|
32a228485b | ||
|
|
560e47747a |
111
.gitea/workflows/develop_3.1_MS_build_manual.yaml
Normal file
111
.gitea/workflows/develop_3.1_MS_build_manual.yaml
Normal file
@@ -0,0 +1,111 @@
|
||||
name: 手动 AiDA back-java 开发分支构建部署
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
runs-on: java21
|
||||
|
||||
outputs:
|
||||
build_status: ${{ job.status }}
|
||||
build_url: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
REMOTE_DEPLOY_PATH: /workspace/workspace_aida/DevelopVersion/develop-MS-version-aida-back
|
||||
|
||||
steps:
|
||||
- name: 0.记录开始时间
|
||||
id: build_start_time
|
||||
run: echo "current_time=$(TZ='Asia/Hong_Kong' date '+%Y-%m-%d %H:%M:%S %Z')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: 1.检出代码
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: dev/3.1_release_merge_MS
|
||||
|
||||
|
||||
- name: 3.缓存 Maven 依赖
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
|
||||
- name: 4.构建项目
|
||||
run: |
|
||||
java -version
|
||||
mvn -v
|
||||
mvn clean package -DskipTests
|
||||
|
||||
- name: 5.生成Dockerfile
|
||||
run: |
|
||||
echo "===== 生成Dockerfile ====="
|
||||
cat > Dockerfile << 'EOF'
|
||||
FROM openjdk:21-ea-21-jdk-slim
|
||||
VOLUME /tmp
|
||||
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
|
||||
RUN echo 'Asia/Shanghai' > /etc/timezone
|
||||
ADD ./target/aida-0.0.1-SNAPSHOT.jar /app.jar
|
||||
ENTRYPOINT ["java","-jar","/app.jar"]
|
||||
EOF
|
||||
echo "Dockerfile内容:"
|
||||
cat Dockerfile
|
||||
|
||||
- name: 6.生成docker-compose.yml
|
||||
run: |
|
||||
echo "===== 生成docker-compose.yml ====="
|
||||
cat > docker-compose.yml << 'EOF'
|
||||
version: '3'
|
||||
services:
|
||||
aida_back:
|
||||
container_name: develop-aida-ms
|
||||
build: .
|
||||
volumes:
|
||||
# 数据挂载
|
||||
- ./log:/log
|
||||
- ./temp:/temp
|
||||
- ./uploads:/temp/uploads
|
||||
ports:
|
||||
- '10092:10092'
|
||||
restart: always
|
||||
EOF
|
||||
# 验证docker-compose.yml生成
|
||||
echo "docker-compose.yml内容:"
|
||||
cat docker-compose.yml
|
||||
|
||||
- name: 7.上传jar到远程服务器
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
port: 22
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
source: "target/*.jar,Dockerfile,docker-compose.yml"
|
||||
target: ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
preserve_host_directory_structure: false
|
||||
|
||||
- name: 8. 重启 Docker 服务
|
||||
uses: appleboy/ssh-action@master # 👈 专门执行命令的 action
|
||||
with:
|
||||
host: ${{ secrets.SERVER_HOST }}
|
||||
username: ${{ secrets.SERVER_USER }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
key_base64: true
|
||||
script: |
|
||||
echo "========= 进入部署目录 ========="
|
||||
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
ls -l
|
||||
|
||||
echo "========= 停止旧服务 ========="
|
||||
docker compose down
|
||||
|
||||
echo "========= 启动新服务 ========="
|
||||
docker compose up -d --build
|
||||
|
||||
echo "========= 查看运行状态 ========="
|
||||
docker compose ps
|
||||
@@ -103,7 +103,13 @@ jobs:
|
||||
- ./uploads:/temp/uploads
|
||||
ports:
|
||||
- '10090:5567'
|
||||
networks:
|
||||
- aida_java_net
|
||||
restart: always
|
||||
networks:
|
||||
aida_java_net:
|
||||
external: true
|
||||
name: aida_java_net
|
||||
EOF
|
||||
# 验证docker-compose.yml生成
|
||||
echo "docker-compose.yml内容:"
|
||||
@@ -135,6 +141,8 @@ jobs:
|
||||
cd ${{ env.REMOTE_DEPLOY_PATH }}
|
||||
echo "停止旧容器..."
|
||||
docker compose down || true
|
||||
echo "清理Docker资源..."
|
||||
docker system prune -f
|
||||
echo "构建镜像..."
|
||||
docker compose build --no-cache
|
||||
echo "启动服务..."
|
||||
|
||||
67
pom.xml
67
pom.xml
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.1.6</version>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.aida</groupId>
|
||||
@@ -15,7 +15,7 @@
|
||||
<description>ai da</description>
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<mybatis.plus.version>3.5.5</mybatis.plus.version>
|
||||
<mybatis.plus.version>3.5.7</mybatis.plus.version>
|
||||
<hutool.version>5.8.23</hutool.version>
|
||||
<wx.java.version>4.2.7.B</wx.java.version>
|
||||
<fastjson.version>2.0.43</fastjson.version>
|
||||
@@ -28,6 +28,11 @@
|
||||
<javacv.version>1.5.5</javacv.version>
|
||||
<system.windowsx64>windows-x86_64</system.windowsx64>
|
||||
<javacpp.platform.linux-x86_64>linux-x86_64</javacpp.platform.linux-x86_64>
|
||||
|
||||
<!-- Spring Cloud Alibaba 版本 -->
|
||||
<spring-cloud-alibaba.version>2023.0.3.4</spring-cloud-alibaba.version>
|
||||
<!-- Spring Cloud 版本 -->
|
||||
<spring-cloud.version>2023.0.4</spring-cloud.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -38,6 +43,22 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Spring Cloud 依赖版本管理 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-dependencies</artifactId>
|
||||
<version>${spring-cloud.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Spring Cloud Alibaba 依赖版本管理 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
|
||||
<version>${spring-cloud-alibaba.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -74,9 +95,14 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
<version>${mybatis.plus.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>3.0.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@@ -237,10 +263,16 @@
|
||||
<version>2.15.1</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>3.13.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.stripe</groupId>
|
||||
<artifactId>stripe-java</artifactId>
|
||||
<version>32.0.0</version>
|
||||
<version>26.2.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- aws s3 -->
|
||||
@@ -432,6 +464,33 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- ==================== Spring Cloud Alibaba 微服务相关 ==================== -->
|
||||
<!-- 启用 bootstrap.yml 加载 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-bootstrap</artifactId>
|
||||
</dependency>
|
||||
<!-- Nacos 服务注册与发现 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
<!-- Nacos 配置中心 -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
<!-- OpenFeign 服务调用 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Cloud LoadBalancer 负载均衡 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
package com.ai.da;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@EnableAsync
|
||||
public class AiDaApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AiDaApplication.class, args);
|
||||
log.info("AiDaApplication 启动完成!");
|
||||
}
|
||||
|
||||
}
|
||||
package com.ai.da;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
@Slf4j
|
||||
@SpringBootApplication
|
||||
@EnableScheduling
|
||||
@EnableAsync
|
||||
@EnableFeignClients
|
||||
@EnableDiscoveryClient
|
||||
public class AiDaApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AiDaApplication.class, args);
|
||||
log.info("AiDaApplication 启动完成!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -559,83 +559,83 @@ public class GenerateConsumer {
|
||||
log.info("============ProcessPoseTransformResult End listening==========");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer1(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 1");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer2(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 2");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer3(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 3");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer4(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 4");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer5(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 5");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer6(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 6");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer7(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 7");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer8(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 8");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
@RabbitHandler
|
||||
public void generateConsumer9(Message msg, Channel channel) {
|
||||
generate(msg, channel, "consumer 9");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.generateResult}")
|
||||
@RabbitHandler
|
||||
public void getGenerateResult(Message msg, Channel channel) {
|
||||
processGenerateResult(msg, channel);
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.toProductImageResult}")
|
||||
@RabbitHandler
|
||||
public void getToProductImageResult(Message msg, Channel channel) {
|
||||
processToProductImageResult(msg, channel);
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.relightResult}")
|
||||
@RabbitHandler
|
||||
public void getRelightResult(Message msg, Channel channel) {
|
||||
processRelightResult(msg, channel);
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.poseTransform}")
|
||||
@RabbitHandler
|
||||
public void getPoseTransformationResult(Message msg, Channel channel) {
|
||||
processPoseTransformResult(msg, channel);
|
||||
}
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer1(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 1");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer2(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 2");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer3(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 3");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer4(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 4");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer5(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 5");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer6(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 6");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer7(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 7");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer8(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 8");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generate}")
|
||||
// @RabbitHandler
|
||||
// public void generateConsumer9(Message msg, Channel channel) {
|
||||
// generate(msg, channel, "consumer 9");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.generateResult}")
|
||||
// @RabbitHandler
|
||||
// public void getGenerateResult(Message msg, Channel channel) {
|
||||
// processGenerateResult(msg, channel);
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.toProductImageResult}")
|
||||
// @RabbitHandler
|
||||
// public void getToProductImageResult(Message msg, Channel channel) {
|
||||
// processToProductImageResult(msg, channel);
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.relightResult}")
|
||||
// @RabbitHandler
|
||||
// public void getRelightResult(Message msg, Channel channel) {
|
||||
// processRelightResult(msg, channel);
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.poseTransform}")
|
||||
// @RabbitHandler
|
||||
// public void getPoseTransformationResult(Message msg, Channel channel) {
|
||||
// processPoseTransformResult(msg, channel);
|
||||
// }
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.designBatch}")
|
||||
// @RabbitHandler
|
||||
// public void getDesignBatchResult(Message msg, Channel channel) {
|
||||
|
||||
@@ -222,16 +222,16 @@ public class SRConsumer {
|
||||
taskListService.updateTaskStatusOrOutputRedis(uniqueId, "fail", null);
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.sr}")
|
||||
@RabbitHandler
|
||||
public void SRConsumer1(Message msg, Channel channel) {
|
||||
superResolution(msg, channel, "consumer 1");
|
||||
}
|
||||
|
||||
@RabbitListener(queues = "#{rabbitMQProperties.queues.srResult}")
|
||||
@RabbitHandler
|
||||
public void SRResultConsumer1(Message msg, Channel channel) {
|
||||
getSRResult(msg, channel, "consumer 1");
|
||||
}
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.sr}")
|
||||
// @RabbitHandler
|
||||
// public void SRConsumer1(Message msg, Channel channel) {
|
||||
// superResolution(msg, channel, "consumer 1");
|
||||
// }
|
||||
//
|
||||
// @RabbitListener(queues = "#{rabbitMQProperties.queues.srResult}")
|
||||
// @RabbitHandler
|
||||
// public void SRResultConsumer1(Message msg, Channel channel) {
|
||||
// getSRResult(msg, channel, "consumer 1");
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
@@ -44,9 +44,11 @@ public class ControllerLoggingAspect {
|
||||
|
||||
// 获取当前用户ID
|
||||
Long userId = null;
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
if (authPrincipalVo != null) {
|
||||
try {
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
userId = authPrincipalVo.getId();
|
||||
} catch (RuntimeException e) {
|
||||
// 匿名接口,无认证上下文,忽略
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
@@ -121,9 +123,11 @@ public class ControllerLoggingAspect {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
|
||||
Long userId = null;
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
if (authPrincipalVo != null) {
|
||||
try {
|
||||
AuthPrincipalVo authPrincipalVo = UserContext.getUserHolder();
|
||||
userId = authPrincipalVo.getId();
|
||||
} catch (RuntimeException e) {
|
||||
// 匿名接口,无认证上下文,忽略
|
||||
}
|
||||
|
||||
// 获取请求参数
|
||||
|
||||
@@ -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);
|
||||
|
||||
34
src/main/java/com/ai/da/common/config/SecurityConfig.java
Normal file
34
src/main/java/com/ai/da/common/config/SecurityConfig.java
Normal file
@@ -0,0 +1,34 @@
|
||||
package com.ai.da.common.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* Spring Security 配置。
|
||||
* 由于鉴权逻辑已迁移至 Gateway(GlobalAuthWebFilter),
|
||||
* 后端服务 (aida-back) 默认放行所有请求,仅依赖网关传递的用户信息。
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用 CSRF(微服务通常不需要)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
// 允许所有请求,具体鉴权在网关层完成
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
// 禁用默认的表单登录和 HTTP Basic 认证,防止 302 重定向
|
||||
.formLogin(AbstractHttpConfigurer::disable)
|
||||
.httpBasic(AbstractHttpConfigurer::disable);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
package com.ai.da.common.config;
|
||||
|
||||
|
||||
import com.ai.da.common.interceptor.UserContextInterceptor;
|
||||
import org.hibernate.validator.HibernateValidator;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
@@ -17,11 +20,20 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
static final String ORIGINS[] = new String[]{"GET", "POST", "PUT", "DELETE"};
|
||||
|
||||
@Resource
|
||||
private UserContextInterceptor userContextInterceptor;
|
||||
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**").allowedOriginPatterns("*").allowCredentials(true).allowedMethods(ORIGINS).maxAge(3600);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(userContextInterceptor)
|
||||
.addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Validator validator() {
|
||||
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
|
||||
|
||||
@@ -1,89 +1,99 @@
|
||||
package com.ai.da.common.config.exception;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.ai.da.common.response.ResultEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: 全局异常捕获
|
||||
* @create: 2019-12-03 10:24
|
||||
**/
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public class ExceptionCatch {
|
||||
|
||||
/**
|
||||
* 线程安全,且构建后不可更改
|
||||
*/
|
||||
private static ImmutableMap<Class<? extends Throwable>, ResultEnum> EXCEPTIONS;
|
||||
|
||||
/**
|
||||
* 用于构建ImmutableMap
|
||||
*/
|
||||
private static ImmutableMap.Builder<Class<? extends Throwable>, ResultEnum> builder = ImmutableMap.builder();
|
||||
|
||||
@ResponseBody
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Response<String> businessExceptionCatch(BusinessException e) {
|
||||
log.error("发生业务异常,code:[{}],msg:[{}]", e.getCode(), e.getMsg(), e);
|
||||
return Response.error(e.getCode(), e.getMsg());
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Response<String> exceptionCatch(Exception e) {
|
||||
log.error("发生系统异常,message:[{}]", e.getMessage(), e);
|
||||
//如果ImmutableMap集合为空,构建ImmutableMap
|
||||
if (EXCEPTIONS == null || EXCEPTIONS.size() == 0) {
|
||||
EXCEPTIONS = builder.build();
|
||||
}
|
||||
//获取不可预知异常自定义错误码
|
||||
if (EXCEPTIONS != null) {
|
||||
ResultEnum resultEnum = EXCEPTIONS.get(e.getClass());
|
||||
if (resultEnum != null) {
|
||||
return Response.error(resultEnum.getCode(), resultEnum.getMsg());
|
||||
}
|
||||
}
|
||||
return Response.error(ResultEnum.ERROR.getCode(), e.getMessage() == null ? ResultEnum.ERROR.getMsg() : e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*
|
||||
* @param e
|
||||
* @return ResponseData
|
||||
*/
|
||||
@ResponseBody
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Response<String> bindExceptionHandler(BindException e) {
|
||||
log.error("参数错误bind:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
BusinessException businessException = new BusinessException(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
return Response.error(businessException.getCode(), businessException.getMsg());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*
|
||||
* @param e
|
||||
* @return ResponseData
|
||||
*/
|
||||
@ResponseBody
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Response<String> handleValidationException(MethodArgumentNotValidException e) {
|
||||
log.error("参数错误bind:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
BusinessException businessException = new BusinessException(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
return Response.error(businessException.getCode(), businessException.getMsg());
|
||||
}
|
||||
|
||||
//初始化,不可预知异常自定义错误编码
|
||||
static {
|
||||
// builder.put(FileNotFoundException.class, ResultEnum.FILE_NOT_EXIST);
|
||||
}
|
||||
}
|
||||
package com.ai.da.common.config.exception;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.ai.da.common.response.ResultEnum;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: 全局异常捕获
|
||||
* @create: 2019-12-03 10:24
|
||||
**/
|
||||
@Slf4j
|
||||
@ControllerAdvice
|
||||
public class ExceptionCatch {
|
||||
|
||||
/**
|
||||
* 线程安全,且构建后不可更改
|
||||
*/
|
||||
private static ImmutableMap<Class<? extends Throwable>, ResultEnum> EXCEPTIONS;
|
||||
|
||||
/**
|
||||
* 用于构建ImmutableMap
|
||||
*/
|
||||
private static ImmutableMap.Builder<Class<? extends Throwable>, ResultEnum> builder = ImmutableMap.builder();
|
||||
|
||||
@ResponseBody
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public Response<String> businessExceptionCatch(BusinessException e) {
|
||||
log.error("发生业务异常,code:[{}],msg:[{}]", e.getCode(), e.getMsg(), e);
|
||||
return Response.error(e.getCode(), e.getMsg());
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
@ExceptionHandler(UnauthorizedException.class)
|
||||
public Response<String> unauthorizedExceptionCatch(UnauthorizedException e) {
|
||||
log.error("Unauthorized: {}", e.getMessage());
|
||||
return Response.error(401, e.getMessage());
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@ExceptionHandler(Exception.class)
|
||||
public Response<String> exceptionCatch(Exception e) {
|
||||
log.error("发生系统异常,message:[{}]", e.getMessage(), e);
|
||||
//如果ImmutableMap集合为空,构建ImmutableMap
|
||||
if (EXCEPTIONS == null || EXCEPTIONS.size() == 0) {
|
||||
EXCEPTIONS = builder.build();
|
||||
}
|
||||
//获取不可预知异常自定义错误码
|
||||
if (EXCEPTIONS != null) {
|
||||
ResultEnum resultEnum = EXCEPTIONS.get(e.getClass());
|
||||
if (resultEnum != null) {
|
||||
return Response.error(resultEnum.getCode(), resultEnum.getMsg());
|
||||
}
|
||||
}
|
||||
return Response.error(ResultEnum.ERROR.getCode(), e.getMessage() == null ? ResultEnum.ERROR.getMsg() : e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*
|
||||
* @param e
|
||||
* @return ResponseData
|
||||
*/
|
||||
@ResponseBody
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Response<String> bindExceptionHandler(BindException e) {
|
||||
log.error("参数错误bind:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
BusinessException businessException = new BusinessException(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
return Response.error(businessException.getCode(), businessException.getMsg());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理参数校验异常
|
||||
*
|
||||
* @param e
|
||||
* @return ResponseData
|
||||
*/
|
||||
@ResponseBody
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Response<String> handleValidationException(MethodArgumentNotValidException e) {
|
||||
log.error("参数错误bind:{}", e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
BusinessException businessException = new BusinessException(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
|
||||
return Response.error(businessException.getCode(), businessException.getMsg());
|
||||
}
|
||||
|
||||
//初始化,不可预知异常自定义错误编码
|
||||
static {
|
||||
// builder.put(FileNotFoundException.class, ResultEnum.FILE_NOT_EXIST);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.ai.da.common.config.exception;
|
||||
|
||||
public class TokenMissingOrExpiredException extends RuntimeException {
|
||||
public TokenMissingOrExpiredException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Throwable fillInStackTrace() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.ai.da.common.config.exception;
|
||||
|
||||
public class UnauthorizedException extends RuntimeException {
|
||||
|
||||
public UnauthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public UnauthorizedException() {
|
||||
super("Gateway token verification failed");
|
||||
}
|
||||
}
|
||||
@@ -10,12 +10,11 @@ public class CommonConstant {
|
||||
// 单位 秒 两天过期
|
||||
public static final Long CREDITS_EXPIRE_TIME = 2 * 24 * 60 * 60L;
|
||||
// 单位 分钟
|
||||
public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60;
|
||||
public static final Integer MINIO_IMAGE_EXPIRE_TIME = 24 * 60 * 7;
|
||||
// 单位 秒 一天过期 in redis
|
||||
public static final Long GENERATE_RESULT_EXPIRE_TIME = 24 * 60 * 60L;
|
||||
// 单位 秒 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;
|
||||
// 单位 秒 7天过期
|
||||
public static final Long REDIS_SET_EXPIRE_TIME = 24 * 60 * 60 * 7L;
|
||||
|
||||
public static class Numbers{
|
||||
public static final Integer NUMBER_10 = 10;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package com.ai.da.common.constant;
|
||||
|
||||
/**
|
||||
* @author yanglei
|
||||
* 异常类常量
|
||||
*/
|
||||
public class TokenConstant {
|
||||
/**
|
||||
* 固定session
|
||||
*/
|
||||
public static final String FIX_SESSION = "qrLS_003af9d8c1363fc4_6c97e932665c4460a1fdbfbf47ce3490";
|
||||
|
||||
public static final String PERMISSIONS = "9672233956";
|
||||
}
|
||||
@@ -1,19 +1,41 @@
|
||||
package com.ai.da.common.context;
|
||||
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
|
||||
public class UserContext {
|
||||
private static ThreadLocal<AuthPrincipalVo> userHolder = new ThreadLocal<AuthPrincipalVo>();
|
||||
|
||||
public static AuthPrincipalVo getUserHolder() {
|
||||
return userHolder.get();
|
||||
}
|
||||
|
||||
public static void delete() {
|
||||
userHolder.remove();
|
||||
}
|
||||
|
||||
public static void setUserHolder(AuthPrincipalVo authPrincipalVo) {
|
||||
userHolder.set(authPrincipalVo);
|
||||
}
|
||||
}
|
||||
package com.ai.da.common.context;
|
||||
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
|
||||
public class UserContext {
|
||||
private static final ThreadLocal<AuthPrincipalVo> userHolder = new ThreadLocal<>();
|
||||
|
||||
public static void setUserHolder(AuthPrincipalVo authPrincipalVo) {
|
||||
userHolder.set(authPrincipalVo);
|
||||
}
|
||||
|
||||
public static AuthPrincipalVo getUserHolder() {
|
||||
AuthPrincipalVo holder = userHolder.get();
|
||||
if (holder == null) {
|
||||
throw new RuntimeException("User not authenticated");
|
||||
}
|
||||
if (!"AIDA".equals(holder.getSource())) {
|
||||
throw new RuntimeException("Access denied: source must be AIDA");
|
||||
}
|
||||
return holder;
|
||||
}
|
||||
|
||||
public static void delete() {
|
||||
userHolder.remove();
|
||||
}
|
||||
|
||||
public static Long getUserId() {
|
||||
return getUserHolder().getId();
|
||||
}
|
||||
|
||||
public static Long getBuyerId() {
|
||||
AuthPrincipalVo holder = userHolder.get();
|
||||
if (holder == null) {
|
||||
throw new RuntimeException("User not authenticated");
|
||||
}
|
||||
if (!"BUYER".equals(holder.getSource())) {
|
||||
throw new RuntimeException("Access denied: source must be BUYER");
|
||||
}
|
||||
return holder.getId();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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_DAILY("init_daily","100"),
|
||||
INIT_WEEKLY("init_weekly","6000"),
|
||||
RESET_YEAR_CREDITS("reset_year_credits","6000"),
|
||||
|
||||
// SUPER_RESOLUTION("Super Resolution","30"),
|
||||
|
||||
@@ -34,11 +34,6 @@ public enum OrderStatusEnum {
|
||||
* 已退款
|
||||
*/
|
||||
REFUND_SUCCESS("已退款"),
|
||||
|
||||
/**
|
||||
* 已部分退款
|
||||
*/
|
||||
PARTIAL_REFUND_SUCCESS("已部分退款"),
|
||||
/**
|
||||
* 退款异常
|
||||
*/
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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,8 +3,6 @@ package com.ai.da.common.enums;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProductEnum {
|
||||
@@ -25,27 +23,11 @@ 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.ai.da.common.httpdata.token;
|
||||
|
||||
public enum TokenApis {
|
||||
/**
|
||||
* token
|
||||
*/
|
||||
GET_TOKEN("POST", "/api/openApi/v2/Weixin/QrCodeLoginCheck?session="),
|
||||
GENERATE_USER("POST", "/api/openApi/v2/Welink/TopicGetjson");
|
||||
|
||||
private String method;
|
||||
private String url;
|
||||
|
||||
TokenApis(String method, String url) {
|
||||
this.method = method;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(String method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.ai.da.common.httpdata.token;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
import cn.hutool.http.HttpUtil;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class TokenQuery {
|
||||
|
||||
private static final String GET_TOKEN_DOMAIN = "https://www.szsige.com";
|
||||
private static final String GENERATE_USER_DOMAIN = "https://www.szsige.com";
|
||||
|
||||
public static JSONObject getToken(String session) {
|
||||
String url = GET_TOKEN_DOMAIN + TokenApis.GET_TOKEN.getUrl() + session;
|
||||
log.info("获取用户token接口请求url:" + url);
|
||||
HttpResponse httpResponse = HttpUtil.createPost(url).execute();
|
||||
log.info("获取用户token接口响应:" + httpResponse);
|
||||
if (httpResponse.isOk() && StrUtil.isNotEmpty(httpResponse.body())) {
|
||||
return JSONObject.parseObject(httpResponse.body());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static JSONObject generateUser(Map<String, Object> param, String token) {
|
||||
HttpResponse httpResponse = HttpUtil.createPost(GENERATE_USER_DOMAIN + TokenApis.GENERATE_USER.getUrl())
|
||||
.body(JSONObject.toJSONString(param != null ? param : new HashMap<>()))
|
||||
.header("Authorization", "Bearer " + token)
|
||||
.header("X-Promiss", "9672233956")
|
||||
.execute();
|
||||
log.info("生成用户信息接口响应:" + httpResponse);
|
||||
if (httpResponse.isOk() && StrUtil.isNotEmpty(httpResponse.body())) {
|
||||
return JSONObject.parseObject(httpResponse.body());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ai.da.common.interceptor;
|
||||
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
/**
|
||||
* 从 Gateway 转发的请求头中读取已鉴权的用户身份,写入 UserContext。
|
||||
* <p>
|
||||
* Gateway 验证 JWT 后将 X-User-Id 和 X-User-Info 写入请求头,
|
||||
* 此拦截器负责将信息填充到 ThreadLocal,供业务代码使用。
|
||||
* 不需要 Gateway 鉴权路径(如 login、静态资源)不会有这两个头,
|
||||
* 此时 UserContext 保持为空,业务代码应自行处理。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class UserContextInterceptor implements HandlerInterceptor {
|
||||
|
||||
private static final String USER_ID_HEADER = "X-User-Id";
|
||||
private static final String USER_INFO_HEADER = "X-User-Info";
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
|
||||
String userInfoJson = request.getHeader(USER_INFO_HEADER);
|
||||
if (userInfoJson != null && !userInfoJson.isBlank()) {
|
||||
try {
|
||||
AuthPrincipalVo principal = objectMapper.readValue(userInfoJson, AuthPrincipalVo.class);
|
||||
UserContext.setUserHolder(principal);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to parse X-User-Info header: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
|
||||
Object handler, Exception ex) {
|
||||
UserContext.delete();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package com.ai.da.common.security;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.response.ResultEnum;
|
||||
import com.ai.da.common.utils.JSONResponseUtils;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @ClassName UserAuthAccessDeniedHandler
|
||||
* @Description 无权限处理类
|
||||
* @Author dwjian
|
||||
* @Date 2020/7/9 20:30
|
||||
*/
|
||||
@Component
|
||||
public class UserAuthAccessDeniedHandler implements AccessDeniedHandler {
|
||||
@Override
|
||||
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException exception) throws IOException {
|
||||
Response<String> response = Response.error(ResultEnum.NO_PERMISSION.getCode(), ResultEnum.NO_PERMISSION.getMsg());
|
||||
JSONResponseUtils.build(httpServletResponse, response);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.ai.da.common.security;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.response.ResultEnum;
|
||||
import com.ai.da.common.utils.JSONResponseUtils;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @ClassName UserAuthenticationEntryPointHandler
|
||||
* @Description 未登录处理类
|
||||
* @Author dwjian
|
||||
* @Date 2020/7/9 20:13
|
||||
*/
|
||||
@Component
|
||||
public class UserAuthenticationEntryPointHandler implements AuthenticationEntryPoint {
|
||||
@Override
|
||||
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
|
||||
Response<String> response = Response.error(ResultEnum.NO_LOGIN.getCode(), ResultEnum.NO_LOGIN.getMsg());
|
||||
httpServletResponse.setStatus(401);
|
||||
JSONResponseUtils.build(httpServletResponse, response);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.ai.da.common.security;
|
||||
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: 认证管理器
|
||||
* @create: 2020-07-10 15:58
|
||||
**/
|
||||
@Component
|
||||
public class UserAuthenticationManager implements AuthenticationManager {
|
||||
|
||||
@Resource
|
||||
private UserAuthenticationProvider userAuthenticationProvider;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
return userAuthenticationProvider.authenticate(authentication);
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package com.ai.da.common.security;
|
||||
|
||||
import com.ai.da.common.config.RsaProperties;
|
||||
import com.ai.da.common.utils.RsaDecryptUtils;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: 登录校验处理类
|
||||
* @create: 2020-07-09 14:39
|
||||
**/
|
||||
@Component
|
||||
public class UserAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String userName = (String) authentication.getPrincipal();
|
||||
String password = (String) authentication.getCredentials();
|
||||
try {
|
||||
password = RsaDecryptUtils.decrypt(password, RsaProperties.privateKey);
|
||||
} catch (Exception e) {
|
||||
throw new BadCredentialsException("用户名或密码错误");
|
||||
}
|
||||
// User user = userService.getByUsername(userName);
|
||||
// if (user == null) {
|
||||
// throw new UsernameNotFoundException("用户名或密码错误");
|
||||
// }
|
||||
// //账号已冻结
|
||||
// if(user.getStatus() == 1){
|
||||
// throw new LockedException("账号已冻结");
|
||||
// }
|
||||
// if(!passwordEncoder.matches(password, user.getPassword())){
|
||||
// throw new BadCredentialsException("用户名或密码错误");
|
||||
// }
|
||||
// //超级管理员
|
||||
// Set<SimpleGrantedAuthority> authorities = new HashSet<>();
|
||||
// if(user.getIsAdmin()) {
|
||||
// authorities.add(new SimpleGrantedAuthority("admin"));
|
||||
// return new UsernamePasswordAuthenticationToken(user, password, authorities);
|
||||
// }else {
|
||||
// List<RoleMenuDto> userMenus = menuService.getRoleMenusByUserId(user.getId(), null);
|
||||
// if(CollUtil.isNotEmpty(userMenus)){
|
||||
// authorities.addAll(userMenus.stream().map(RoleMenuDto::getPermission).filter(StringUtils::isNotEmpty).map(SimpleGrantedAuthority::new).collect(Collectors.toSet()));
|
||||
// }
|
||||
// return new UsernamePasswordAuthenticationToken(user, password, authorities);
|
||||
// }
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> aClass) {
|
||||
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package com.ai.da.common.security;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.response.ResultEnum;
|
||||
import com.ai.da.common.utils.JSONResponseUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.*;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @ClassName UserLoginFailureHandler
|
||||
* @Description 登录失败处理类
|
||||
* @Author dwjian
|
||||
* @Date 2020/7/9 20:17
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserLoginFailureHandler implements AuthenticationFailureHandler {
|
||||
@Override
|
||||
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
|
||||
Response<String> response;
|
||||
if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) {
|
||||
response = Response.fail(e.getMessage());
|
||||
} else if (e instanceof LockedException) {
|
||||
response = Response.fail(ResultEnum.ACCOUNT_LOCK);
|
||||
} else if (e instanceof CredentialsExpiredException) {
|
||||
response = Response.fail("证书过期,请联系管理员!");
|
||||
} else if (e instanceof AccountExpiredException) {
|
||||
response = Response.fail("账户过期,请联系管理员!");
|
||||
} else if (e instanceof DisabledException) {
|
||||
response = Response.fail("账户被禁用,请联系管理员!");
|
||||
} else if (e instanceof AuthenticationServiceException) {
|
||||
response = Response.fail(e.getMessage());
|
||||
} else {
|
||||
log.error("登录失败:", e);
|
||||
response = Response.fail("登录失败!");
|
||||
}
|
||||
JSONResponseUtils.build(httpServletResponse, response);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package com.ai.da.common.security;
|
||||
|
||||
import com.ai.da.common.response.ResultEnum;
|
||||
import com.ai.da.common.utils.JSONResponseUtils;
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.security.jwt.JWTTokenHelper;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: 登录成功处理类
|
||||
* @create: 2020-07-09 14:58
|
||||
**/
|
||||
@Component
|
||||
public class UserLoginSuccessHandler implements AuthenticationSuccessHandler {
|
||||
|
||||
@Resource
|
||||
private JWTTokenHelper jwtTokenHelper;
|
||||
|
||||
@Override
|
||||
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
|
||||
// User user = (User) authentication.getPrincipal();
|
||||
// AuthPrincipalVo principal = new AuthPrincipalVo();
|
||||
// BeanUtils.copyProperties(user, principal);
|
||||
// // 获取用户角色
|
||||
// List<UserRoleDto> userRoles = roleService.getUserRoles(user.getId(), 0);
|
||||
// principal.setRoles(userRoles);
|
||||
// // 获取角色部门
|
||||
// if(CollUtil.isNotEmpty(userRoles)){
|
||||
// principal.setDepts(deptService.getDeptByRoleIds(userRoles.stream().map(UserRoleDto::getRoleId).collect(Collectors.toList())));
|
||||
// }
|
||||
// // 用户角色权限
|
||||
// List<String> authorities = new ArrayList<>(authentication.getAuthorities()).stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList());
|
||||
// principal.setAuthorities(authorities);
|
||||
// AuthVo authVo = new AuthVo();
|
||||
// authVo.setAuthorities(authorities);
|
||||
// authVo.setToken(jwtTokenHelper.createToken(principal));
|
||||
// authVo.setPrincipal(principal);
|
||||
// user.setLastLoginTime(new Date());
|
||||
// userService.updateById(user);
|
||||
JSONResponseUtils.build(response, Response.success(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMsg(), null));
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
package com.ai.da.common.security;
|
||||
|
||||
import org.springframework.security.access.PermissionEvaluator;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @ClassName UserPermissionEvaluator
|
||||
* @Description 权限校验处理器
|
||||
* @Author dwjian
|
||||
* @Date 2020/7/12 10:16
|
||||
*/
|
||||
@Component
|
||||
public class UserPermissionEvaluator implements PermissionEvaluator {
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
|
||||
System.out.println(authentication);
|
||||
System.out.println(targetUrl);
|
||||
System.out.println(permission);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) {
|
||||
System.out.println(authentication);
|
||||
System.out.println(targetId);
|
||||
System.out.println(targetType);
|
||||
System.out.println(permission);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
package com.ai.da.common.security.config;
|
||||
|
||||
import com.ai.da.common.security.*;
|
||||
import com.ai.da.common.security.filter.AuthenticationFilter;
|
||||
import com.ai.da.common.security.filter.UserAuthenticationProcessingFilter;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity(prePostEnabled = true)
|
||||
@EnableConfigurationProperties(SecurityProperties.class)
|
||||
public class SecurityConfig {
|
||||
|
||||
@Resource
|
||||
private SecurityProperties securityProperties;
|
||||
@Resource
|
||||
private UserLoginSuccessHandler userLoginSuccessHandler;
|
||||
@Resource
|
||||
private UserLoginFailureHandler userLoginFailureHandler;
|
||||
@Resource
|
||||
private UserAuthAccessDeniedHandler userAuthAccessDeniedHandler;
|
||||
@Resource
|
||||
private UserAuthenticationEntryPointHandler userAuthenticationEntryPointHandler;
|
||||
@Resource
|
||||
private UserAuthenticationManager userAuthenticationManager;
|
||||
@Resource
|
||||
private UserAuthenticationProcessingFilter userAuthenticationProcessingFilter;
|
||||
|
||||
/**
|
||||
* 不通过注入spring管理 让Security来管理 这样自定义的Filter就不会走,.permitAll()才能起作用
|
||||
*/
|
||||
@Resource
|
||||
private AuthenticationFilter authenticationFilter;
|
||||
@Resource
|
||||
private UserPermissionEvaluator userPermissionEvaluator;
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager() throws Exception {
|
||||
return this.userAuthenticationManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
|
||||
httpSecurity
|
||||
.cors(Customizer.withDefaults())
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(
|
||||
new AntPathRequestMatcher("/doc.html"),
|
||||
new AntPathRequestMatcher("/swagger-ui.html"),
|
||||
new AntPathRequestMatcher("/swagger-ui/**"),
|
||||
new AntPathRequestMatcher("/swagger-resources/**"),
|
||||
new AntPathRequestMatcher("/v2/api-docs"),
|
||||
new AntPathRequestMatcher("/v3/api-docs/**"),
|
||||
new AntPathRequestMatcher("/webjars/**")
|
||||
).permitAll()
|
||||
.requestMatchers(securityProperties.getIgnorePaths()).permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.headers(headers -> headers
|
||||
.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable)
|
||||
.cacheControl(cache -> cache.disable())
|
||||
)
|
||||
.exceptionHandling(exception -> exception
|
||||
.authenticationEntryPoint(userAuthenticationEntryPointHandler)
|
||||
.accessDeniedHandler(userAuthAccessDeniedHandler)
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginProcessingUrl(securityProperties.getAuthApi())
|
||||
.successHandler(userLoginSuccessHandler)
|
||||
.failureHandler(userLoginFailureHandler)
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
);
|
||||
|
||||
//自定义过滤器在登录时认证用户名、密码
|
||||
httpSecurity.addFilterAt(userAuthenticationProcessingFilter, UsernamePasswordAuthenticationFilter.class)
|
||||
.addFilterBefore(authenticationFilter, BasicAuthenticationFilter.class);
|
||||
|
||||
return httpSecurity.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler() {
|
||||
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
|
||||
handler.setPermissionEvaluator(userPermissionEvaluator);
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package com.ai.da.common.security.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: JWT配置类
|
||||
* @create: 2020-07-09 09:38
|
||||
**/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "spring.security")
|
||||
public class SecurityProperties {
|
||||
|
||||
private String jwtSecret;
|
||||
|
||||
private String jwtTokenHeader;
|
||||
|
||||
private String jwtTokenPrefix;
|
||||
|
||||
private long jwtExpiration;
|
||||
|
||||
private String[] ignorePaths;
|
||||
|
||||
private String authApi;
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package com.ai.da.common.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ai.da.common.config.exception.TokenMissingOrExpiredException;
|
||||
import com.ai.da.common.context.UserContext;
|
||||
import com.ai.da.common.security.config.SecurityProperties;
|
||||
import com.ai.da.common.security.jwt.JWTTokenHelper;
|
||||
import com.ai.da.common.utils.LocalCacheUtils;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
import com.ai.da.common.utils.MultiReadHttpServletRequest;
|
||||
import com.ai.da.common.utils.MultiReadHttpServletResponse;
|
||||
import com.ai.da.common.utils.RequestInfoUtil;
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.StopWatch;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: 认证拦截器
|
||||
* @create: 2020-07-10 16:50
|
||||
**/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class AuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
@Resource
|
||||
private JWTTokenHelper jwtTokenHelper;
|
||||
@Resource
|
||||
private SecurityProperties properties;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
private static final List<String> FILTER_URL =
|
||||
Arrays.asList("/favicon.ico", "/doc.html", "/swagger-ui.html",
|
||||
"/swagger-resources", "/swagger-resources/", "/swagger-resources/configuration/ui", "/swagger-resources/configuration/security",
|
||||
"/webjars/", "/v2/api-docs", "/v3/api-docs", "/v3/api-docs/swagger-config",
|
||||
"/api/account/login", "/api/account/preLogin", "api/account/sendEmail","api/account/noLoginRequired",
|
||||
"/api/account/resetPwd",
|
||||
"/api/python/saveGeneratePicture", "/api/python/getLibraryByUserId",
|
||||
"/api/third/party/addUser","/api/third/party/addTrialUser", "/api/third/party/editUser", "/api/element/initDefaultSysFile",
|
||||
"/api/third/party/addNoLoginRequiredNew","/api/third/party/deleteNoLoginRequiredNew","/api/third/party/updateNoLoginRequiredNew",
|
||||
"/api/third/party/existNoLoginRequired","/api/third/party/getRedirectUrl",
|
||||
"/api/python/flush","/api/account/healthy","/api/ali-pay/trade/notify","/api/paypal/ipn/back","/api/alipay-hk/trade/notify",
|
||||
"/api/portfolio/page", "/api/portfolio/detail", "/api/portfolio/commentPage", "/api/portfolio/viewsIncrease",
|
||||
"/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",
|
||||
//GlobalAwardController
|
||||
"/api/global-award"
|
||||
);
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest httpServletRequest, @NonNull HttpServletResponse httpServletResponse, @NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
String requestURI = httpServletRequest.getRequestURI();
|
||||
|
||||
if (calculateUrl(requestURI)/* || hasAuthorizationToken(httpServletRequest)*/) {
|
||||
StopWatch stopWatch = new StopWatch();
|
||||
HttpServletRequest wrappedRequest = httpServletRequest;
|
||||
HttpServletResponse wrappedResponse = httpServletResponse;
|
||||
try {
|
||||
stopWatch.start();
|
||||
if ((httpServletRequest.getContentType() == null && httpServletRequest.getContentLength() > 0) || (httpServletRequest.getContentType() != null && !httpServletRequest.getContentType().contains("application/json"))) {
|
||||
extracted(wrappedRequest);
|
||||
filterChain.doFilter(wrappedRequest, wrappedResponse);
|
||||
} else {
|
||||
wrappedRequest = new MultiReadHttpServletRequest(httpServletRequest);
|
||||
wrappedResponse = new MultiReadHttpServletResponse(httpServletResponse);
|
||||
extracted(wrappedRequest);
|
||||
// excel导出使用原始response,不对响应做包装
|
||||
if (requestURI.equals("/api/account/exportAccountsToExcel")) {
|
||||
filterChain.doFilter(httpServletRequest, httpServletResponse); // 不包装
|
||||
} else {
|
||||
filterChain.doFilter(wrappedRequest, wrappedResponse);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
SecurityContextHolder.clearContext();
|
||||
throw e;
|
||||
} finally {
|
||||
stopWatch.stop();
|
||||
}
|
||||
} else {
|
||||
//先清空当前线程变量,防止上一个线程遗留
|
||||
UserContext.delete();
|
||||
filterChain.doFilter(httpServletRequest, httpServletResponse);
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean calculateUrl(String requestURI) {
|
||||
String filterUrl = FILTER_URL.stream().filter(url -> requestURI.contains(url)).findFirst().orElse(null);
|
||||
return null == filterUrl ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
|
||||
private boolean hasAuthorizationToken(HttpServletRequest request) {
|
||||
String authorizationHeader = request.getHeader("Authorization");
|
||||
return authorizationHeader != null && authorizationHeader.startsWith("Bearer");
|
||||
}
|
||||
|
||||
private void extracted(HttpServletRequest request) {
|
||||
String jwtToken = request.getHeader(properties.getJwtTokenHeader());
|
||||
// log.debug("后台检查令牌:{}", jwtToken);
|
||||
|
||||
if (StrUtil.isBlank(jwtToken)) {
|
||||
String ipAddress = RequestInfoUtil.getIpAddress(request);
|
||||
log.info("本次请求的ip为 : " + ipAddress);
|
||||
// throw new RuntimeException("请传入token!");
|
||||
throw new TokenMissingOrExpiredException("请传入token!");
|
||||
}
|
||||
if(jwtToken.equals("Bearer-eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIyIiwic3ViIjoie1wiaWRcIjoyLFwidXNlcm5hbWVcIjpcImxpcnNcIn0iLCJpYXQiOjE2NjU3NDEwODcsImlzcyI6IkRXSiIsImF1dGhvcml0aWVzIjoiW10iLCJleHAiOjE2NzQzODEwODd9.ShM9R_NNFD7oo1OvxrEgg7PFeWinOuAKkuInUCMQupp66s64Hhv8tN0Wwr83nIN4rHPqtn95wmd4msWcvaFYJA")){
|
||||
//写死 暂时放行
|
||||
return;
|
||||
}
|
||||
// 检查token
|
||||
boolean validate = jwtTokenHelper.validateToken(jwtToken);
|
||||
if (validate) {
|
||||
AuthPrincipalVo principal = jwtTokenHelper.parserToUser(jwtToken);
|
||||
if (principal == null) {
|
||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
||||
throw new TokenMissingOrExpiredException("TOKEN已过期,请重新登录!(token without userInfo)");
|
||||
}
|
||||
//先清空当前线程变量,防止上一个线程遗留
|
||||
UserContext.delete();
|
||||
//存取用户信息到缓存
|
||||
UserContext.setUserHolder(principal);
|
||||
// 校验 token:先查本地缓存,再查 Redis,保证服务重启后仍然有效
|
||||
String userIdStr = String.valueOf(principal.getId());
|
||||
String cacheToken = LocalCacheUtils.getTokenCache(userIdStr);
|
||||
|
||||
if (StringUtils.isEmpty(cacheToken)) {
|
||||
// 本地缓存为空时,尝试从 Redis 读取
|
||||
cacheToken = redisUtil.getLoginToken(principal.getId());
|
||||
if (StringUtils.isEmpty(cacheToken)) {
|
||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
||||
throw new TokenMissingOrExpiredException("TOKEN已过期,请重新登录!(cache & redis empty)");
|
||||
}
|
||||
// 将 Redis 中的 token 回填到本地缓存,减少后续 Redis 访问
|
||||
LocalCacheUtils.setTokenCache(userIdStr, cacheToken);
|
||||
}
|
||||
if(!cacheToken.equals(jwtToken) ){
|
||||
// throw new RuntimeException("TOKEN已过期,请重新登录!");
|
||||
throw new TokenMissingOrExpiredException("TOKEN已过期,请重新登录!(token not match local cache)");
|
||||
}
|
||||
// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(null, null);
|
||||
// SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.ai.da.common.security.filter;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.ai.da.common.security.UserLoginSuccessHandler;
|
||||
import com.ai.da.common.security.config.SecurityProperties;
|
||||
import com.ai.da.common.utils.RedisCacheUtils;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.ai.da.common.security.UserAuthenticationManager;
|
||||
import com.ai.da.common.security.UserLoginFailureHandler;
|
||||
import com.ai.da.common.utils.MultiReadHttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: 用户认证过滤器
|
||||
* @create: 2020-07-10 15:58
|
||||
**/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class UserAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
|
||||
|
||||
/**
|
||||
* @param securityProperties 配置(从配置中读取登录url)
|
||||
* @param authenticationManager 认证管理器
|
||||
* @param adminAuthenticationSuccessHandler 认证成功处理器
|
||||
* @param adminAuthenticationFailureHandler 认证失败处理器
|
||||
*/
|
||||
public UserAuthenticationProcessingFilter(SecurityProperties securityProperties, UserAuthenticationManager authenticationManager, UserLoginSuccessHandler adminAuthenticationSuccessHandler, UserLoginFailureHandler adminAuthenticationFailureHandler) {
|
||||
super(new AntPathRequestMatcher(securityProperties.getAuthApi(), HttpMethod.POST.name()));
|
||||
this.setAuthenticationManager(authenticationManager);
|
||||
this.setAuthenticationSuccessHandler(adminAuthenticationSuccessHandler);
|
||||
this.setAuthenticationFailureHandler(adminAuthenticationFailureHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
|
||||
if (request.getContentType() == null || !request.getContentType().contains("application/json")) {
|
||||
throw new AuthenticationServiceException("请求头类型不支持: " + request.getContentType());
|
||||
}
|
||||
UsernamePasswordAuthenticationToken authRequest;
|
||||
try {
|
||||
MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(request);
|
||||
// 将前端传递的数据转换成jsonBean数据格式
|
||||
JSONObject jsonObject = JSONObject.parseObject(wrappedRequest.getBodyJsonStrByJson(wrappedRequest));
|
||||
String code = jsonObject.getString("code");
|
||||
String uuid = jsonObject.getString("uuid");
|
||||
if (StrUtil.isEmpty(code) || StrUtil.isEmpty(uuid) || !code.equals(RedisCacheUtils.get("code-key-" + uuid, String.class))) {
|
||||
throw new AuthenticationServiceException("验证码错误");
|
||||
}
|
||||
RedisCacheUtils.delete("code-key-" + uuid);
|
||||
authRequest = new UsernamePasswordAuthenticationToken(jsonObject.get("username"), jsonObject.get("password"), null);
|
||||
authRequest.setDetails(authenticationDetailsSource.buildDetails(wrappedRequest));
|
||||
} catch (Exception e) {
|
||||
throw new AuthenticationServiceException(e.getMessage());
|
||||
}
|
||||
return this.getAuthenticationManager().authenticate(authRequest);
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package com.ai.da.common.security.jwt;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.common.security.config.SecurityProperties;
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @author: dangweijian
|
||||
* @description: JWT工具
|
||||
* @create: 2020-07-09 09:27
|
||||
**/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class JWTTokenHelper {
|
||||
|
||||
@Resource
|
||||
private SecurityProperties securityProperties;
|
||||
|
||||
private static final String ISSUER = "DWJ";
|
||||
private static final String AUTHORITIES = "authorities";
|
||||
private static final String CHANGE_MAILBOX = "changeMailbox";
|
||||
|
||||
public String createToken(AuthPrincipalVo principal) {
|
||||
SecretKey key = buildSigningKey();
|
||||
String token = Jwts.builder()
|
||||
.id(String.valueOf(principal.getId()))
|
||||
.subject(JSONObject.toJSONString(principal))
|
||||
.issuedAt(new Date())
|
||||
.issuer(ISSUER)
|
||||
.claim(AUTHORITIES, JSON.toJSONString(new ArrayList<>()))//自定义属性 权限
|
||||
.expiration(new Date(System.currentTimeMillis() + securityProperties.getJwtExpiration()))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
token = securityProperties.getJwtTokenPrefix() + token;
|
||||
return token;
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
Claims claims = parser(token);
|
||||
if (MapUtil.isEmpty(claims)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public AuthPrincipalVo parserToUser(String token) {
|
||||
String subject = parser(token).getSubject();
|
||||
if (StrUtil.isNotEmpty(subject)) {
|
||||
return JSONObject.parseObject(subject, AuthPrincipalVo.class);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Claims parser(String token) {
|
||||
token = token.replaceAll(securityProperties.getJwtTokenPrefix(), "");
|
||||
SecretKey key = buildSigningKey();
|
||||
return Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
public String createToken(Long userId, String userEmail){
|
||||
SecretKey key = buildSigningKey();
|
||||
String token = Jwts.builder()
|
||||
.id(String.valueOf(userId))
|
||||
.subject(userEmail + "_" + userId)
|
||||
.issuedAt(new Date())
|
||||
.issuer(ISSUER)
|
||||
.claim(CHANGE_MAILBOX, JSON.toJSONString(new ArrayList<>()))//自定义属性 权限
|
||||
.expiration(new Date(System.currentTimeMillis() + CommonConstant.CHANGE_MAILBOX_LINK_VALIDITY))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
return token;
|
||||
}
|
||||
|
||||
public String parseToEmailAndId(String token) {
|
||||
return parser(token).getSubject();
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 要求 HMAC-SHA 的密钥至少 256 bit,这里统一扩展/哈希密钥长度避免 WeakKeyException。
|
||||
*/
|
||||
private SecretKey buildSigningKey() {
|
||||
byte[] raw = securityProperties.getJwtSecret().getBytes(StandardCharsets.UTF_8);
|
||||
if (raw.length < 32) {
|
||||
raw = DigestUtil.sha256(raw);
|
||||
}
|
||||
return Keys.hmacShaKeyFor(raw);
|
||||
}
|
||||
}
|
||||
@@ -28,13 +28,13 @@ public class AccountTask {
|
||||
* 每个月月初只刷新教育子账号的积分
|
||||
*/
|
||||
// @Scheduled(cron = "0 25 14 * * ?")
|
||||
@Scheduled(cron = "0 0 0 1 * ?")
|
||||
// @Scheduled(cron = "0 0 0 1 * ?")
|
||||
public void refreshCreditsMonthly() {
|
||||
log.info("每月1号0点 重置教育版子账号为默认积分");
|
||||
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();
|
||||
@@ -54,7 +54,7 @@ public class AccountTask {
|
||||
}*/
|
||||
|
||||
// 每天检测正式用户到期情况,每天凌晨0点执行
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
// @Scheduled(cron = "0 0 0 * * ?")
|
||||
public void paidUserToVisitor() {
|
||||
// 1、查询当前已过期正式用户或试用用户
|
||||
List<Account> accountList = accountService.getExpiredUserBySystemUser(1);
|
||||
@@ -77,7 +77,7 @@ public class AccountTask {
|
||||
accountService.registerUserToVisitor();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0 0 1 * ?")
|
||||
// @Scheduled(cron = "0 0 0 1 * ?")
|
||||
// 每月初刷新所有用户用户名剩余修改次数
|
||||
public void resetUsernameModifyTimes(){
|
||||
log.info("重置所有用户的用户名修改次数");
|
||||
@@ -85,17 +85,17 @@ public class AccountTask {
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 35 14 * * ?")
|
||||
@Scheduled(cron = "0 5 0 * * ?")
|
||||
// @Scheduled(cron = "0 5 0 * * ?")
|
||||
public void checkEduAdminExpireStatus() {
|
||||
accountService.checkEduAdminExpireStatus();
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 5 0 * * ?")
|
||||
// @Scheduled(cron = "0 5 0 * * ?")
|
||||
public void activeSubscriptionPlan() {
|
||||
subscriptionPlanService.activeSubscriptionPlan(null);
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
public void expireSubscription() {
|
||||
subscriptionPlanService.expireSubscription();
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ public class GenerateTask {
|
||||
* 故这里通过定时任务做补偿
|
||||
* flux五分钟查询一次,万相1小时查询一次
|
||||
*/
|
||||
@Scheduled(cron = "0 */4 * * * ?")
|
||||
// @Scheduled(cron = "0 */4 * * * ?")
|
||||
public void fluxCompensationMechanism(){
|
||||
// 1、查所有 任务还没成功、还没失败,正在等待或者执行中的任务id有哪些
|
||||
// (由于获取结果的polling_url在redis中只存一天,大部分结果超过一天之后就无法再找到任务,小部分可以通过公共路径查到结果)
|
||||
@@ -98,7 +98,7 @@ public class GenerateTask {
|
||||
}
|
||||
|
||||
// 万相 -> pose transformation 补偿 当前任务执行完后,5分钟再执行一次(不会出现任务重叠的情况)
|
||||
@Scheduled(fixedDelay = 5 * 60 * 1000)
|
||||
// @Scheduled(fixedDelay = 5 * 60 * 1000)
|
||||
public void wxCompensationMechanism(){
|
||||
List<APIGenerate> apiGenerates = apiGenerateService.getPendingTaskByStatus("wx");
|
||||
if (apiGenerates != null && !apiGenerates.isEmpty()){
|
||||
|
||||
@@ -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,19 +97,19 @@ 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();
|
||||
}
|
||||
|
||||
// 定时同步(每分钟一次)
|
||||
@Scheduled(fixedRate = 60000)
|
||||
// @Scheduled(fixedRate = 60000)
|
||||
public void syncLinkViewCountToDB(){
|
||||
affiliateService.syncLinkViewCountToDB();
|
||||
}
|
||||
|
||||
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
||||
// @Scheduled(cron = "0 0 8 28-31 * ?")
|
||||
public void commissionSummaryReminder(){
|
||||
// 每个月末的最后一天的早上八点执行
|
||||
LocalDate today = LocalDate.now();
|
||||
@@ -120,7 +120,7 @@ public class PaymentTask {
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
// @Scheduled(cron = "0 */5 * * * *") // Run every 5 minutes
|
||||
public void calcCouponsCommission(){
|
||||
// log.info("优惠券佣金计算定时器");
|
||||
affiliateService.calcCouponsCommission();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,178 +1,171 @@
|
||||
package com.ai.da.common.utils;
|
||||
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.model.dto.BasicEmailParamDTO;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.sun.mail.smtp.SMTPTransport;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.InputStreamSource;
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.*;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MailUtil {
|
||||
|
||||
@Resource
|
||||
private JavaMailSender javaMailSender;
|
||||
|
||||
@Resource
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
/**
|
||||
* 发送邮件 - 默认发件人
|
||||
*
|
||||
* @param basicEmailParamDTO 发送邮件所需参数
|
||||
* @param inputStreamSource 附件(如果有)
|
||||
*/
|
||||
public int sendMail(BasicEmailParamDTO basicEmailParamDTO, String fileName, InputStreamSource inputStreamSource) throws MessagingException {
|
||||
MimeMessage mimeMessage = createSimpleMail(basicEmailParamDTO, fileName, inputStreamSource);
|
||||
// 提取配置
|
||||
String host;
|
||||
String username;
|
||||
String password;
|
||||
if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getServiceAddress())) {
|
||||
host = ((JavaMailSenderImpl) javaMailSender).getHost();
|
||||
} else {
|
||||
host = basicEmailParamDTO.getServiceAddress();
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getSenderUser())) {
|
||||
username = ((JavaMailSenderImpl) javaMailSender).getUsername();
|
||||
} else {
|
||||
username = basicEmailParamDTO.getSenderUser();
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getServiceAddress())) {
|
||||
password = ((JavaMailSenderImpl) javaMailSender).getPassword();
|
||||
} else {
|
||||
password = basicEmailParamDTO.getPassword();
|
||||
}
|
||||
return sendMail(mimeMessage, host, username, password);
|
||||
}
|
||||
|
||||
private int sendMail(MimeMessage mimeMessage, String host, String username, String password) throws MessagingException {
|
||||
SMTPTransport transport = null;
|
||||
try {
|
||||
// 获取 SMTPTransport
|
||||
transport = (SMTPTransport) mimeMessage.getSession().getTransport("smtp");
|
||||
// 连接到 SMTP 服务器
|
||||
transport.connect(host, username, password);
|
||||
// 发送邮件
|
||||
transport.sendMessage(mimeMessage, mimeMessage.getAllRecipients());
|
||||
// 获取 SMTP 服务器的响应
|
||||
String lastServerResponse = transport.getLastServerResponse();
|
||||
int lastReturnCode = transport.getLastReturnCode();
|
||||
|
||||
log.info("SMTP 状态码: {}, SMTP 服务器响应: {}", lastReturnCode, lastServerResponse);
|
||||
return lastReturnCode;
|
||||
} catch (MailException | MessagingException e) {
|
||||
// 记录日志或执行其他补偿逻辑
|
||||
log.info("邮件发送失败:{}", e.getMessage());
|
||||
} finally {
|
||||
// 关闭连接
|
||||
assert transport != null;
|
||||
transport.close();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一封邮件
|
||||
*
|
||||
* @param basicEmailParamDTO 创建邮件需要的参数
|
||||
* @param inputStreamSource 附件(如果有)
|
||||
* @return 一封邮件
|
||||
*/
|
||||
private MimeMessage createSimpleMail(BasicEmailParamDTO basicEmailParamDTO, String fileName, InputStreamSource inputStreamSource) throws MessagingException {
|
||||
// 创建邮件对象
|
||||
MimeMessage message = javaMailSender.createMimeMessage();
|
||||
// 使用 MimeMessageHelper 简化邮件内容和附件的设置
|
||||
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(message, true);
|
||||
// 设置发件人
|
||||
mimeMessageHelper.setFrom(new InternetAddress(basicEmailParamDTO.getSenderUserMail()));
|
||||
// 设置收件人
|
||||
mimeMessageHelper.setTo(basicEmailParamDTO.getMailTo());
|
||||
// 设置抄送人
|
||||
if (basicEmailParamDTO.getCc() != null && basicEmailParamDTO.getCc().length > 0) {
|
||||
mimeMessageHelper.setCc(basicEmailParamDTO.getCc());
|
||||
}
|
||||
// 设置暗送人
|
||||
if (basicEmailParamDTO.getBcc() != null && basicEmailParamDTO.getBcc().length > 0) {
|
||||
mimeMessageHelper.setBcc(basicEmailParamDTO.getBcc());
|
||||
}
|
||||
// 设置邮件主题
|
||||
mimeMessageHelper.setSubject(basicEmailParamDTO.getSubject());
|
||||
// 设置邮件内容(HTML 格式)
|
||||
mimeMessageHelper.setText(basicEmailParamDTO.getContent(), true);
|
||||
// 设置附件
|
||||
if (inputStreamSource != null) {
|
||||
mimeMessageHelper.addAttachment(fileName, inputStreamSource);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置实体参数
|
||||
*
|
||||
* @param mailTo 接收邮件的邮箱地址
|
||||
* @param jsonObject 模板中变量的值
|
||||
* @return 返回一个MailEntity
|
||||
* @throws AddressException 邮箱地址值异常
|
||||
*/
|
||||
public BasicEmailParamDTO setBasicEmailParams(List<String> mailTo, JSONObject jsonObject, String templatePath, String title) throws AddressException {
|
||||
BasicEmailParamDTO basicEmailParamDTO = new BasicEmailParamDTO();
|
||||
// basicEmailParamDTO.setSenderUserMail("info@aida.com.hk");
|
||||
basicEmailParamDTO.setSenderUserMail(CommonConstant.senderEmail);
|
||||
basicEmailParamDTO.setMailTo(getInternetAddressList(mailTo));
|
||||
basicEmailParamDTO.setSubject(title);
|
||||
// todo 邮件模板不存在的报错与重试机制
|
||||
basicEmailParamDTO.setContent(setContent(jsonObject, templatePath));
|
||||
return basicEmailParamDTO;
|
||||
}
|
||||
|
||||
public BasicEmailParamDTO setBasicEmailParams(List<String> mailTo, String title) throws AddressException {
|
||||
BasicEmailParamDTO basicEmailParamDTO = new BasicEmailParamDTO();
|
||||
basicEmailParamDTO.setSenderUserMail("info@aida.com.hk");
|
||||
basicEmailParamDTO.setMailTo(getInternetAddressList(mailTo));
|
||||
basicEmailParamDTO.setSubject(title);
|
||||
return basicEmailParamDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将地址转换为InternetAddress类型
|
||||
*
|
||||
* @param addressList 普通的地址字符串列表
|
||||
* @return InternetAddress类型的地址列表
|
||||
* @throws AddressException 地址异常
|
||||
*/
|
||||
public InternetAddress[] getInternetAddressList(List<String> addressList) throws AddressException {
|
||||
InternetAddress[] toAddress = new InternetAddress[addressList.size()];
|
||||
for (String address : addressList) {
|
||||
toAddress[addressList.indexOf(address)] = new InternetAddress(address);
|
||||
}
|
||||
return toAddress;
|
||||
}
|
||||
|
||||
public String setContent(JSONObject jsonObject, String templatePath) {
|
||||
Context context = new Context();
|
||||
if (Objects.nonNull(jsonObject)) {
|
||||
for (String key : jsonObject.keySet()) {
|
||||
context.setVariable(key, jsonObject.get(key));
|
||||
}
|
||||
}
|
||||
return templateEngine.process(templatePath, context);
|
||||
}
|
||||
|
||||
}
|
||||
package com.ai.da.common.utils;
|
||||
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.model.dto.BasicEmailParamDTO;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.io.InputStreamSource;
|
||||
import org.springframework.mail.MailException;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.*;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class MailUtil {
|
||||
|
||||
@Resource
|
||||
private JavaMailSender javaMailSender;
|
||||
|
||||
@Resource
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
/**
|
||||
* 发送邮件 - 默认发件人
|
||||
*
|
||||
* @param basicEmailParamDTO 发送邮件所需参数
|
||||
* @param fileName 附件名(如果有)
|
||||
* @param inputStreamSource 附件(如果有)
|
||||
*/
|
||||
public int sendMail(BasicEmailParamDTO basicEmailParamDTO, String fileName, InputStreamSource inputStreamSource) throws MessagingException {
|
||||
MimeMessage mimeMessage = createSimpleMail(basicEmailParamDTO, fileName, inputStreamSource);
|
||||
// 提取配置
|
||||
String host;
|
||||
String username;
|
||||
String password;
|
||||
if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getServiceAddress())) {
|
||||
host = ((JavaMailSenderImpl) javaMailSender).getHost();
|
||||
} else {
|
||||
host = basicEmailParamDTO.getServiceAddress();
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getSenderUser())) {
|
||||
username = ((JavaMailSenderImpl) javaMailSender).getUsername();
|
||||
} else {
|
||||
username = basicEmailParamDTO.getSenderUser();
|
||||
}
|
||||
if (StringUtil.isNullOrEmpty(basicEmailParamDTO.getServiceAddress())) {
|
||||
password = ((JavaMailSenderImpl) javaMailSender).getPassword();
|
||||
} else {
|
||||
password = basicEmailParamDTO.getPassword();
|
||||
}
|
||||
return sendMail(mimeMessage, host, username, password);
|
||||
}
|
||||
|
||||
private int sendMail(MimeMessage mimeMessage, String host, String username, String password) throws MessagingException {
|
||||
try {
|
||||
// 配置连接属性
|
||||
java.util.Properties props = mimeMessage.getSession().getProperties();
|
||||
props.put("mail.smtp.auth", "true");
|
||||
props.put("mail.smtp.host", host);
|
||||
props.put("mail.user", username);
|
||||
props.put("mail.password", password);
|
||||
|
||||
// 使用 JavaMailSender 发送邮件(Spring Boot 3.x 标准方式)
|
||||
javaMailSender.send(mimeMessage);
|
||||
log.info("邮件发送成功至: {}", host);
|
||||
return 1;
|
||||
} catch (MailException e) {
|
||||
log.info("邮件发送失败:{}", e.getMessage());
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一封邮件
|
||||
*
|
||||
* @param basicEmailParamDTO 创建邮件需要的参数
|
||||
* @param inputStreamSource 附件(如果有)
|
||||
* @return 一封邮件
|
||||
*/
|
||||
private MimeMessage createSimpleMail(BasicEmailParamDTO basicEmailParamDTO, String fileName, InputStreamSource inputStreamSource) throws MessagingException {
|
||||
// 创建邮件对象
|
||||
MimeMessage message = javaMailSender.createMimeMessage();
|
||||
// 使用 MimeMessageHelper 简化邮件内容和附件的设置
|
||||
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(message, true);
|
||||
// 设置发件人
|
||||
mimeMessageHelper.setFrom(new InternetAddress(basicEmailParamDTO.getSenderUserMail()));
|
||||
// 设置收件人
|
||||
mimeMessageHelper.setTo(basicEmailParamDTO.getMailTo());
|
||||
// 设置抄送人
|
||||
if (basicEmailParamDTO.getCc() != null && basicEmailParamDTO.getCc().length > 0) {
|
||||
mimeMessageHelper.setCc(basicEmailParamDTO.getCc());
|
||||
}
|
||||
// 设置暗送人
|
||||
if (basicEmailParamDTO.getBcc() != null && basicEmailParamDTO.getBcc().length > 0) {
|
||||
mimeMessageHelper.setBcc(basicEmailParamDTO.getBcc());
|
||||
}
|
||||
// 设置邮件主题
|
||||
mimeMessageHelper.setSubject(basicEmailParamDTO.getSubject());
|
||||
// 设置邮件内容(HTML 格式)
|
||||
mimeMessageHelper.setText(basicEmailParamDTO.getContent(), true);
|
||||
// 设置附件
|
||||
if (inputStreamSource != null) {
|
||||
mimeMessageHelper.addAttachment(fileName, inputStreamSource);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置实体参数
|
||||
*
|
||||
* @param mailTo 接收邮件的邮箱地址
|
||||
* @param jsonObject 模板中变量的值
|
||||
* @return 返回一个MailEntity
|
||||
* @throws AddressException 邮箱地址值异常
|
||||
*/
|
||||
public BasicEmailParamDTO setBasicEmailParams(List<String> mailTo, JSONObject jsonObject, String templatePath, String title) throws AddressException {
|
||||
BasicEmailParamDTO basicEmailParamDTO = new BasicEmailParamDTO();
|
||||
// basicEmailParamDTO.setSenderUserMail("info@aida.com.hk");
|
||||
basicEmailParamDTO.setSenderUserMail(CommonConstant.senderEmail);
|
||||
basicEmailParamDTO.setMailTo(getInternetAddressList(mailTo));
|
||||
basicEmailParamDTO.setSubject(title);
|
||||
// todo 邮件模板不存在的报错与重试机制
|
||||
basicEmailParamDTO.setContent(setContent(jsonObject, templatePath));
|
||||
return basicEmailParamDTO;
|
||||
}
|
||||
|
||||
public BasicEmailParamDTO setBasicEmailParams(List<String> mailTo, String title) throws AddressException {
|
||||
BasicEmailParamDTO basicEmailParamDTO = new BasicEmailParamDTO();
|
||||
basicEmailParamDTO.setSenderUserMail("info@aida.com.hk");
|
||||
basicEmailParamDTO.setMailTo(getInternetAddressList(mailTo));
|
||||
basicEmailParamDTO.setSubject(title);
|
||||
return basicEmailParamDTO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将地址转换为InternetAddress类型
|
||||
*
|
||||
* @param addressList 普通的地址字符串列表
|
||||
* @return InternetAddress类型的地址列表
|
||||
* @throws AddressException 地址异常
|
||||
*/
|
||||
public InternetAddress[] getInternetAddressList(List<String> addressList) throws AddressException {
|
||||
InternetAddress[] toAddress = new InternetAddress[addressList.size()];
|
||||
for (String address : addressList) {
|
||||
toAddress[addressList.indexOf(address)] = new InternetAddress(address);
|
||||
}
|
||||
return toAddress;
|
||||
}
|
||||
|
||||
public String setContent(JSONObject jsonObject, String templatePath) {
|
||||
Context context = new Context();
|
||||
if (Objects.nonNull(jsonObject)) {
|
||||
for (String key : jsonObject.keySet()) {
|
||||
context.setVariable(key, jsonObject.get(key));
|
||||
}
|
||||
}
|
||||
return templateEngine.process(templatePath, context);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@@ -41,6 +42,9 @@ public class MinioUtil {
|
||||
@Autowired
|
||||
private MinioClient minioClient;
|
||||
|
||||
@Value("${minio.endpoint}")
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* 获取MinIO客户端实例
|
||||
*/
|
||||
@@ -48,6 +52,18 @@ public class MinioUtil {
|
||||
return minioClient;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
/**
|
||||
* Redis缓存key前缀,用于Minio签名URL缓存
|
||||
*/
|
||||
private static final String REDIS_MINIO_URL_PREFIX = "minio:url:";
|
||||
|
||||
/**
|
||||
* 签名URL缓存过期时间(秒),默认1天
|
||||
*/
|
||||
private static final long URL_CACHE_EXPIRE_SECONDS = 24 * 60 * 60;
|
||||
/**
|
||||
* description: 判断bucket是否存在,不存在则创建
|
||||
*
|
||||
@@ -388,6 +404,11 @@ public class MinioUtil {
|
||||
* @return 文件的临时URL,如果出现异常则返回null
|
||||
*/
|
||||
public String getPreSignedUrl(String bucketName, String fileName, int expiry) {
|
||||
String cacheKey = REDIS_MINIO_URL_PREFIX + bucketName + "/" + fileName;
|
||||
Object cachedUrl = redisUtil.getFromString(cacheKey);
|
||||
if (cachedUrl != null) {
|
||||
return cachedUrl.toString();
|
||||
}
|
||||
try {
|
||||
|
||||
String lowerName = fileName.toLowerCase();
|
||||
@@ -415,8 +436,9 @@ public class MinioUtil {
|
||||
|
||||
builder.extraQueryParams(queryParams);
|
||||
}
|
||||
|
||||
return minioClient.getPresignedObjectUrl(builder.build());
|
||||
String presignedObjectUrl = minioClient.getPresignedObjectUrl(builder.build());
|
||||
redisUtil.addToString(cacheKey, presignedObjectUrl, URL_CACHE_EXPIRE_SECONDS);
|
||||
return presignedObjectUrl;
|
||||
} catch (MinioException | InvalidKeyException
|
||||
| IOException | NoSuchAlgorithmException | IllegalArgumentException e) {
|
||||
e.printStackTrace();
|
||||
@@ -958,6 +980,166 @@ public class MinioUtil {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测字符串是否为预签名URL
|
||||
* 通过检查URL中是否包含minio endpoint来判断
|
||||
*
|
||||
* @param str 待检测的字符串
|
||||
* @return true表示是预签名URL,false表示不是
|
||||
*/
|
||||
public boolean isPresignedUrl(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// 检查字符串是否是一个有效的URL
|
||||
URL url = new URL(str);
|
||||
String host = url.getHost();
|
||||
// 获取endpoint中的主机部分(去掉http://或https://)
|
||||
String endpointHost = endpoint;
|
||||
if (endpointHost.startsWith("http://")) {
|
||||
endpointHost = endpointHost.substring(7);
|
||||
} else if (endpointHost.startsWith("https://")) {
|
||||
endpointHost = endpointHost.substring(8);
|
||||
}
|
||||
// 去掉端口号
|
||||
if (endpointHost.contains(":")) {
|
||||
endpointHost = endpointHost.substring(0, endpointHost.indexOf(":"));
|
||||
}
|
||||
// 检查URL的host是否与endpoint的host匹配
|
||||
return host.equals(endpointHost);
|
||||
} catch (Exception e) {
|
||||
// 不是有效的URL
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测字符串是否为MinIO逻辑路径(bucketName/objectName格式)
|
||||
* 逻辑路径特点:
|
||||
* 1. 包含 "/"(桶名和对象名之间的分隔符)
|
||||
* 2. 不是完整的URL(不以http://或https://开头)
|
||||
* 3. 路径中没有查询参数
|
||||
*
|
||||
* @param str 待检测的字符串
|
||||
* @return true表示是MinIO逻辑路径,false表示不是
|
||||
*/
|
||||
public boolean isMinioLogicalPath(String str) {
|
||||
if (str == null || str.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 必须是字符串
|
||||
if (!(str instanceof String)) {
|
||||
return false;
|
||||
}
|
||||
String trimStr = str.trim();
|
||||
// 不应该以http://或https://开头
|
||||
if (trimStr.startsWith("http://") || trimStr.startsWith("https://")) {
|
||||
return false;
|
||||
}
|
||||
// 应该包含 "/"(bucket/object格式)
|
||||
if (!trimStr.contains("/")) {
|
||||
return false;
|
||||
}
|
||||
// 不应该包含空格或特殊字符
|
||||
if (trimStr.contains(" ") || trimStr.contains("\n") || trimStr.contains("\t")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将预签名URL转换为逻辑路径
|
||||
*
|
||||
* @param presignedUrl 预签名URL
|
||||
* @return 逻辑路径(格式:bucketName/objectName)
|
||||
*/
|
||||
public String getLogicalPathFromPresignedUrl(String presignedUrl) {
|
||||
try {
|
||||
// 解析URL
|
||||
URL url = new URL(presignedUrl);
|
||||
|
||||
// 获取路径部分(去掉开头的/)
|
||||
String path = url.getPath();
|
||||
if (path.startsWith("/")) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
|
||||
// 路径格式为 bucketName/objectName
|
||||
// Minio路径中可能包含多个/,需要正确分割
|
||||
int firstSlashIndex = path.indexOf("/");
|
||||
if (firstSlashIndex <= 0) {
|
||||
throw new MinioException("预签名URL路径格式无效,应包含桶名和对象名: " + presignedUrl);
|
||||
}
|
||||
|
||||
String bucketName = path.substring(0, firstSlashIndex);
|
||||
String objectName = path.substring(firstSlashIndex + 1);
|
||||
|
||||
// log.info("预签名URL转换成功,桶名: {}, 对象名: {}", bucketName, objectName);
|
||||
return bucketName + "/" + objectName;
|
||||
} catch (Exception e) {
|
||||
log.error("预签名URL解析失败: {}", e.getMessage(), e);
|
||||
throw new BusinessException("system.error");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 处理MinIO资源(预签名URL或逻辑路径),统一生成预签名URL
|
||||
*
|
||||
* @param resource 预签名URL或逻辑路径
|
||||
* @param expires 过期时间(秒)
|
||||
* @return 新的预签名URL
|
||||
*/
|
||||
public String processMinioResource(String resource, int expires) {
|
||||
try {
|
||||
String logicalPath;
|
||||
if (isPresignedUrl(resource)) {
|
||||
// 是预签名URL,解析为逻辑路径
|
||||
logicalPath = getLogicalPathFromPresignedUrl(resource);
|
||||
} else if (isMinioLogicalPath(resource)) {
|
||||
// 本身就是逻辑路径
|
||||
logicalPath = resource.trim();
|
||||
} else {
|
||||
// 不认识的内容,直接返回原始值
|
||||
log.warn("未识别的MinIO资源格式: {}", resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
// 统一生成预签名URL
|
||||
return getPreSignedUrl(logicalPath, expires);
|
||||
} catch (Exception e) {
|
||||
log.error("处理MinIO资源失败: {}, error: {}", resource, e.getMessage(), e);
|
||||
// 如果失败,返回原始内容
|
||||
return resource;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将任意MinIO URL转换为逻辑路径
|
||||
* 检测URL类型并转换为逻辑路径返回
|
||||
*
|
||||
* @param url 预签名URL或逻辑路径
|
||||
* @return 逻辑路径(格式:bucketName/objectName)
|
||||
* @throws MinioException 如果不是有效的MinIO资源
|
||||
*/
|
||||
public String convertToLogicalPath(String url) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
throw new BusinessException("url.cannot.be.empty");
|
||||
}
|
||||
if (isMinioLogicalPath(url)) {
|
||||
// 本身就是逻辑路径,直接返回
|
||||
return url.trim();
|
||||
} else if (isPresignedUrl(url)) {
|
||||
// 是预签名URL,转换为逻辑路径
|
||||
return getLogicalPathFromPresignedUrl(url);
|
||||
} else {
|
||||
// 不认识的内容,抛出异常
|
||||
throw new BusinessException("无法识别的MinIO资源格式: " + url + ",请提供有效的预签名URL或逻辑路径");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -549,26 +549,6 @@ public class RedisUtil {
|
||||
public final static String STRIPE_EXCEPTION_LOG = "StripeException:";
|
||||
public final static String SUBSCRIPTION_SENT_EMAIL_TYPE = "SubscriptionEmailSentType:";
|
||||
|
||||
private static final String STRIPE_WEBHOOK_PROCESSED_PREFIX = "StripeWebhook:processed:";
|
||||
|
||||
/**
|
||||
* 尝试将 webhook eventId 标记为已处理(SETNX 语义)
|
||||
* @return true=该事件之前未处理(本次处理),false=该事件已处理过(跳过)
|
||||
*/
|
||||
public boolean tryMarkWebhookProcessed(String eventId, long expireSeconds) {
|
||||
String key = STRIPE_WEBHOOK_PROCESSED_PREFIX + eventId;
|
||||
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, "1", expireSeconds, TimeUnit.SECONDS);
|
||||
return Boolean.TRUE.equals(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 webhook eventId 是否已处理
|
||||
*/
|
||||
public boolean isWebhookProcessed(String eventId) {
|
||||
String key = STRIPE_WEBHOOK_PROCESSED_PREFIX + eventId;
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
}
|
||||
|
||||
public void batchDeleteKeysWithSamePrefix(String prefix){
|
||||
Set<String> keys = redisTemplate.keys(prefix + "*");
|
||||
assert keys != null;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package com.ai.da.common.utils;
|
||||
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
public class SecurityContextUtils {
|
||||
|
||||
public static AuthPrincipalVo getCurrentUser() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication != null && authentication.getPrincipal() != null) {
|
||||
return (AuthPrincipalVo) authentication.getPrincipal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Long getCurrentUserId() {
|
||||
AuthPrincipalVo currentUser = getCurrentUser();
|
||||
if (currentUser != null) {
|
||||
return currentUser.getId();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static String getCurrentUsername() {
|
||||
AuthPrincipalVo currentUser = getCurrentUser();
|
||||
if (currentUser != null) {
|
||||
return currentUser.getUsername();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -17,38 +17,17 @@ 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
|
||||
*/
|
||||
@@ -786,7 +765,9 @@ public class SendEmailUtil {
|
||||
|
||||
public static boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||
try {
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
Credential cred = new Credential(SECRET_ID, SECRET_KEy);
|
||||
// 实例化一个http选项,可选的,没有特殊需求可以跳过
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
@@ -985,7 +966,9 @@ public class SendEmailUtil {
|
||||
// 实例化一个请求对象,每个接口都会对应一个request对象
|
||||
SendEmailRequest req = new SendEmailRequest();
|
||||
req.setFromEmailAddress(SEND_ADDRESS);
|
||||
req.setDestination(buildMerchantReceiverEmail());
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developerEmail = "xupei@code-create.com.hk";
|
||||
req.setDestination(new String[]{/*merchantEmail,*/ developerEmail});
|
||||
Template template = new Template();
|
||||
req.setSubject("New Credit Purchase Order");
|
||||
template.setTemplateID(CREDITS_PURCHASE_MERCHANT);
|
||||
@@ -1093,25 +1076,45 @@ 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);
|
||||
}
|
||||
private final static Long SELLER_APPROVED = 184414L;
|
||||
private final static Long SELLER_REJECTED = 184415L;
|
||||
public static void sellerApproval(String receiver, boolean isApproved) {
|
||||
try {
|
||||
// 实例化一个认证对象
|
||||
Credential cred = new Credential(SECRET_ID, SECRET_KEy);
|
||||
HttpProfile httpProfile = new HttpProfile();
|
||||
httpProfile.setEndpoint("ses.tencentcloudapi.com");
|
||||
ClientProfile clientProfile = new ClientProfile();
|
||||
clientProfile.setHttpProfile(httpProfile);
|
||||
SesClient client = new SesClient(cred, "ap-hongkong", clientProfile);
|
||||
SendEmailRequest req = new SendEmailRequest();
|
||||
req.setFromEmailAddress(CODE_CREATE_SEND_ADDRESS);
|
||||
req.setDestination(new String[]{receiver});
|
||||
|
||||
// 根据邮件类型设置不同的主题和模板
|
||||
String subject;
|
||||
Template template = new Template();
|
||||
if (isApproved) {
|
||||
subject = "AiDA卖家权限已开通 AiDA Seller Access Enabled";
|
||||
template.setTemplateID(SELLER_APPROVED);
|
||||
}else {
|
||||
subject = "AiDA卖家权限审批不通过 Seller Access Not Approved";
|
||||
template.setTemplateID(SELLER_REJECTED);
|
||||
}
|
||||
|
||||
req.setSubject(subject);
|
||||
req.setTemplate(template);
|
||||
|
||||
// 发送邮件
|
||||
SendEmailResponse resp = client.SendEmail(req);
|
||||
log.info("邮件发送成功,收件人地址:{}", receiver);
|
||||
log.info("短信发送结果res###{}", SendEmailResponse.toJsonString(resp));
|
||||
} catch (TencentCloudSDKException e) {
|
||||
log.info(receiver);
|
||||
log.error("邮件发送失败###{},收件人地址:{}", e.toString(), receiver);
|
||||
|
||||
}
|
||||
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]);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
131
src/main/java/com/ai/da/common/utils/TokenGenerateUtils.java
Normal file
131
src/main/java/com/ai/da/common/utils/TokenGenerateUtils.java
Normal file
@@ -0,0 +1,131 @@
|
||||
package com.ai.da.common.utils;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.model.vo.AuthPrincipalVo;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Token 生成工具类(仅负责生成,不负责鉴权)。
|
||||
* 鉴权逻辑已迁移至 Gateway(GlobalAuthWebFilter)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TokenGenerateUtils {
|
||||
|
||||
private static final String ISSUER = "DWJ";
|
||||
|
||||
@Value("${spring.security.jwtSecret:JWTSECRET}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${spring.security.jwtExpiration:8640000000}")
|
||||
private long jwtExpiration;
|
||||
|
||||
@Value("${spring.security.jwtTokenPrefix:Bearer-}")
|
||||
private String jwtTokenPrefix;
|
||||
|
||||
/**
|
||||
* 生成 JWT Token。
|
||||
* @param principal 用户信息
|
||||
* @return 完整的 token(含 prefix)
|
||||
*/
|
||||
public String createToken(AuthPrincipalVo principal) {
|
||||
SecretKey key = buildSigningKey();
|
||||
String token = Jwts.builder()
|
||||
.id(String.valueOf(principal.getId()))
|
||||
.subject(JSONObject.toJSONString(principal))
|
||||
.issuedAt(new Date())
|
||||
.issuer(ISSUER)
|
||||
.expiration(new Date(System.currentTimeMillis() + jwtExpiration))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
return jwtTokenPrefix + token;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Token 过期时间(毫秒)。
|
||||
*/
|
||||
public long getJwtExpiration() {
|
||||
return jwtExpiration;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成用于邮箱变更的简化 Token。
|
||||
* @param userId 用户 ID
|
||||
* @param mailbox 新邮箱
|
||||
* @return token(不含 prefix)
|
||||
*/
|
||||
public String createMailboxToken(Long userId, String mailbox) {
|
||||
SecretKey key = buildSigningKey();
|
||||
return Jwts.builder()
|
||||
.id(String.valueOf(userId))
|
||||
.subject(mailbox + "_" + userId)
|
||||
.issuedAt(new Date())
|
||||
.issuer(ISSUER)
|
||||
.expiration(new Date(System.currentTimeMillis() + CommonConstant.CHANGE_MAILBOX_LINK_VALIDITY))
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Token 是否有效(签名和有效期)。
|
||||
*/
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Claims claims = parseTokenBody(token);
|
||||
return claims != null;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Token 中解析用户信息。
|
||||
*/
|
||||
public AuthPrincipalVo parserToUser(String token) {
|
||||
try {
|
||||
String subject = parseTokenBody(token).getSubject();
|
||||
if (StrUtil.isNotEmpty(subject)) {
|
||||
return JSONObject.parseObject(subject, AuthPrincipalVo.class);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("JWT解析用户信息失败: {}", e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析邮箱变更 Token,返回 "email_id" 格式字符串。
|
||||
*/
|
||||
public String parseMailboxToken(String token) {
|
||||
return parseTokenBody(token).getSubject();
|
||||
}
|
||||
|
||||
private Claims parseTokenBody(String token) {
|
||||
SecretKey key = buildSigningKey();
|
||||
return Jwts.parser()
|
||||
.verifyWith(key)
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
|
||||
private SecretKey buildSigningKey() {
|
||||
byte[] raw = jwtSecret.getBytes(StandardCharsets.UTF_8);
|
||||
if (raw.length < 32) {
|
||||
raw = DigestUtil.sha256(raw);
|
||||
}
|
||||
return Keys.hmacShaKeyFor(raw);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ 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;
|
||||
@@ -199,19 +198,6 @@ public class GlobalAwardController {
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.ai.da.model.dto.GetNotificationDTO;
|
||||
import com.ai.da.model.vo.NotificationVO;
|
||||
import com.ai.da.model.dto.PublishSysNotificationDTO;
|
||||
import com.ai.da.service.MessageCenterService;
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -60,4 +61,12 @@ public class MessageCenterController {
|
||||
messageCenterService.setReadAll(type);
|
||||
return Response.success("success");
|
||||
}
|
||||
|
||||
@Hidden
|
||||
@Operation(summary = "卖家审批结果站内信通知")
|
||||
@PostMapping("/sellerApprovalNotice")
|
||||
public Response<String> sellerApprovalNotice(@RequestParam("userId") Long userId, @RequestParam("isApproved") boolean isApproved) {
|
||||
messageCenterService.sellerApprovalNotice(userId, isApproved);
|
||||
return Response.success("success");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
@@ -11,7 +10,6 @@ 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;
|
||||
@@ -42,8 +40,6 @@ public class StripeController {
|
||||
private StripeService stripeService;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
|
||||
@Operation(summary = "创建支付链接")
|
||||
@PostMapping("/createOrder")
|
||||
@@ -57,29 +53,30 @@ public class StripeController {
|
||||
@Operation(summary = "支付通知")
|
||||
@PostMapping("/trade/notify")
|
||||
public void callback(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
|
||||
boolean result;
|
||||
try {
|
||||
result = stripeService.notify(request);
|
||||
} catch (Exception e) {
|
||||
log.error("Stripe Controller层异常捕捉, {}", e.getMessage(), e);
|
||||
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();
|
||||
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 ("1".equals(webhookReminderFlag) && size == 3) {
|
||||
// 给我发送邮件
|
||||
if (webhookReminderFlag.equals("1") && 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);
|
||||
@@ -88,7 +85,7 @@ public class StripeController {
|
||||
}else {
|
||||
return Response.fail("Request for refund failed.");
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
@Operation(summary = "获取订阅")
|
||||
@GetMapping("/getSubscription")
|
||||
@@ -103,8 +100,7 @@ public class StripeController {
|
||||
@Operation(summary = "取消订阅")
|
||||
@GetMapping("/cancelSubscription")
|
||||
public Response<String> cancelSubscription(@RequestParam String subscriptionId, @RequestParam(required = false) String reason) {
|
||||
Long accountId = UserContext.getUserHolder().getId();
|
||||
stripeSubscriptionService.cancelSubscription(subscriptionId, reason, accountId);
|
||||
stripeService.cancelSubscription(subscriptionId, reason);
|
||||
return Response.success("success");
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.ai.da.feign.gateway;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* 调用 Gateway 黑名单接口,将指定用户的 token 加入黑名单。
|
||||
* 替代原来的 SellerFeignClient.clearTokenCache。
|
||||
*/
|
||||
@FeignClient(name = "aida-gateway", path = "/internal")
|
||||
public interface GatewayFeignClient {
|
||||
|
||||
/**
|
||||
* 将用户 token 加入黑名单。
|
||||
* 后续 Gateway 会拒绝携带该用户 token 的请求。
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
Response<Void> logout(@RequestParam("userId") Long userId);
|
||||
|
||||
/**
|
||||
* 清除用户黑名单,允许该用户重新登录(登录时会自动调用)。
|
||||
*/
|
||||
@PostMapping("/clear-blacklist")
|
||||
Response<Void> clearBlacklist(@RequestParam("userId") Long userId);
|
||||
}
|
||||
20
src/main/java/com/ai/da/feign/seller/SellerFeignClient.java
Normal file
20
src/main/java/com/ai/da/feign/seller/SellerFeignClient.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package com.ai.da.feign.seller;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/**
|
||||
* 调用 aida-seller 设计师相关接口的 Feign 客户端
|
||||
*/
|
||||
@FeignClient(name = "aida-seller", path = "/api/designer")
|
||||
public interface SellerFeignClient {
|
||||
|
||||
@GetMapping("/check")
|
||||
Response<Boolean> checkDesignerQualification(@RequestParam("userId") Long userId);
|
||||
|
||||
@PostMapping("/cache/clear")
|
||||
Response<Void> clearTokenCache(@RequestParam("userId") Long userId);
|
||||
}
|
||||
@@ -23,6 +23,8 @@ public class OrderInfo extends BaseEntity{
|
||||
|
||||
private String note;
|
||||
|
||||
private byte autoRenewal;
|
||||
|
||||
private String paymentType;//支付方式
|
||||
|
||||
// 可用于标记用户订单是否首次订阅
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.ai.da.model.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.Hidden;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@@ -25,7 +24,6 @@ public class ProductPurchaseDTO {
|
||||
@Schema(description = "EcoMonth || Month || Year")
|
||||
private String subscribeType;
|
||||
|
||||
@Hidden
|
||||
@Schema(description = "是否自动续订 one_time || recurring")
|
||||
private Boolean autoRenewal;
|
||||
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
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,8 +34,4 @@ public class QueryUserConditionsVO extends PageQueryBaseVo {
|
||||
|
||||
private Integer systemUser;
|
||||
|
||||
private Long subscriptionPlanId;
|
||||
|
||||
private Long organizationId;
|
||||
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class PythonService {
|
||||
@Value("${access.python.generate_sr_port}")
|
||||
private String srServicePort;
|
||||
|
||||
@Value("${design.callback.url}")
|
||||
@Value("${design.callback.url.aida}")
|
||||
private String callbackUrl;
|
||||
|
||||
@Resource
|
||||
@@ -2912,71 +2912,72 @@ public class PythonService {
|
||||
|
||||
private PrintToPython resolveDesignSinglePrint(List<DesignSinglePrint> printObject, String partialDesign) {
|
||||
PrintToPython printToPython = new PrintToPython();
|
||||
// DesignPythonItemPrint printSingle = new DesignPythonItemPrint();
|
||||
DesignPythonItemPrint printOverall = new DesignPythonItemPrint();
|
||||
// printToPython.setSingle(printSingle);
|
||||
printToPython.setOverall(printOverall);
|
||||
printToPython.setPartial(StringUtil.isNullOrEmpty(partialDesign) ? null : partialDesign);
|
||||
if (Objects.isNull(printObject) || printObject.isEmpty()) {
|
||||
return printToPython;
|
||||
}
|
||||
|
||||
// 1. 先对 printObject 按 priority 排序(升序)
|
||||
List<DesignSinglePrint> sortedPrints = printObject.stream()
|
||||
.sorted(Comparator.comparingInt(DesignSinglePrint::getPriority))
|
||||
.toList();
|
||||
// 没有印花时的参数设置
|
||||
// if (printObject.getIfSingle().equals(Boolean.FALSE) && CollectionUtil.isEmpty(printObject.getPrints())) {
|
||||
// return new DesignPythonItemPrint(new ArrayList<>(), false);
|
||||
// }
|
||||
// DesignPythonItemPrint print = CopyUtil.copyObject(printObject, DesignPythonItemPrint.class);
|
||||
|
||||
// 2. 分别收集单印和非单印的数据
|
||||
List<DesignSinglePrint> singlePrints = sortedPrints.stream()
|
||||
.filter(DesignSinglePrint::getIfSingle)
|
||||
.toList();
|
||||
int size = printObject.size();
|
||||
// 占位符填充数组
|
||||
List<List<Float>> locationS = new ArrayList<>(Collections.nCopies(size, null));
|
||||
List<List<Float>> scaleS = new ArrayList<>(Collections.nCopies(size, null));
|
||||
List<Double> angleS = new ArrayList<>(Collections.nCopies(size, null));
|
||||
ArrayList<String> pathsS = new ArrayList<>(Collections.nCopies(size, null));
|
||||
|
||||
List<DesignSinglePrint> overallPrints = sortedPrints.stream()
|
||||
.filter(p -> !p.getIfSingle())
|
||||
.toList();
|
||||
List<List<Float>> locationO = new ArrayList<>(Collections.nCopies(size, null));
|
||||
List<List<Float>> scaleO = new ArrayList<>(Collections.nCopies(size, null));
|
||||
List<Double> angleO = new ArrayList<>(Collections.nCopies(size, null));
|
||||
ArrayList<String> pathsO = new ArrayList<>(Collections.nCopies(size, null));
|
||||
|
||||
// 3. 处理单印数据
|
||||
if (!singlePrints.isEmpty()) {
|
||||
List<List<Float>> locationS = new ArrayList<>();
|
||||
List<List<Float>> scaleS = new ArrayList<>();
|
||||
List<Double> angleS = new ArrayList<>();
|
||||
List<String> pathsS = new ArrayList<>();
|
||||
|
||||
for (DesignSinglePrint p : singlePrints) {
|
||||
setUriToMinioPath(p);
|
||||
locationS.add(p.getLocation());
|
||||
scaleS.add(p.getScale());
|
||||
angleS.add(p.getAngle());
|
||||
pathsS.add(p.getMinIOPath());
|
||||
// 设置印花的位置、大小、旋转角度
|
||||
// 优先级越大,越靠近顶层,在传输给python的数组中,越靠前
|
||||
// List<DesignSinglePrint> prints = printObject.getPrints();
|
||||
printObject.forEach(p -> {
|
||||
p.getLocation().set(0, p.getLocation().get(0));
|
||||
p.getLocation().set(1, p.getLocation().get(1));
|
||||
Integer priority = p.getPriority();
|
||||
setUriToMinioPath(p);
|
||||
// todo 下标越界问题
|
||||
if (p.getIfSingle()) {
|
||||
locationS.set(priority - 1, p.getLocation());
|
||||
scaleS.set(priority - 1, p.getScale());
|
||||
angleS.set(priority - 1, p.getAngle());
|
||||
pathsS.set(priority - 1, p.getMinIOPath());
|
||||
} else {
|
||||
locationO.set(priority - 1, p.getLocation());
|
||||
scaleO.set(priority - 1, p.getScale());
|
||||
angleO.set(priority - 1, p.getAngle());
|
||||
pathsO.set(priority - 1, p.getMinIOPath());
|
||||
}
|
||||
// log.info("本次print打点locations###{}###fileVO{}", p.getLocation(), JSON.toJSONString(fileVO));
|
||||
});
|
||||
/*locationS.removeAll(Collections.singleton(null));
|
||||
scaleS.removeAll(Collections.singleton(null));
|
||||
angleS.removeAll(Collections.singleton(null));
|
||||
pathsS.removeAll(Collections.singleton(null));
|
||||
printSingle.setLocation(locationS);
|
||||
printSingle.setPrint_scale_list(scaleS);
|
||||
printSingle.setPrint_angle_list(angleS);
|
||||
printSingle.setPrint_path_list(pathsS);*/
|
||||
|
||||
// 注意:如果 printOverall 中需要设置单印数据,请在这里添加相应的 setter
|
||||
// 根据您的原始代码,似乎只设置了 overall(非单印)的数据
|
||||
// 如果需要设置单印,请取消下面的注释并添加对应的字段
|
||||
// printOverall.setSingleLocation(locationS);
|
||||
// printOverall.setSingleScale(scaleS);
|
||||
// printOverall.setSingleAngle(angleS);
|
||||
// printOverall.setSinglePath(pathsS);
|
||||
}
|
||||
|
||||
// 4. 处理非单印数据(整体印花)
|
||||
if (!overallPrints.isEmpty()) {
|
||||
List<List<Float>> locationO = new ArrayList<>();
|
||||
List<List<Float>> scaleO = new ArrayList<>();
|
||||
List<Double> angleO = new ArrayList<>();
|
||||
List<String> pathsO = new ArrayList<>();
|
||||
|
||||
for (DesignSinglePrint p : overallPrints) {
|
||||
setUriToMinioPath(p);
|
||||
locationO.add(p.getLocation());
|
||||
scaleO.add(p.getScale());
|
||||
angleO.add(p.getAngle());
|
||||
pathsO.add(p.getMinIOPath());
|
||||
}
|
||||
|
||||
printOverall.setLocation(locationO);
|
||||
printOverall.setPrint_scale_list(scaleO);
|
||||
printOverall.setPrint_angle_list(angleO);
|
||||
printOverall.setPrint_path_list(pathsO);
|
||||
}
|
||||
locationO.removeAll(Collections.singleton(null));
|
||||
scaleO.removeAll(Collections.singleton(null));
|
||||
angleO.removeAll(Collections.singleton(null));
|
||||
pathsO.removeAll(Collections.singleton(null));
|
||||
printOverall.setLocation(locationO);
|
||||
printOverall.setPrint_scale_list(scaleO);
|
||||
printOverall.setPrint_angle_list(angleO);
|
||||
printOverall.setPrint_path_list(pathsO);
|
||||
|
||||
return printToPython;
|
||||
}
|
||||
|
||||
37
src/main/java/com/ai/da/seller/DesignUrlsDTO.java
Normal file
37
src/main/java/com/ai/da/seller/DesignUrlsDTO.java
Normal file
@@ -0,0 +1,37 @@
|
||||
package com.ai.da.seller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 设计URLs DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "设计URLs数据传输对象")
|
||||
public class DesignUrlsDTO {
|
||||
|
||||
/**
|
||||
* 设计项ID
|
||||
*/
|
||||
@Schema(description = "设计项ID", example = "1")
|
||||
private Long designItemId;
|
||||
|
||||
/**
|
||||
* TO_PRODUCT_IMAGE类型的URL列表
|
||||
*/
|
||||
@Schema(description = "TO_PRODUCT_IMAGE类型的URL列表")
|
||||
private List<String> toProductImageUrls;
|
||||
|
||||
/**
|
||||
* DesignItemDetail的path列表
|
||||
*/
|
||||
@Schema(description = "DesignItemDetail的path列表")
|
||||
private List<String> clothes;
|
||||
|
||||
/**
|
||||
* 姿势转换视频信息列表
|
||||
*/
|
||||
@Schema(description = "姿势转换视频信息列表")
|
||||
private List<PoseTransformationVideoDTO> videos;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.ai.da.seller;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 姿势转换视频信息DTO
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "姿势转换视频信息数据传输对象")
|
||||
public class PoseTransformationVideoDTO {
|
||||
|
||||
/**
|
||||
* GIF第一帧截图URL
|
||||
*/
|
||||
@Schema(description = "GIF第一帧截图URL")
|
||||
private String firstFrameUrl;
|
||||
|
||||
/**
|
||||
* GIF视频URL
|
||||
*/
|
||||
@Schema(description = "GIF视频URL")
|
||||
private String gifUrl;
|
||||
|
||||
/**
|
||||
* 视频URL
|
||||
*/
|
||||
@Schema(description = "视频URL")
|
||||
private String videoUrl;
|
||||
}
|
||||
44
src/main/java/com/ai/da/seller/SellerController.java
Normal file
44
src/main/java/com/ai/da/seller/SellerController.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.ai.da.seller;
|
||||
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.service.UserLikeGroupService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Seller Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/seller")
|
||||
@RequiredArgsConstructor
|
||||
@Tag(name = "Seller", description = "Seller相关接口")
|
||||
public class SellerController {
|
||||
|
||||
private final UserLikeGroupService userLikeGroupService;
|
||||
|
||||
/**
|
||||
* 根据designItemId列表获取设计相关的URL列表
|
||||
* @param designItemIds designItemId列表
|
||||
* @return 设计URLs DTO列表
|
||||
*/
|
||||
@GetMapping("/sketchDetail")
|
||||
@Operation(summary = "获取设计相关URL列表", description = "根据designItemId列表获取设计相关的URL列表,包括TO_PRODUCT_IMAGE类型的URL和DesignItemDetail的path列表")
|
||||
public Response<List<DesignUrlsDTO>> getDesignUrlsByDesignItemIds(
|
||||
@Parameter(description = "设计项ID列表", required = true, example = "1,2,3")
|
||||
@RequestParam List<Long> designItemIds) {
|
||||
List<DesignUrlsDTO> designUrlsDTOList = new ArrayList<>();
|
||||
for (Long designItemId : designItemIds) {
|
||||
DesignUrlsDTO designUrlsDTO = userLikeGroupService.getToProductImageUrlsByDesignItemId(designItemId);
|
||||
designUrlsDTOList.add(designUrlsDTO);
|
||||
}
|
||||
return Response.success(designUrlsDTOList);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@ 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;
|
||||
@@ -47,22 +46,6 @@ public interface GlobalAwardService {
|
||||
* @return 参赛者数量和最大参赛者编号
|
||||
*/
|
||||
ContestantCountVO getContestantCount();
|
||||
|
||||
/**
|
||||
* 记录比赛页面的访问量
|
||||
* <ul>
|
||||
* <li>rawVisitCount: 每次访问或刷新都计一次(不去重)</li>
|
||||
* <li>uniqueVisitCount: 5秒内刷新只算一次(基于会话去重)</li>
|
||||
* </ul>
|
||||
* @param sessionId 会话ID(用于5秒去重判断)
|
||||
*/
|
||||
void recordPageVisit(String sessionId);
|
||||
|
||||
/**
|
||||
* 获取比赛页面的两种访问量
|
||||
* @return 原始访问量和去重访问量
|
||||
*/
|
||||
PageVisitCountVO getPageVisitCount();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -29,4 +29,6 @@ public interface MessageCenterService extends IService<Notification> {
|
||||
void publishSystemNotification(PublishSysNotificationDTO message);
|
||||
|
||||
void videoFinishedMsg(Long userId, String projectName, boolean isSuccess);
|
||||
|
||||
void sellerApprovalNotice(Long userId, boolean isApproved);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
HttpServletRequest request, byte autoRenewal);
|
||||
|
||||
void saveCodeUrl(String orderNo, String codeUrl);
|
||||
|
||||
|
||||
@@ -9,8 +9,6 @@ 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;
|
||||
@@ -25,15 +23,9 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
||||
|
||||
void createPaymentInfoForAliPayHK(AlipayHKCallbackDTO alipayHKCallbackDTO, String type);
|
||||
|
||||
void createOrUpdatePaymentInfoForStripe(Session session);
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice);
|
||||
|
||||
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);
|
||||
PaymentInfo createOrUpdatePaymentInfoForStripe(Charge charge);
|
||||
|
||||
List<PaymentInfo> getPaymentInfoByOrderNo(String orderId, String order);
|
||||
|
||||
@@ -43,9 +35,5 @@ public interface PaymentInfoService extends IService<PaymentInfo> {
|
||||
|
||||
List<PaymentInfo> getPaymentInfoByPromCode(Long accountId, String promCode);
|
||||
|
||||
// PaymentInfo updatePaymentRefundStatus(Charge charge);
|
||||
|
||||
void updatePaymentRefundStatusByChargeId(Charge charge, String status);
|
||||
|
||||
void updatePaymentRefundStatusByInvoiceId(String invoiceId, String status);
|
||||
PaymentInfo updatePaymentRefundStatus(Charge charge);
|
||||
}
|
||||
|
||||
@@ -24,18 +24,10 @@ 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,20 +21,42 @@ 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);
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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);
|
||||
}
|
||||
@@ -1,124 +1,132 @@
|
||||
package com.ai.da.service;
|
||||
|
||||
import com.ai.da.common.response.PageBaseResponse;
|
||||
import com.ai.da.mapper.primary.entity.*;
|
||||
import com.ai.da.model.dto.*;
|
||||
import com.ai.da.model.vo.*;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.minio.errors.MinioException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 服务类
|
||||
*
|
||||
* @author yanglei
|
||||
* @since 2022-09-11
|
||||
*/
|
||||
public interface UserLikeGroupService extends IService<UserLikeGroup> {
|
||||
|
||||
void deleteUserGroup(Long userGroupId);
|
||||
|
||||
HistoryUpdateVO updateUserGroupName(Long userGroupId, String userGroupName, String timeZone);
|
||||
|
||||
Long insertUserGroup(Long userId, Long collectionId, String timeZone, Long projectId);
|
||||
|
||||
/**
|
||||
* choose
|
||||
*
|
||||
* @param userGroupId
|
||||
* @return
|
||||
*/
|
||||
UserLikeChooseVO choose(Long userGroupId);
|
||||
|
||||
ProjectChooseVO choose(ProjectDTO projectDTO);
|
||||
|
||||
UserLikeGroup getByProjectId(Long projectId);
|
||||
|
||||
void deleteTrialData(Long id);
|
||||
|
||||
void updateDate(Long id, String timeZone);
|
||||
|
||||
Long exportSave(MultipartFile file, Long projectId, String module, Long designItemDetailId);
|
||||
|
||||
List<ToProductImageResultVO> toProduct(ToProductImageDTO toProductImageDTO);
|
||||
|
||||
void toProduct(String taskId);
|
||||
|
||||
ToProductElementVO toProductImageElementUpload(MultipartFile file, Long projectId, String type);
|
||||
|
||||
CollectionSort productImageLike(ProductImageLikeDTO productImageLikeDTO);
|
||||
|
||||
List<MagicToolResultVO> getToProductImageResultList(List<String> taskIdList);
|
||||
|
||||
JSONObject exportSearch(ExportSearchDTO exportSearchDTO);
|
||||
|
||||
CanvasElementUpload canvasElementUpload(MultipartFile file);
|
||||
|
||||
List<ToProductImageResultVO> productImageLikeList(ToProductImageDTO toProductImageDTO);
|
||||
|
||||
Boolean productImageUnLike(ProductImageLikeDTO productImageLikeDTO);
|
||||
|
||||
void relight(String taskId);
|
||||
|
||||
List<ToProductImageResultVO> relight(ToProductImageDTO toProductImageDTO);
|
||||
|
||||
List<MagicToolResultVO> getRelightResult(List<String> taskIdList);
|
||||
|
||||
void deleteToProductRelightResult(Long id, Long projectId, String type);
|
||||
|
||||
String likeHistoryRelSketch();
|
||||
|
||||
String download();
|
||||
|
||||
Boolean productImageInitialize(ProductImageInitializeDTO productImageInitializeDTO);
|
||||
|
||||
InitializeProgressVO getInitializeProgress(ProductImageInitializeDTO productImageInitializeDTO);
|
||||
|
||||
IPage<ProjectVO> getPage(ProjectQueryDTO projectQueryDTO);
|
||||
|
||||
ModuleChooseVO getModuleContent(ProjectDTO projectDTO);
|
||||
|
||||
ModuleChooseVO saveModuleContent(ModuleSaveDTO moduleSaveDTO);
|
||||
|
||||
QueryLibraryPageVO getMannequinDetail(MannequinDTO mannequinDTO);
|
||||
|
||||
BrandLogoUploadVO brandLogoUpload(MultipartFile file);
|
||||
|
||||
Boolean brandDNASaveOrUpdate(BrandDNADTO brandDNADTO);
|
||||
|
||||
LibraryUpdateVo brandDNAUpload(MultipartFile file, Long brandId) throws IOException;
|
||||
|
||||
PageBaseResponse<BrandDNAVO> brandDNAPage(BrandDNAQueryDTO brandDNAQueryDTO);
|
||||
|
||||
BrandDNAGenerateVO brandDNAGenerate(String prompt);
|
||||
|
||||
IPage<ThreeDLayoutVO> getThreeDLayoutPage(ThreeDLayoutQueryDTO threeDLayoutQueryDTO);
|
||||
|
||||
ThreeDVO getLayoutDetail(Long threeDSimpleId);
|
||||
|
||||
ThreeDSizeVO getThreeDSize(Long threeDSimpleId);
|
||||
|
||||
void getThreeDGlb(Long threeDSimpleId, HttpServletResponse response) throws MinioException, IOException;
|
||||
|
||||
String downloadZip(Long threeDSimpleId, String sizeType, String size, HttpServletResponse response) throws MinioException, IOException;
|
||||
|
||||
Boolean delete(Long projectId);
|
||||
|
||||
Boolean brandDNADelete(BrandDNADTO brandDNADTO);
|
||||
|
||||
void toProductBatch(String taskId, String url, String progress) throws InterruptedException;
|
||||
|
||||
void relightBatch(String taskId, String url, String progress);
|
||||
|
||||
Boolean collectionLikeUpdate(CollectionLikeUpdateDTO collectionLikeUpdateDTO);
|
||||
|
||||
Boolean toProductImageElementDelete(Long id);
|
||||
|
||||
ToProductElementVO convertRelightElement(Long id);
|
||||
}
|
||||
package com.ai.da.service;
|
||||
|
||||
import com.ai.da.common.response.PageBaseResponse;
|
||||
import com.ai.da.mapper.primary.entity.*;
|
||||
import com.ai.da.model.dto.*;
|
||||
import com.ai.da.model.vo.*;
|
||||
import com.ai.da.seller.DesignUrlsDTO;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import io.minio.errors.MinioException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 服务类
|
||||
*
|
||||
* @author yanglei
|
||||
* @since 2022-09-11
|
||||
*/
|
||||
public interface UserLikeGroupService extends IService<UserLikeGroup> {
|
||||
|
||||
void deleteUserGroup(Long userGroupId);
|
||||
|
||||
HistoryUpdateVO updateUserGroupName(Long userGroupId, String userGroupName, String timeZone);
|
||||
|
||||
Long insertUserGroup(Long userId, Long collectionId, String timeZone, Long projectId);
|
||||
|
||||
/**
|
||||
* choose
|
||||
*
|
||||
* @param userGroupId
|
||||
* @return
|
||||
*/
|
||||
UserLikeChooseVO choose(Long userGroupId);
|
||||
|
||||
ProjectChooseVO choose(ProjectDTO projectDTO);
|
||||
|
||||
UserLikeGroup getByProjectId(Long projectId);
|
||||
|
||||
void deleteTrialData(Long id);
|
||||
|
||||
void updateDate(Long id, String timeZone);
|
||||
|
||||
Long exportSave(MultipartFile file, Long projectId, String module, Long designItemDetailId);
|
||||
|
||||
List<ToProductImageResultVO> toProduct(ToProductImageDTO toProductImageDTO);
|
||||
|
||||
void toProduct(String taskId);
|
||||
|
||||
ToProductElementVO toProductImageElementUpload(MultipartFile file, Long projectId, String type);
|
||||
|
||||
CollectionSort productImageLike(ProductImageLikeDTO productImageLikeDTO);
|
||||
|
||||
List<MagicToolResultVO> getToProductImageResultList(List<String> taskIdList);
|
||||
|
||||
JSONObject exportSearch(ExportSearchDTO exportSearchDTO);
|
||||
|
||||
CanvasElementUpload canvasElementUpload(MultipartFile file);
|
||||
|
||||
List<ToProductImageResultVO> productImageLikeList(ToProductImageDTO toProductImageDTO);
|
||||
|
||||
Boolean productImageUnLike(ProductImageLikeDTO productImageLikeDTO);
|
||||
|
||||
void relight(String taskId);
|
||||
|
||||
List<ToProductImageResultVO> relight(ToProductImageDTO toProductImageDTO);
|
||||
|
||||
List<MagicToolResultVO> getRelightResult(List<String> taskIdList);
|
||||
|
||||
void deleteToProductRelightResult(Long id, Long projectId, String type);
|
||||
|
||||
String likeHistoryRelSketch();
|
||||
|
||||
String download();
|
||||
|
||||
Boolean productImageInitialize(ProductImageInitializeDTO productImageInitializeDTO);
|
||||
|
||||
InitializeProgressVO getInitializeProgress(ProductImageInitializeDTO productImageInitializeDTO);
|
||||
|
||||
IPage<ProjectVO> getPage(ProjectQueryDTO projectQueryDTO);
|
||||
|
||||
ModuleChooseVO getModuleContent(ProjectDTO projectDTO);
|
||||
|
||||
ModuleChooseVO saveModuleContent(ModuleSaveDTO moduleSaveDTO);
|
||||
|
||||
QueryLibraryPageVO getMannequinDetail(MannequinDTO mannequinDTO);
|
||||
|
||||
BrandLogoUploadVO brandLogoUpload(MultipartFile file);
|
||||
|
||||
Boolean brandDNASaveOrUpdate(BrandDNADTO brandDNADTO);
|
||||
|
||||
LibraryUpdateVo brandDNAUpload(MultipartFile file, Long brandId) throws IOException;
|
||||
|
||||
PageBaseResponse<BrandDNAVO> brandDNAPage(BrandDNAQueryDTO brandDNAQueryDTO);
|
||||
|
||||
BrandDNAGenerateVO brandDNAGenerate(String prompt);
|
||||
|
||||
IPage<ThreeDLayoutVO> getThreeDLayoutPage(ThreeDLayoutQueryDTO threeDLayoutQueryDTO);
|
||||
|
||||
ThreeDVO getLayoutDetail(Long threeDSimpleId);
|
||||
|
||||
ThreeDSizeVO getThreeDSize(Long threeDSimpleId);
|
||||
|
||||
void getThreeDGlb(Long threeDSimpleId, HttpServletResponse response) throws MinioException, IOException;
|
||||
|
||||
String downloadZip(Long threeDSimpleId, String sizeType, String size, HttpServletResponse response) throws MinioException, IOException;
|
||||
|
||||
Boolean delete(Long projectId);
|
||||
|
||||
Boolean brandDNADelete(BrandDNADTO brandDNADTO);
|
||||
|
||||
void toProductBatch(String taskId, String url, String progress) throws InterruptedException;
|
||||
|
||||
void relightBatch(String taskId, String url, String progress);
|
||||
|
||||
Boolean collectionLikeUpdate(CollectionLikeUpdateDTO collectionLikeUpdateDTO);
|
||||
|
||||
Boolean toProductImageElementDelete(Long id);
|
||||
|
||||
ToProductElementVO convertRelightElement(Long id);
|
||||
|
||||
/**
|
||||
* 根据designItemId获取TO_PRODUCT_IMAGE类型的URL列表和DesignItemDetail的path列表
|
||||
* @param designItemId designItemId
|
||||
* @return 包含TO_PRODUCT_IMAGE类型的URL列表和DesignItemDetail的path列表的对象
|
||||
*/
|
||||
DesignUrlsDTO getToProductImageUrlsByDesignItemId(Long designItemId);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import com.ai.da.common.enums.LoginTypeEnum;
|
||||
import com.ai.da.common.enums.ProductEnum;
|
||||
import com.ai.da.common.response.PageBaseResponse;
|
||||
import com.ai.da.common.response.ResultEnum;
|
||||
import com.ai.da.common.security.jwt.JWTTokenHelper;
|
||||
import com.ai.da.common.utils.TokenGenerateUtils;
|
||||
import com.ai.da.feign.gateway.GatewayFeignClient;
|
||||
import com.ai.da.feign.seller.SellerFeignClient;
|
||||
import com.ai.da.common.utils.*;
|
||||
import com.ai.da.mapper.primary.*;
|
||||
import com.ai.da.mapper.primary.entity.*;
|
||||
@@ -94,7 +96,13 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
private AccountExtendMapper accountExtendMapper;
|
||||
|
||||
@Resource
|
||||
private JWTTokenHelper jwtTokenHelper;
|
||||
private TokenGenerateUtils tokenGenerateUtils;
|
||||
|
||||
@Resource
|
||||
private SellerFeignClient sellerFeignClient;
|
||||
|
||||
@Resource
|
||||
private GatewayFeignClient gatewayFeignClient;
|
||||
|
||||
@Resource
|
||||
private AccountLoginLogService accountLoginLogService;
|
||||
@@ -136,9 +144,6 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Resource
|
||||
private com.ai.da.common.security.config.SecurityProperties securityProperties;
|
||||
|
||||
@Resource
|
||||
private UserFollowService userFollowService;
|
||||
|
||||
@@ -353,12 +358,20 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
principal.setUsername(account.getUserName());
|
||||
principal.setLanguage(account.getLanguage());
|
||||
principal.setCountry(account.getCountry());
|
||||
String token2 = jwtTokenHelper.createToken(principal);
|
||||
//区分买家端登录
|
||||
principal.setSource("AIDA");
|
||||
String token2 = tokenGenerateUtils.createToken(principal);
|
||||
// 本地 JVM 缓存(适配旧逻辑)
|
||||
LocalCacheUtils.setTokenCache(String.valueOf(account.getId()), token2);
|
||||
// 同步写入 Redis,重启后仍然可用
|
||||
long jwtExpiration = securityProperties.getJwtExpiration();
|
||||
long jwtExpiration = tokenGenerateUtils.getJwtExpiration();
|
||||
redisUtil.setLoginToken(account.getId(), token2, jwtExpiration);
|
||||
// 清除黑名单,允许用户重新登录(仅当黑名单功能开启时)
|
||||
try {
|
||||
gatewayFeignClient.clearBlacklist(account.getId());
|
||||
} catch (Exception e) {
|
||||
log.warn("登录时清除黑名单失败,userId={}, error={}", account.getId(), e.getMessage());
|
||||
}
|
||||
return token2;
|
||||
}
|
||||
|
||||
@@ -614,11 +627,23 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
|
||||
@Override
|
||||
public Boolean logout(AccountLogoutDTO accountLogoutDTO) {
|
||||
// jwt 本身失效比较难做,统一用缓存实现:删除缓存即失效
|
||||
// 1. 删除本地缓存(保留,防止 Gateway 未启动时还能本地验证)
|
||||
String userIdStr = String.valueOf(accountLogoutDTO.getUserId());
|
||||
LocalCacheUtils.delTokenCache(userIdStr);
|
||||
// 同时删除 Redis 中的 token,防止服务重启后仍然有效
|
||||
// 2. 删除 Redis 中的 token(Gateway 黑名单会接力生效)
|
||||
redisUtil.deleteLoginToken(accountLogoutDTO.getUserId());
|
||||
// 3. 调用 Gateway 黑名单接口,将 token 加入 Redis 黑名单
|
||||
try {
|
||||
gatewayFeignClient.logout(accountLogoutDTO.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("调用 Gateway 黑名单接口失败,userId={}, error={}", accountLogoutDTO.getUserId(), e.getMessage());
|
||||
}
|
||||
// 4. 同步调用 seller 清除本地缓存(兼容未接入 Gateway 的节点)
|
||||
try {
|
||||
sellerFeignClient.clearTokenCache(accountLogoutDTO.getUserId());
|
||||
} catch (Exception e) {
|
||||
log.warn("调用 seller 清理缓存失败,userId={}, error={}", accountLogoutDTO.getUserId(), e.getMessage());
|
||||
}
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
@@ -1646,7 +1671,6 @@ 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");
|
||||
@@ -2154,7 +2178,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
redisUtil.addToString(key, newMailbox, CommonConstant.CHANGE_MAILBOX_LINK_VALIDITY / 1000);
|
||||
|
||||
String username = userHolder.getUsername();
|
||||
String token = jwtTokenHelper.createToken(accountId, newMailbox);
|
||||
String token = tokenGenerateUtils.createMailboxToken(accountId, newMailbox);
|
||||
// 准备激活链接,链接应该要有有效期
|
||||
String link = "?" + token;
|
||||
// 向新邮箱发送邮件,邮件附带激活链接,点击链接进行验证
|
||||
@@ -2164,7 +2188,7 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
|
||||
// 验证激活链接
|
||||
public void activateNewEmail(String token){
|
||||
// 获取链接地址信息,更新指定用户邮箱
|
||||
String emailAndId = jwtTokenHelper.parseToEmailAndId(token);
|
||||
String emailAndId = tokenGenerateUtils.parseMailboxToken(token);
|
||||
String newMailbox = emailAndId.substring(0, emailAndId.lastIndexOf("_"));
|
||||
String accountId = emailAndId.substring(emailAndId.lastIndexOf("_") + 1);
|
||||
|
||||
@@ -3383,14 +3407,12 @@ 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
|
||||
@@ -3403,36 +3425,34 @@ 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);
|
||||
// 添加积分变更记录(订单续订时的积分变更也需要记录)
|
||||
creditsService.insertToCreditsDetail(accountId,
|
||||
description + "--Stripe",
|
||||
String.valueOf(productCredits),
|
||||
"set", orderNo);
|
||||
/*CreditsDetail creditsDetail = creditsService.queryDetailByTaskId(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,6 +34,7 @@ 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;
|
||||
@@ -79,7 +80,9 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
affiliate.setPromotionMethod(promotionMethod);
|
||||
baseMapper.insert(affiliate);
|
||||
// 邮件通知审批者
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), new AffiliateEmailParamsDTO(userHolder.getUsername(), promotionMethod), "new");
|
||||
}else {
|
||||
@@ -437,7 +440,9 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
affiliateEmailParamsDTO.setUnpaidEarnings(String.valueOf(unpaidCommission));
|
||||
affiliateEmailParamsDTO.setPaidEarnings(String.valueOf(paidCommission));
|
||||
|
||||
String[] receiverEmail = buildMerchantReceiverEmail();
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
String[] receiverEmail = {/*merchantEmail,*/ developer};
|
||||
// 邮件通知
|
||||
SendEmailUtil.affiliateEmailReminder(receiverEmail, affiliateEmailParamsDTO, "summary");
|
||||
// emailService.affiliateEmailReminder(Arrays.asList(/*merchantEmail,*/ developer), affiliateEmailParamsDTO, "summary");
|
||||
@@ -602,8 +607,4 @@ public class AffiliateServiceImpl extends ServiceImpl<AffiliateMapper, Affiliate
|
||||
coupon.setUnpaidCommission(unpaidCommission);
|
||||
}
|
||||
|
||||
private String[] buildMerchantReceiverEmail() {
|
||||
return SendEmailUtil.buildMerchantReceiverEmail();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -787,14 +787,6 @@ 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";
|
||||
|
||||
@@ -83,7 +83,7 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
*
|
||||
* @param changeEvent 导致积分变更的事件
|
||||
* @param credits 变更的积分
|
||||
* @param changeType 变更类型 : positive->增 negative->减 set->重置
|
||||
* @param changeType 变更类型 : positive->增 negative->减
|
||||
*/
|
||||
@Override
|
||||
public void insertToCreditsDetail(Long accountId, String changeEvent, String credits, String changeType, String orderNo) {
|
||||
@@ -94,11 +94,9 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
if ("positive".equals(changeType)) {
|
||||
// finalCredits = account.getCredits().add(new BigDecimal(credits));
|
||||
changeCredits = "+" + credits;
|
||||
} else if ("negative".equals(changeType)) {
|
||||
} else {
|
||||
// finalCredits = account.getCredits().subtract(new BigDecimal(credits));
|
||||
changeCredits = "-" + credits;
|
||||
} else {
|
||||
changeCredits = credits;
|
||||
}
|
||||
creditsDetail.setAccountId(accountId);
|
||||
creditsDetail.setChangeEvent(changeEvent);
|
||||
@@ -109,7 +107,6 @@ public class CreditsServiceImpl extends ServiceImpl<CreditsDetailMapper, Credits
|
||||
creditsDetail.setCreateTime(LocalDateTime.now());
|
||||
|
||||
baseMapper.insert(creditsDetail);
|
||||
log.info("creditsDetail inserted");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,7 +7,6 @@ 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;
|
||||
@@ -586,7 +585,9 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
public boolean subscriptionEmailReminder(String type, SubscriptionEmailParamsDTO subscriptionEmailParamsDTO, String language, String receiverAddress) {
|
||||
try {
|
||||
List<String> merchantReceiver = buildMerchantReceiverList();
|
||||
String merchantEmail = "kimwong@code-create.com.hk";
|
||||
String developer = "xupei3360@163.com";
|
||||
List<String> merchantReceiver = Arrays.asList(/*merchantEmail,*/ developer);
|
||||
|
||||
String merchantSubject = null;
|
||||
String merchantTemplate = null;
|
||||
@@ -722,13 +723,15 @@ 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(buildMerchantReceiverList(), jsonObject, CREDITS_PURCHASE_MERCHANT, "New Credit Purchase Order", null, null);
|
||||
sendEmail(Arrays.asList(/*merchantEmail,*/ developerEmail), jsonObject, CREDITS_PURCHASE_MERCHANT, "New Credit Purchase Order", null, null);
|
||||
}
|
||||
|
||||
private final static String COMMON_EXCEPTION_REMINDER = "135279_common-exception-reminder.html";
|
||||
@@ -739,10 +742,6 @@ public class EmailServiceImpl implements EmailService {
|
||||
|
||||
sendEmail(destination, param, COMMON_EXCEPTION_REMINDER, "AiDA发生异常,请及时处理", null, null);
|
||||
}
|
||||
|
||||
private List<String> buildMerchantReceiverList() {
|
||||
return Arrays.asList(SendEmailUtil.buildMerchantReceiverEmail());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3934,48 +3934,11 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
|
||||
public byte[] downloadVideoOrImage(String url) {
|
||||
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());
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4262,8 +4225,11 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
}
|
||||
|
||||
// 发送POST请求到Flux API
|
||||
long start = System.currentTimeMillis();
|
||||
String resp = sendRequestUtil.sendFluxPost(fluxRequestUrl, requestBody.toString());
|
||||
JSONObject respObj = JSONUtil.parseObj(resp);
|
||||
long end = System.currentTimeMillis();
|
||||
log.info("flux 耗时:{}ms", end - start);
|
||||
log.info("flux 发起生成请求返回结果: {}", respObj);
|
||||
|
||||
// 从响应中提取任务ID
|
||||
|
||||
@@ -13,7 +13,6 @@ 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;
|
||||
@@ -620,37 +619,6 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import com.ai.da.common.enums.CollectionLevel1TypeEnum;
|
||||
import com.ai.da.common.response.PageBaseResponse;
|
||||
import com.ai.da.common.response.PageResponse;
|
||||
import com.ai.da.common.response.Response;
|
||||
import com.ai.da.common.security.jwt.JWTTokenHelper;
|
||||
import com.ai.da.common.utils.TokenGenerateUtils;
|
||||
import com.ai.da.common.utils.MinioUtil;
|
||||
import com.ai.da.mapper.primary.*;
|
||||
import com.ai.da.mapper.primary.entity.*;
|
||||
@@ -70,7 +70,7 @@ public class LLMServiceImpl implements LLMService {
|
||||
@Resource
|
||||
private WorkspaceRelStyleMapper workspaceRelStyleMapper;
|
||||
@Resource
|
||||
private JWTTokenHelper jwtTokenHelper;
|
||||
private TokenGenerateUtils tokenGenerateUtils;
|
||||
@Resource
|
||||
private DesignService designService;
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
@@ -89,9 +89,9 @@ public class LLMServiceImpl implements LLMService {
|
||||
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
boolean validate = jwtTokenHelper.validateToken(token); //
|
||||
boolean validate = tokenGenerateUtils.validateToken(token); //
|
||||
if (validate) {
|
||||
AuthPrincipalVo principal = jwtTokenHelper.parserToUser(token);
|
||||
AuthPrincipalVo principal = tokenGenerateUtils.parserToUser(token);
|
||||
Long accountId = principal.getId();
|
||||
// String url = "http://18.167.251.121:10002/chat-stream";
|
||||
String url = "http://10.1.1.240:1013/chat-stream";
|
||||
@@ -237,10 +237,10 @@ public class LLMServiceImpl implements LLMService {
|
||||
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
boolean validate = jwtTokenHelper.validateToken(token);
|
||||
boolean validate = tokenGenerateUtils.validateToken(token);
|
||||
// boolean validate = true;
|
||||
if (validate) {
|
||||
AuthPrincipalVo principal = jwtTokenHelper.parserToUser(token);
|
||||
AuthPrincipalVo principal = tokenGenerateUtils.parserToUser(token);
|
||||
Long accountId = principal.getId();
|
||||
// String url = "http://18.167.251.121:10002/api/chat_stream";
|
||||
String url = "http://18.167.251.121:2011/api/chat_stream";
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.ai.da.common.response.PageBaseResponse;
|
||||
import com.ai.da.common.utils.CopyUtil;
|
||||
import com.ai.da.common.utils.MinioUtil;
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
import com.ai.da.common.utils.SendEmailUtil;
|
||||
import com.ai.da.common.websocket.NotificationConnection;
|
||||
import com.ai.da.mapper.primary.*;
|
||||
import com.ai.da.mapper.primary.entity.*;
|
||||
@@ -441,4 +442,50 @@ public class MessageCenterServiceImpl extends ServiceImpl<NotificationMapper, No
|
||||
pushMessage("system", userId);
|
||||
}
|
||||
|
||||
private final static String APPROVED_MESSAGE = "尊敬的用户,您的卖家权限已开通。" +
|
||||
"现在可通过\"成为卖家\"的同一入口进入卖家中心。\n在卖家中心中,您可以:" +
|
||||
"\n·从设计项目中批量选择服装设计,并创建上架内容 " +
|
||||
"\n·将设计及高级工具媒体转为可售卖的数字商品 " +
|
||||
"\n·编辑、保存、发布并管理商品状态" +
|
||||
"\n\nDear User, your seller access has been enabled. " +
|
||||
"You can now enter the Seller Dashboard from the same entry point used to become a seller.\nIn the Seller Dashboard, you can:" +
|
||||
"\n·Batch select apparel designs from a design project and create listings" +
|
||||
"\n·Turn designs and Advanced Tools media into sellable digital items " +
|
||||
"\n·Edit, save, publish, and manage item status";
|
||||
|
||||
private final static String REJECTED_MESSAGE = "尊敬的用户,您的卖家权限申请审批未通过。 请检查您提交的信息,并确保您的卖家资料符合平台要求。您可以更新相关信息后重新提交申请。\n\n" +
|
||||
"Dear User, your seller access request was not approved. Please review the information you submitted and make sure your seller profile meets the platform requirements. You may update the relevant information and resubmit your application.";
|
||||
|
||||
public void sellerApprovalNotice(Long userId, boolean isApproved) {
|
||||
if (userId != null && userId != 0) {
|
||||
PublishSysNotificationDTO sysNotificationDTO = new PublishSysNotificationDTO();
|
||||
Notification notification = new Notification();
|
||||
notification.setType("system");
|
||||
notification.setReceiverId(userId);
|
||||
if (isApproved) {
|
||||
sysNotificationDTO.setTitle("卖家权限审批通过 Seller Access Enabled");
|
||||
sysNotificationDTO.setContent(APPROVED_MESSAGE);
|
||||
|
||||
} else {
|
||||
sysNotificationDTO.setTitle("卖家权限审批不通过 Seller Access Not Approved");
|
||||
sysNotificationDTO.setContent(REJECTED_MESSAGE);
|
||||
|
||||
}
|
||||
notification.setContent(JSON.toJSONString(sysNotificationDTO));
|
||||
notification.setIsRead(0);
|
||||
notification.setCreateTime(LocalDateTime.now());
|
||||
// 保存消息内容
|
||||
save(notification);
|
||||
// 推送系统消息
|
||||
pushMessage("system", userId);
|
||||
|
||||
Account account = accountService.getById(userId);
|
||||
if (account != null) {
|
||||
// 发送邮件
|
||||
SendEmailUtil.sellerApproval(account.getUserEmail(), isApproved);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -31,7 +31,6 @@ 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;
|
||||
@@ -91,7 +90,7 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
}
|
||||
|
||||
public OrderInfo createOrderByProductId(Integer amount, String paymentType, ProductEnum product,
|
||||
HttpServletRequest request) {
|
||||
HttpServletRequest request, byte autoRenewal) {
|
||||
|
||||
//获取商品信息
|
||||
// Product product = productMapper.selectById(amount);
|
||||
@@ -277,11 +276,10 @@ public class OrderInfoServiceImpl extends ServiceImpl<OrderInfoMapper, OrderInfo
|
||||
|
||||
public void updateTotalFeeByOrderNo(String orderNo) {
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("order_no", orderNo).in("trade_state", Arrays.asList("paid", "COMPLETED", ""));
|
||||
qw.eq("order_no", orderNo);
|
||||
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qw);
|
||||
Float sum = paymentInfos.stream()
|
||||
.map(PaymentInfo::getPayerTotal)
|
||||
.filter(Objects::nonNull)
|
||||
.reduce(0f, Float::sum);
|
||||
|
||||
baseMapper.update(
|
||||
|
||||
@@ -2,11 +2,9 @@ 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;
|
||||
@@ -22,17 +20,13 @@ import com.google.gson.Gson;
|
||||
import com.paypal.orders.Order;
|
||||
import com.stripe.Stripe;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.stripe.model.*;
|
||||
import com.stripe.model.Charge;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.Subscription;
|
||||
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.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -55,9 +49,6 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
|
||||
@Resource
|
||||
private ProductCouponsMapper productCouponsMapper;
|
||||
|
||||
/**
|
||||
* 记录支付日志:微信支付
|
||||
* @param plainText
|
||||
@@ -203,199 +194,38 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
baseMapper.insert(paymentInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 为 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;
|
||||
|
||||
public void createOrUpdatePaymentInfoForStripe(Session session){
|
||||
String orderId = session.getMetadata().get("orderId");
|
||||
String status = session.getStatus();
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
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(orderNo);
|
||||
paymentInfo.setOrderNo(orderId);
|
||||
paymentInfo.setPaymentType(PayTypeEnum.STRIPE.getType());
|
||||
paymentInfo.setTransactionId(invoiceId);
|
||||
paymentInfo.setTransactionId(sessionId);
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setPayerTotal(divide);
|
||||
Gson gson = new Gson();
|
||||
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)) {
|
||||
String json = gson.toJson(session);
|
||||
paymentInfo.setContent(json);
|
||||
// 获取订单信息
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderId);
|
||||
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, Map<String, String> paymentMethodInfo, List<Session.Discount> discounts) {
|
||||
public PaymentInfo createOrUpdatePaymentInfoForStripe(Invoice invoice){
|
||||
Stripe.apiKey = privateKey;
|
||||
StripeService stripeService = SpringUtils.getBean(StripeService.class);
|
||||
// 获取transactionId,从sessionId更改为invoiceId
|
||||
@@ -405,61 +235,29 @@ 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 (!CollectionUtils.isEmpty(discounts)) {
|
||||
promotionCode = getPromotionCodeByPromotionCodeId(discounts.getFirst().getPromotionCode());
|
||||
if (Objects.nonNull(invoice.getDiscount()) && !StringUtil.isNullOrEmpty(invoice.getDiscount().getPromotionCode())){
|
||||
ProductCoupons productCoupon = stripeService.getProductCoupon(null, invoice.getDiscount().getPromotionCode());
|
||||
promotionCode = productCoupon.getPromotionCode();
|
||||
}
|
||||
// 判断当前支付是否已经被记录,确保同一个支付不会被重复记录
|
||||
if (Objects.isNull(paymentInfo)){
|
||||
String orderNo = null;
|
||||
String billingReason = invoice.getBillingReason();
|
||||
String paymentIntentIdForCharge = null;
|
||||
String orderNo;
|
||||
try {
|
||||
if ("manual".equals(billingReason)){
|
||||
if (invoice.getBillingReason().equals("manual")){
|
||||
// 手动创建的发票,针对one-time支付
|
||||
// 获取 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);
|
||||
}
|
||||
}
|
||||
// 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 - ", "");
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
log.error("[createOrUpdatePaymentInfoForStripe] 获取订单号失败,invoiceId={}, error={}", invoiceId, e.getMessage());
|
||||
throw new RuntimeException("Failed to retrieve orderNo from invoice: " + invoiceId, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
Long amountTotal;
|
||||
if (status.equals("paid")){
|
||||
@@ -470,9 +268,11 @@ 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") ? PaymentInfoType.NEW.getType() :
|
||||
invoice.getBillingReason().equals("subscription_cycle") ? PaymentInfoType.RENEWAL.getType() : invoice.getBillingReason();
|
||||
String type = invoice.getBillingReason().equals("subscription_create") ? "new" :
|
||||
invoice.getBillingReason().equals("subscription_cycle") ? "renewal" : invoice.getBillingReason();
|
||||
|
||||
// 获取支付方式
|
||||
Map<String, String> paymentMethod = stripeService.getPaymentMethodByInvoiceId(invoiceId);
|
||||
// 获取订单信息
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
|
||||
@@ -488,8 +288,8 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
paymentInfo.setContent(json);
|
||||
paymentInfo.setType(type);
|
||||
paymentInfo.setNotified(0);
|
||||
paymentInfo.setPaymentMethod(paymentMethodInfo.get("paymentMethod"));
|
||||
paymentInfo.setLast4(paymentMethodInfo.get("last4"));
|
||||
paymentInfo.setPaymentMethod(paymentMethod.get("paymentMethod"));
|
||||
paymentInfo.setLast4(paymentMethod.get("last4"));
|
||||
paymentInfo.setHostedInvoiceUrl(invoice.getHostedInvoiceUrl());
|
||||
paymentInfo.setPromotionCode(promotionCode);
|
||||
paymentInfo.setCreateTime(LocalDateTime.now());
|
||||
@@ -500,85 +300,87 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
}
|
||||
int row = baseMapper.insertIgnore(paymentInfo);
|
||||
log.info("Payment Info insert affect rows:{}", row);
|
||||
orderInfoService.updateTotalFeeByOrderNo(orderNo);
|
||||
} else {
|
||||
}else {
|
||||
paymentInfo.setTradeState(status);
|
||||
paymentInfo.setPromotionCode(promotionCode);
|
||||
paymentInfo.setUpdateTime(LocalDateTime.now());
|
||||
baseMapper.updateById(paymentInfo);
|
||||
}
|
||||
|
||||
return paymentInfo;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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());
|
||||
}
|
||||
} 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;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
return paymentInfo;
|
||||
}
|
||||
|
||||
|
||||
@@ -637,234 +439,26 @@ public class PaymentInfoServiceImpl extends ServiceImpl<PaymentInfoMapper, Payme
|
||||
return baseMapper.selectPaidPaymentsByAccountAndPromotion(accountId, promCode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 通过 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;
|
||||
}
|
||||
public PaymentInfo updatePaymentRefundStatus(Charge charge){
|
||||
// 判断当前退款是部分退款还是全部退款
|
||||
QueryWrapper<PaymentInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("transaction_id", invoiceId);
|
||||
qw.eq("transaction_id", charge.getInvoice());
|
||||
PaymentInfo paymentInfo = baseMapper.selectOne(qw);
|
||||
if (Objects.nonNull(paymentInfo)) {
|
||||
if (!paymentInfo.getTradeState().equals(status)) {
|
||||
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)){
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1,59 +1,31 @@
|
||||
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.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.*;
|
||||
import com.ai.da.service.OrderInfoService;
|
||||
import com.ai.da.service.RefundInfoService;
|
||||
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;
|
||||
|
||||
/**
|
||||
* 根据订单号创建退款订单
|
||||
@@ -245,275 +217,18 @@ public class RefundInfoServiceImpl extends ServiceImpl<RefundInfoMapper, RefundI
|
||||
}
|
||||
|
||||
public RefundInfo updateRefundForStripe(Charge charge){
|
||||
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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 通过 charge 更新 paymentInfo 状态为 refunded
|
||||
if (charge != null) {
|
||||
paymentInfoService.updatePaymentRefundStatusByChargeId(charge, "Refunded");
|
||||
}
|
||||
|
||||
// 如果是全额退款,执行后续业务逻辑
|
||||
if (!StringUtil.isNullOrEmpty(refundInfo.getOrderNo())) {
|
||||
handleRefundSuccess(refundInfo);
|
||||
}
|
||||
|
||||
return refundInfo;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,18 +23,22 @@ 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;
|
||||
@@ -42,16 +46,14 @@ 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
|
||||
@@ -60,10 +62,14 @@ 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;
|
||||
@@ -75,8 +81,6 @@ public class StripeServiceImpl implements StripeService {
|
||||
private ProductCouponsMapper productCouponsMapper;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
@Resource
|
||||
private StripeWebhookService stripeWebhookService;
|
||||
|
||||
@Value("${stripe.private-key}")
|
||||
private String privateKey;
|
||||
@@ -105,7 +109,6 @@ public class StripeServiceImpl implements StripeService {
|
||||
productPurchaseDTO.setAutoRenewal(false);
|
||||
break;
|
||||
case "Subscription":
|
||||
productPurchaseDTO.setAutoRenewal(true);
|
||||
switch (productPurchaseDTO.getSubscribeType()){
|
||||
case "Month":
|
||||
productEnum = ProductEnum.MonthlySubscription;
|
||||
@@ -141,13 +144,16 @@ 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);
|
||||
PayTypeEnum.STRIPE.getType(), productEnum, request, autoRenewal);
|
||||
|
||||
try {
|
||||
Long id = UserContext.getUserHolder().getId();
|
||||
@@ -166,20 +172,10 @@ 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);
|
||||
// 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());
|
||||
sessionBuilder.setSubscriptionData(SessionCreateParams.SubscriptionData.builder().setDescription("AiDA - " + orderId).build());
|
||||
}else {
|
||||
sessionBuilder.setMode(SessionCreateParams.Mode.PAYMENT);
|
||||
// Stripe SDK 32.0.0: 使用 PaymentIntentData.setMetadata() 将 orderId 传递到 PaymentIntent
|
||||
// 对于手动创建的 invoice,metadata 需要在 invoice 创建时单独设置
|
||||
sessionBuilder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder()
|
||||
.setDescription("AiDA - " + orderId)
|
||||
.putMetadata("orderId", orderId)
|
||||
.build());
|
||||
sessionBuilder.setPaymentIntentData(SessionCreateParams.PaymentIntentData.builder().setDescription("AiDA - " + orderId).build());
|
||||
// one-time 手动创建发票;订阅会自动创建invoice
|
||||
sessionBuilder.setInvoiceCreation(SessionCreateParams.InvoiceCreation.builder().setEnabled(Boolean.TRUE).build());
|
||||
}
|
||||
@@ -193,8 +189,7 @@ public class StripeServiceImpl implements StripeService {
|
||||
.setQuantity((long) productPurchaseDTO.getQuantity())
|
||||
.setPrice(priceId)
|
||||
.build());
|
||||
// 将 orderId 写入 metadata,Stripe Checkout 会自动传递给关联的 PaymentIntent/Subscription
|
||||
sessionBuilder.putMetadata("orderId", orderId);
|
||||
sessionBuilder.putMetadata("orderId", orderId); //通过订单号关联用于检索支付信息(可选)
|
||||
|
||||
Session session = Session.create(sessionBuilder.build());
|
||||
List<String> paymentMethodTypes = session.getPaymentMethodTypes();
|
||||
@@ -281,9 +276,422 @@ public class StripeServiceImpl implements StripeService {
|
||||
return Price.create(priceCreateParams.build());
|
||||
}
|
||||
|
||||
@Resource
|
||||
private EmailService emailService;
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean notify(HttpServletRequest request) {
|
||||
return stripeWebhookService.notify(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;
|
||||
}
|
||||
}
|
||||
|
||||
public SubscriptionInfo getLatestSubscriptionInfoByAccountId(Long accountId){
|
||||
@@ -297,6 +705,67 @@ 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 {
|
||||
@@ -310,39 +779,101 @@ 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;
|
||||
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;
|
||||
}
|
||||
// 1、通过orderNo 查询sessionId
|
||||
// todo transactionId不再是sessionId而是invoiceId,所以这里需要更新
|
||||
PaymentInfo paymentInfo = paymentInfoService.getPaymentInfoByOrderNo(orderNo, "DESC").get(0);
|
||||
try {
|
||||
Session session = Session.retrieve(transactionId);
|
||||
String status = session.getStatus();
|
||||
if ("open".equals(status) || "expired".equals(status)) {
|
||||
Session session = Session.retrieve(paymentInfo.getTransactionId());
|
||||
if (Objects.isNull(session)) {
|
||||
log.warn("核实订单未创建 ===> {}", orderNo);
|
||||
return;
|
||||
} else if (session.getStatus().equals("open") || session.getStatus().equals("expired")) {
|
||||
// 订单未支付 || 订单过期 ---> 均设置为超时未支付
|
||||
log.info("订单超时未支付 ===> {}", orderNo);
|
||||
//更新本地订单状态
|
||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.TIMEOUT_CLOSED);
|
||||
paymentInfoService.updatePaymentStatusById(paymentInfo.getId(),
|
||||
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);
|
||||
}
|
||||
session.getStatus(),
|
||||
new Gson().toJson(session));
|
||||
} else if (session.getStatus().equals("complete")) {
|
||||
// 订单已完成
|
||||
processOrder(session);
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
// transactionId 可能是 invoiceId(Payment Mode),此时无法用 sessionId 查询
|
||||
log.warn("根据 transactionId={} 查询 Stripe Session 失败,可能为 invoiceId,error={}", transactionId, e.getMessage());
|
||||
log.error("根据sessionId获取Stripe Session失败");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public List<Subscription> getSubscription(String username, String userEmail) {
|
||||
@@ -402,6 +933,84 @@ 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;
|
||||
@@ -486,8 +1095,7 @@ 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("\"" + paymentInfo.getHostedInvoiceUrl() + "\"");
|
||||
emailParamsDTO.setOrderRef("\"" + orderListLink + paymentInfo.getId().toString() + "\"");
|
||||
emailParamsDTO.setCreateDate(String.valueOf(paymentInfo.getCreateTime()).replace("T", " "));
|
||||
emailParamsDTO.setQuantity(String.valueOf(1));
|
||||
emailParamsDTO.setTotalFee(paymentInfo.getPayerTotal().toString());
|
||||
@@ -565,7 +1173,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;
|
||||
@@ -625,7 +1233,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) {
|
||||
@@ -695,7 +1303,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);
|
||||
@@ -708,13 +1316,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);
|
||||
OrderInfo orderInfo = orderInfoService.createOrderByProductId(1, PayTypeEnum.STRIPE.getType(), ProductEnum.DailySubscription, null, (byte)0);
|
||||
|
||||
// String customerId = getCustomer(name, email);
|
||||
String paymentMethodCode = "pm_card_mastercard";
|
||||
@@ -920,17 +1528,9 @@ 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()
|
||||
.setPromotion(promotion); // 使用 setPromotion 而不是 setCoupon
|
||||
|
||||
.setCoupon(couponId)
|
||||
.setRestrictions(PromotionCodeCreateParams.Restrictions.builder().build());
|
||||
if (Objects.nonNull(maxRedemption)){
|
||||
promotionCodeParams.setMaxRedemptions(maxRedemption);
|
||||
}
|
||||
|
||||
@@ -1,607 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,7 @@ public class SubscriptionPlanServiceImpl extends ServiceImpl<SubscriptionPlanMap
|
||||
|
||||
plan.setCurrentPeriodEnd(newEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理账号数量
|
||||
*/
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,35 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,295 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.common.enums.CreditsEventsEnum;
|
||||
import com.ai.da.common.enums.OrderStatusEnum;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.ai.da.common.enums.ProductEnum;
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.CreditsDetail;
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.*;
|
||||
import com.stripe.exception.StripeException;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.InvoiceLineItem;
|
||||
import com.stripe.model.checkout.Session;
|
||||
import io.netty.util.internal.StringUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* checkout.session.completed 事件处理器
|
||||
* 业务场景:
|
||||
* 1. mode == subscription - 首次订阅成功
|
||||
* 2. mode == payment - 单次购买成功/积分购买
|
||||
* 业务动作:更新订单状态、新建支付记录、累加积分、更新用户角色、发送通知
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Order(20)
|
||||
public class CheckoutSessionCompletedHandler implements StripeEventHandler {
|
||||
|
||||
private static final String EVENT_TYPE = "checkout.session.completed";
|
||||
private static final String MODE_SUBSCRIPTION = "subscription";
|
||||
private static final String MODE_PAYMENT = "payment";
|
||||
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
@Resource
|
||||
private PaymentInfoService paymentInfoService;
|
||||
@Resource
|
||||
private CreditsService creditsService;
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
@Resource
|
||||
private AccountService accountService;
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedEventTypes() {
|
||||
return Collections.singletonList(EVENT_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Session> getSupportObjectType() {
|
||||
return Session.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean handle(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
Session session = (Session) stripeObject;
|
||||
String eventId = session.getId();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("[checkout.session.completed] 开始处理,sessionId={}", eventId);
|
||||
|
||||
String orderNo = extractOrderNoFromSession(session);
|
||||
if (orderNo == null) {
|
||||
log.warn("[checkout.session.completed] 无法提取 orderNo,sessionId={}", eventId);
|
||||
return true; // 非业务异常,返回成功
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
updateOrderStatus(orderNo);
|
||||
|
||||
// 根据 mode 判断业务类型
|
||||
String mode = session.getMode();
|
||||
if (MODE_SUBSCRIPTION.equals(mode)) {
|
||||
// 首次订阅成功
|
||||
handleSubscriptionMode(session, orderNo);
|
||||
} else if (MODE_PAYMENT.equals(mode)) {
|
||||
// 单次购买成功/积分购买
|
||||
handlePaymentMode(session, orderNo);
|
||||
}
|
||||
|
||||
|
||||
|
||||
log.info("[checkout.session.completed] 处理完成,sessionId={},orderNo={},mode={},耗时={}ms",
|
||||
eventId, orderNo, mode, System.currentTimeMillis() - startTime);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[checkout.session.completed] 处理异常,sessionId={},error={}", eventId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态
|
||||
*/
|
||||
private void updateOrderStatus(String orderNo) {
|
||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (orderInfo == null) {
|
||||
log.warn("[checkout.session.completed] 订单不存在,orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
String orderStatus = orderInfo.getOrderStatus();
|
||||
if (OrderStatusEnum.NOT_PAY.getType().equals(orderStatus)
|
||||
|| OrderStatusEnum.TIMEOUT_CLOSED.getType().equals(orderStatus)) {
|
||||
orderInfoService.updateStatusByOrderNo(orderNo, OrderStatusEnum.SUCCESS);
|
||||
log.info("[checkout.session.completed] 订单状态已更新,orderNo={}", orderNo);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理订阅模式(首次订阅成功)
|
||||
* 统一处理自动续订和非自动续订:从 session 获取 invoice,直接取 periodStart/periodEnd 创建订阅记录
|
||||
*/
|
||||
private void handleSubscriptionMode(Session session, String orderNo) {
|
||||
// 创建支付记录
|
||||
paymentInfoService.createOrUpdatePaymentInfoForStripe(session);
|
||||
|
||||
String invoiceId = session.getInvoice();
|
||||
if (StringUtil.isNullOrEmpty(invoiceId)) {
|
||||
log.warn("[checkout.session.completed] 订阅模式无 invoiceId,orderNo={}", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Invoice invoice = Invoice.retrieve(invoiceId);
|
||||
List<InvoiceLineItem> lines = invoice.getLines().getData();
|
||||
if (lines == null || lines.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
InvoiceLineItem lineItem = lines.getFirst();
|
||||
long periodStart = lineItem.getPeriod().getStart();
|
||||
long periodEnd = lineItem.getPeriod().getEnd();
|
||||
String interval = getIntervalFromLineItem(lineItem);
|
||||
String subscriptionId = getSubscriptionByInvoice(invoice);
|
||||
String status = "active";
|
||||
|
||||
// 避免重复创建
|
||||
if (!StringUtil.isNullOrEmpty(subscriptionId)) {
|
||||
QueryWrapper<SubscriptionInfo> qw = new QueryWrapper<>();
|
||||
qw.eq("subscription_id", subscriptionId);
|
||||
if (subscriptionInfoMapper.selectCount(qw) > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (orderInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriptionInfo subscriptionInfo = new SubscriptionInfo();
|
||||
subscriptionInfo.setSubscriptionId(subscriptionId);
|
||||
subscriptionInfo.setAccountId(orderInfo.getAccountId());
|
||||
subscriptionInfo.setOrderNo(orderNo);
|
||||
subscriptionInfo.setType(interval);
|
||||
subscriptionInfo.setStatus(status);
|
||||
subscriptionInfo.setCurrentPeriodStart(periodStart);
|
||||
subscriptionInfo.setCurrentPeriodEnd(periodEnd);
|
||||
subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(periodEnd, "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
||||
subscriptionInfo.setCreateTime(LocalDateTime.now());
|
||||
|
||||
subscriptionInfoMapper.insertIgnore(subscriptionInfo);
|
||||
|
||||
accountService.updateAccountValidity(orderInfo.getAccountId(), periodEnd);
|
||||
accountService.updateUserRoleAndCredits(orderInfo.getAccountId(), orderNo);
|
||||
|
||||
log.info("[checkout.session.completed] 订阅记录创建完成,orderNo={},subscriptionId={},periodEnd={}",
|
||||
orderNo, subscriptionId, periodEnd);
|
||||
|
||||
boolean sent = stripeSubscriptionService.sendSubscriptionEmail(null, "new", orderNo, subscriptionInfo);
|
||||
|
||||
if (sent) {
|
||||
log.info("[checkout.session.completed] 邮件通知完成 类型:new");
|
||||
} else {
|
||||
log.info("[checkout.session.completed] 邮件通知未完成");
|
||||
}
|
||||
} catch (StripeException e) {
|
||||
log.error("[checkout.session.completed] 处理订阅记录失败,orderNo={},error={}", orderNo, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 InvoiceLineItem 描述中提取订阅周期类型
|
||||
*/
|
||||
private String getIntervalFromLineItem(InvoiceLineItem lineItem) {
|
||||
String description = lineItem.getDescription();
|
||||
if (description == null) {
|
||||
return "month";
|
||||
}
|
||||
if (description.contains("Daily") || description.contains("Day")) {
|
||||
return "day";
|
||||
} else if (description.contains("Annual") || description.contains("Year")) {
|
||||
return "year";
|
||||
}
|
||||
return "month";
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付模式(单次购买/积分购买)
|
||||
*/
|
||||
private void handlePaymentMode(Session session, String orderNo) {
|
||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (orderInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建支付记录
|
||||
paymentInfoService.createOrUpdatePaymentInfoForStripe(session);
|
||||
|
||||
// 积分购买处理
|
||||
if (orderInfo.getTitle() != null && orderInfo.getTitle().startsWith("积分购买")) {
|
||||
CreditsDetail detail = creditsService.queryDetailByTaskId(orderNo);
|
||||
if (detail == null) {
|
||||
processCreditsPurchase(orderInfo, session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理积分购买
|
||||
*/
|
||||
private void processCreditsPurchase(OrderInfo orderInfo, Session session) {
|
||||
float totalAmount = new BigDecimal(session.getAmountTotal())
|
||||
.divide(new BigDecimal(100), 2, RoundingMode.HALF_UP).floatValue();
|
||||
float quantity = totalAmount / ProductEnum.CreditsProduct.getPrice();
|
||||
|
||||
creditsService.buyCredits(orderInfo.getAccountId(), quantity);
|
||||
creditsService.insertToCreditsDetail(
|
||||
orderInfo.getAccountId(),
|
||||
CreditsEventsEnum.BUY_CREDITS.getName() + "--Stripe",
|
||||
String.valueOf(Long.parseLong(CreditsEventsEnum.BUY_CREDITS.getValue()) * (long) quantity),
|
||||
"positive",
|
||||
orderInfo.getOrderNo()
|
||||
);
|
||||
|
||||
log.info("[checkout.session.completed] 积分购买处理完成,accountId={},quantity={},orderNo={}",
|
||||
orderInfo.getAccountId(), quantity, orderInfo.getOrderNo());
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 Session 中提取订单号
|
||||
*/
|
||||
private String extractOrderNoFromSession(Session session) {
|
||||
Map<String, String> metadata = session.getMetadata();
|
||||
if (metadata == null || !metadata.containsKey("orderId")) {
|
||||
log.warn("Session {} 缺少 orderId metadata", session.getId());
|
||||
return null;
|
||||
}
|
||||
return metadata.get("orderId");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,118 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.OrderInfo;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.OrderInfoService;
|
||||
import com.ai.da.service.PaymentInfoService;
|
||||
import com.ai.da.service.StripeSubscriptionService;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* checkout.session.expired 事件处理器
|
||||
* 业务场景:Checkout Session 过期
|
||||
* 业务动作:处理过期订单,发送失败通知邮件
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Order(70)
|
||||
public class CheckoutSessionExpiredHandler implements StripeEventHandler {
|
||||
|
||||
private static final String EVENT_TYPE = "checkout.session.expired";
|
||||
|
||||
@Resource
|
||||
private OrderInfoService orderInfoService;
|
||||
@Resource
|
||||
private PaymentInfoService paymentInfoService;
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedEventTypes() {
|
||||
return Collections.singletonList(EVENT_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<com.stripe.model.checkout.Session> getSupportObjectType() {
|
||||
return com.stripe.model.checkout.Session.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
com.stripe.model.checkout.Session session = (com.stripe.model.checkout.Session) stripeObject;
|
||||
String eventId = session.getId();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("[checkout.session.expired] 开始处理,sessionId={}", eventId);
|
||||
|
||||
String orderNo = extractOrderNoFromSession(session);
|
||||
if (orderNo == null) {
|
||||
log.info("[checkout.session.expired] 无法提取 orderNo,跳过,sessionId={}", eventId);
|
||||
return true;
|
||||
}
|
||||
|
||||
OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
|
||||
if (orderInfo == null) {
|
||||
log.info("[checkout.session.expired] 订单不存在,跳过,orderNo={}", orderNo);
|
||||
return true;
|
||||
}
|
||||
// todo 确认订单状态是否会更新为失败
|
||||
// 仅处理失败状态的订单
|
||||
if (!com.ai.da.common.enums.OrderStatusEnum.FAILURE.getType().equals(orderInfo.getOrderStatus())) {
|
||||
log.info("[checkout.session.expired] 订单状态非失败,跳过,orderNo={},status={}", orderNo, orderInfo.getOrderStatus());
|
||||
return true;
|
||||
}
|
||||
|
||||
// 检查后续是否有成功的订阅订单
|
||||
List<OrderInfo> laterSuccessOrders = orderInfoService.getBaseMapper().selectList(
|
||||
new QueryWrapper<OrderInfo>()
|
||||
.eq("account_id", orderInfo.getAccountId())
|
||||
.gt("create_time", orderInfo.getCreateTime())
|
||||
.eq("order_status", com.ai.da.common.enums.OrderStatusEnum.SUCCESS.getType())
|
||||
.likeLeft("title", "Subscription")
|
||||
);
|
||||
|
||||
// todo 支付未完成时,不会自动产生订阅类型的回调;这里逻辑待验证
|
||||
if (laterSuccessOrders.isEmpty()) {
|
||||
// 没有后续成功订单,发送失败通知
|
||||
List<SubscriptionInfo> subInfoList = subscriptionInfoMapper.selectList(
|
||||
new QueryWrapper<SubscriptionInfo>()
|
||||
.eq("order_no", orderNo)
|
||||
);
|
||||
// TODO 确认订阅状态是否会更新为未完成或过期未完成
|
||||
if (subInfoList.isEmpty()
|
||||
|| "incomplete".equals(subInfoList.getFirst().getStatus())
|
||||
|| "incomplete_expired".equals(subInfoList.getFirst().getStatus())) {
|
||||
// 首次订阅失败
|
||||
stripeSubscriptionService.sendFailedNewOrderEmail(orderNo);
|
||||
log.info("[checkout.session.expired] 首次订阅失败邮件已发送,orderNo={}", orderNo);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[checkout.session.expired] 处理完成,sessionId={},耗时={}ms",
|
||||
eventId, System.currentTimeMillis() - startTime);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[checkout.session.expired] 处理异常,sessionId={},error={}", eventId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String extractOrderNoFromSession(com.stripe.model.checkout.Session session) {
|
||||
if (session.getMetadata() != null && session.getMetadata().containsKey("orderId")) {
|
||||
return session.getMetadata().get("orderId");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* customer.subscription.updated 事件处理器
|
||||
* 业务场景:订阅计划变更(升级/降级)、订阅属性变更
|
||||
* 业务动作:同步本地订阅信息
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Order(50)
|
||||
public class CustomerSubscriptionUpdateHandler implements StripeEventHandler {
|
||||
|
||||
private static final String EVENT_TYPE = "customer.subscription.updated";
|
||||
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedEventTypes() {
|
||||
return Collections.singletonList(EVENT_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<com.stripe.model.Subscription> getSupportObjectType() {
|
||||
return com.stripe.model.Subscription.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean handle(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
com.stripe.model.Subscription subscription = (com.stripe.model.Subscription) stripeObject;
|
||||
String subscriptionId = subscription.getId();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("[customer.subscription.updated] 开始处理,subscriptionId={}", subscriptionId);
|
||||
|
||||
var subInfoList = subscriptionInfoMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SubscriptionInfo>()
|
||||
.eq("subscription_id", subscriptionId)
|
||||
);
|
||||
|
||||
if (subInfoList.isEmpty()) {
|
||||
log.info("[customer.subscription.updated] 未找到本地订阅记录,跳过,subscriptionId={}", subscriptionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
SubscriptionInfo subscriptionInfo = subInfoList.getFirst();
|
||||
|
||||
String status = subscription.getStatus();
|
||||
if (status != null) {
|
||||
subscriptionInfo.setStatus(mapStripeStatus(status));
|
||||
}
|
||||
|
||||
updateSubscriptionPeriod(subscription, subscriptionInfo);
|
||||
|
||||
Boolean cancelAtPeriodEnd = subscription.getCancelAtPeriodEnd();
|
||||
if (cancelAtPeriodEnd != null && cancelAtPeriodEnd) {
|
||||
subscriptionInfo.setCancelNotified((byte) 0);
|
||||
subscriptionInfo.setNextPayDate("--");
|
||||
}
|
||||
|
||||
subscriptionInfo.setUpdateTime(java.time.LocalDateTime.now());
|
||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
||||
|
||||
log.info("[customer.subscription.updated] 处理完成,subscriptionId={},status={},耗时={}ms",
|
||||
subscriptionId, status, System.currentTimeMillis() - startTime);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[customer.subscription.updated] 处理异常,subscriptionId={},error={}", subscriptionId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSubscriptionPeriod(com.stripe.model.Subscription subscription, SubscriptionInfo subscriptionInfo) {
|
||||
try {
|
||||
var items = subscription.getItems();
|
||||
if (items != null && !items.getData().isEmpty()) {
|
||||
var item = items.getData().getFirst();
|
||||
Long currentPeriodStart = item.getCurrentPeriodStart();
|
||||
Long currentPeriodEnd = item.getCurrentPeriodEnd();
|
||||
if (currentPeriodStart != null) {
|
||||
subscriptionInfo.setCurrentPeriodStart(currentPeriodStart);
|
||||
}
|
||||
if (currentPeriodEnd != null) {
|
||||
subscriptionInfo.setCurrentPeriodEnd(currentPeriodEnd);
|
||||
subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(currentPeriodEnd, "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[customer.subscription.updated] 从 items 获取周期失败,subscriptionId={}", subscription.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private String mapStripeStatus(String stripeStatus) {
|
||||
if (stripeStatus == null) {
|
||||
return "unknown";
|
||||
}
|
||||
return switch (stripeStatus) {
|
||||
case "active" -> "active";
|
||||
case "past_due" -> "past_due";
|
||||
case "canceled" -> "canceled";
|
||||
case "trialing" -> "trialing";
|
||||
case "incomplete" -> "incomplete";
|
||||
case "incomplete_expired" -> "incomplete_expired";
|
||||
case "unpaid" -> "unpaid";
|
||||
default -> stripeStatus;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.PaymentInfo;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.AccountService;
|
||||
import com.ai.da.service.PaymentInfoService;
|
||||
import com.ai.da.service.StripeSubscriptionService;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.InvoiceLineItem;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* invoice.paid 事件处理器
|
||||
* 业务场景:订阅续费支付成功 / 发票已支付
|
||||
* 业务动作:更新订阅有效期、发送续费成功通知
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Order(30)
|
||||
public class InvoicePaidHandler implements StripeEventHandler {
|
||||
|
||||
private static final String EVENT_TYPE = "invoice.paid";
|
||||
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
@Resource
|
||||
private PaymentInfoService paymentInfoService;
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
@Resource
|
||||
private AccountService accountService;
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedEventTypes() {
|
||||
return Collections.singletonList(EVENT_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Invoice> getSupportObjectType() {
|
||||
return Invoice.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean handle(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
Invoice invoice = (Invoice) stripeObject;
|
||||
String invoiceId = invoice.getId();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("[invoice.paid] 开始处理,invoiceId={}", invoiceId);
|
||||
|
||||
String billingReason = invoice.getBillingReason();
|
||||
if (!"subscription_cycle".equals(billingReason)
|
||||
&& !"subscription_update".equals(billingReason)
|
||||
&& !"subscription".equals(billingReason)) {
|
||||
log.info("[invoice.paid] 非订阅续费发票,跳过,invoiceId={},billingReason={}", invoiceId, billingReason);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
String subscriptionId = getSubscriptionByInvoice(invoice);
|
||||
if (subscriptionId == null) {
|
||||
log.info("[invoice.paid] 无法获取订阅ID,跳过,invoiceId={}", invoiceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
SubscriptionInfo subscriptionInfo = findSubscriptionInfo(subscriptionId);
|
||||
if (subscriptionInfo == null) {
|
||||
log.info("[invoice.paid] 未找到本地订阅记录,跳过,subscriptionId={}", subscriptionId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 创建或更新支付记录(来自 PaymentInfoServiceImpl.createOrUpdatePaymentInfoForStripe(Invoice))
|
||||
createOrUpdatePaymentInfo(invoice, subscriptionId);
|
||||
|
||||
// 更新订阅信息
|
||||
updateSubscriptionPeriod(invoice, subscriptionInfo);
|
||||
|
||||
// 更新用户积分、账号到期时间,添加积分详细记录
|
||||
accountService.updateAccountValidity(subscriptionInfo.getAccountId(), subscriptionInfo.getCurrentPeriodEnd());
|
||||
accountService.updateUserRoleAndCredits(subscriptionInfo.getAccountId(), subscriptionInfo.getOrderNo());
|
||||
|
||||
// 发送通知邮件
|
||||
sendRenewalNotification(subscriptionInfo);
|
||||
|
||||
log.info("[invoice.paid] 处理完成,invoiceId={},subscriptionId={},耗时={}ms",
|
||||
invoiceId, subscriptionId, System.currentTimeMillis() - startTime);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[invoice.paid] 处理异常,invoiceId={},error={}", invoiceId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 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;
|
||||
}
|
||||
|
||||
private SubscriptionInfo findSubscriptionInfo(String subscriptionId) {
|
||||
var list = subscriptionInfoMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SubscriptionInfo>()
|
||||
.eq("subscription_id", subscriptionId)
|
||||
);
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
}
|
||||
|
||||
private void updateSubscriptionPeriod(Invoice invoice, SubscriptionInfo subscriptionInfo) {
|
||||
InvoiceLineItem.Period period = invoice.getLines().getData().getFirst().getPeriod();
|
||||
Long periodStart = period.getStart();
|
||||
Long periodEnd = period.getEnd();
|
||||
if (periodStart != null) {
|
||||
subscriptionInfo.setCurrentPeriodStart(periodStart);
|
||||
}
|
||||
if (periodEnd != null) {
|
||||
subscriptionInfo.setCurrentPeriodEnd(periodEnd);
|
||||
subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(periodEnd, "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
||||
}
|
||||
subscriptionInfo.setUpdateTime(java.time.LocalDateTime.now());
|
||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
||||
log.info("[invoice.paid] 订阅有效期已更新,subscriptionId={},periodStart={},periodEnd={}",
|
||||
subscriptionInfo.getSubscriptionId(), periodStart, periodEnd);
|
||||
}
|
||||
|
||||
private void sendRenewalNotification(SubscriptionInfo subscriptionInfo) {
|
||||
try {
|
||||
if (subscriptionInfo.getSubscriptionId() != null) {
|
||||
stripeSubscriptionService.sendSubscriptionEmail(null, "renewal", subscriptionInfo.getOrderNo(), subscriptionInfo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[invoice.paid] 发送续费通知失败,error={}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void createOrUpdatePaymentInfo(Invoice invoice, String subscriptionId) {
|
||||
try {
|
||||
Map<String, String> paymentMethodInfo = paymentInfoService.getPaymentMethodInfo(null, subscriptionId);
|
||||
PaymentInfo paymentInfo = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice, paymentMethodInfo, null);
|
||||
if (paymentInfo != null) {
|
||||
log.info("[invoice.paid] 支付记录已创建/更新,invoiceId={}, paymentId={}, orderNo={}, tradeState={}",
|
||||
invoice.getId(), paymentInfo.getId(), paymentInfo.getOrderNo(), paymentInfo.getTradeState());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[invoice.paid] 创建/更新支付记录失败,invoiceId={}, error={}", invoice.getId(), e.getMessage(), e);
|
||||
throw new RuntimeException("创建/更新支付记录失败: " + invoice.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.ai.da.common.constant.CommonConstant;
|
||||
import com.ai.da.common.utils.DateUtil;
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.PaymentInfo;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.PaymentInfoService;
|
||||
import com.ai.da.service.StripeSubscriptionService;
|
||||
import com.stripe.model.Invoice;
|
||||
import com.stripe.model.InvoiceLineItem;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* invoice.payment_failed 事件处理器
|
||||
* 业务场景:订阅续费扣款失败
|
||||
* 业务动作:更新订阅状态为 past_due、发送续费失败通知
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Order(31)
|
||||
public class InvoicePaymentFailedHandler implements StripeEventHandler {
|
||||
|
||||
private static final String EVENT_TYPE = "invoice.payment_failed";
|
||||
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
@Resource
|
||||
private PaymentInfoService paymentInfoService;
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedEventTypes() {
|
||||
return Collections.singletonList(EVENT_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Invoice> getSupportObjectType() {
|
||||
return Invoice.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean handle(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
Invoice invoice = (Invoice) stripeObject;
|
||||
String invoiceId = invoice.getId();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("[invoice.payment_failed] 开始处理,invoiceId={}", invoiceId);
|
||||
|
||||
String subscriptionId = getSubscriptionByInvoice(invoice);
|
||||
if (subscriptionId == null) {
|
||||
log.info("[invoice.payment_failed] 无法获取订阅ID,跳过,invoiceId={}", invoiceId);
|
||||
return true;
|
||||
}
|
||||
|
||||
SubscriptionInfo subscriptionInfo = findSubscriptionInfo(subscriptionId);
|
||||
if (subscriptionInfo == null) {
|
||||
log.info("[invoice.payment_failed] 未找到本地订阅记录,跳过,subscriptionId={}", subscriptionId);
|
||||
return true;
|
||||
}
|
||||
createOrUpdatePaymentInfo(invoice, subscriptionId);
|
||||
|
||||
InvoiceLineItem.Period period = invoice.getLines().getData().getFirst().getPeriod();
|
||||
Long periodStart = period.getStart();
|
||||
Long periodEnd = period.getEnd();
|
||||
if (periodStart != null) {
|
||||
subscriptionInfo.setCurrentPeriodStart(periodStart);
|
||||
}
|
||||
if (periodEnd != null) {
|
||||
subscriptionInfo.setCurrentPeriodEnd(periodEnd);
|
||||
subscriptionInfo.setNextPayDate(DateUtil.changeTimeStampFormat(periodEnd, "seconds", CommonConstant.TIME_FORMAT_MMM_dd_yyyy_EEEE));
|
||||
}
|
||||
|
||||
if (invoice.getBillingReason().equals("subscription_cycle")) {
|
||||
sendPaymentFailedNotification(subscriptionInfo);
|
||||
}
|
||||
|
||||
subscriptionInfo.setStatus("past_due");
|
||||
subscriptionInfo.setUpdateTime(java.time.LocalDateTime.now());
|
||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
||||
|
||||
log.info("[invoice.payment_failed] 处理完成,invoiceId={},subscriptionId={},耗时={}ms",
|
||||
invoiceId, subscriptionId, System.currentTimeMillis() - startTime);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[invoice.payment_failed] 处理异常,invoiceId={},error={}", invoiceId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 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;
|
||||
}
|
||||
|
||||
private SubscriptionInfo findSubscriptionInfo(String subscriptionId) {
|
||||
var list = subscriptionInfoMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SubscriptionInfo>()
|
||||
.eq("subscription_id", subscriptionId)
|
||||
);
|
||||
return list.isEmpty() ? null : list.get(0);
|
||||
}
|
||||
|
||||
private void sendPaymentFailedNotification(SubscriptionInfo subscriptionInfo) {
|
||||
try {
|
||||
if (subscriptionInfo.getSubscriptionId() != null) {
|
||||
stripeSubscriptionService.sendSubscriptionEmail(null, "fail_renewal", subscriptionInfo.getOrderNo(), subscriptionInfo);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[invoice.payment_failed] 发送通知失败,error={}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void createOrUpdatePaymentInfo(Invoice invoice, String subscriptionId) {
|
||||
try {
|
||||
Map<String, String> paymentMethodInfo = paymentInfoService.getPaymentMethodInfo(null, subscriptionId);
|
||||
PaymentInfo paymentInfo = paymentInfoService.createOrUpdatePaymentInfoForStripe(invoice, paymentMethodInfo, null);
|
||||
if (paymentInfo != null) {
|
||||
log.info("[invoice.payment_failed] 支付记录已创建/更新,invoiceId={}, paymentId={}, orderNo={}, tradeState={}",
|
||||
invoice.getId(), paymentInfo.getId(), paymentInfo.getOrderNo(), paymentInfo.getTradeState());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("[invoice.payment_failed] 创建/更新支付记录失败,invoiceId={}, error={}", invoice.getId(), e.getMessage(), e);
|
||||
throw new RuntimeException("创建/更新支付记录失败: " + invoice.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.ai.da.mapper.primary.entity.RefundInfo;
|
||||
import com.ai.da.service.RefundInfoService;
|
||||
import com.stripe.model.Refund;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Stripe Refund 事件处理器
|
||||
* 支持多事件类型:refund.created / refund.updated / refund.failed
|
||||
*
|
||||
* 业务场景:
|
||||
* - refund.created:退款创建时,在 t_refund_info 表中创建记录
|
||||
* - refund.updated(status=succeeded):退款完成时,更新退款状态,
|
||||
* 并找到该笔退款对应的 invoice,修改 paymentInfo 表中 transactionId 为 invoiceId 的记录,将状态改为 refunded
|
||||
* - refund.failed:退款失败时,修改 t_refund_info 表状态,并邮件通知商家
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Order(60)
|
||||
public class RefundEventHandler implements StripeEventHandler {
|
||||
|
||||
private static final String EVENT_REFUND_CREATED = "refund.created";
|
||||
private static final String EVENT_REFUND_UPDATED = "refund.updated";
|
||||
private static final String EVENT_REFUND_FAILED = "refund.failed";
|
||||
|
||||
@Resource
|
||||
private RefundInfoService refundInfoService;
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedEventTypes() {
|
||||
return Arrays.asList(EVENT_REFUND_CREATED, EVENT_REFUND_UPDATED, EVENT_REFUND_FAILED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<Refund> getSupportObjectType() {
|
||||
return Refund.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handle(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
Refund refund = (Refund) stripeObject;
|
||||
String refundId = refund.getId();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
if (EVENT_REFUND_CREATED.equals(eventType)) {
|
||||
log.info("[refund.created] 开始处理,refundId={}", refundId);
|
||||
RefundInfo refundInfo = refundInfoService.handleRefundCreated(refund);
|
||||
log.info("[refund.created] 处理完成,refundId={},orderNo={},耗时={}ms",
|
||||
refundId, refundInfo != null ? refundInfo.getOrderNo() : "N/A", System.currentTimeMillis() - startTime);
|
||||
|
||||
} else if (EVENT_REFUND_UPDATED.equals(eventType)) {
|
||||
log.info("[refund.updated] 开始处理,refundId={},status={}", refundId, refund.getStatus());
|
||||
String status = refund.getStatus();
|
||||
if ("succeeded".equals(status)) {
|
||||
RefundInfo refundInfo = refundInfoService.handleRefundSucceeded(refund);
|
||||
log.info("[refund.updated] 退款成功处理完成,refundId={},orderNo={},耗时={}ms",
|
||||
refundId, refundInfo != null ? refundInfo.getOrderNo() : "N/A", System.currentTimeMillis() - startTime);
|
||||
} else {
|
||||
// 其他状态(如 pending、canceled 等),仅更新状态
|
||||
RefundInfo refundInfo = refundInfoService.updateRefundStatusForStripe(refund);
|
||||
log.info("[refund.updated] 退款状态已更新,refundId={},status={},耗时={}ms",
|
||||
refundId, status, System.currentTimeMillis() - startTime);
|
||||
}
|
||||
|
||||
} else if (EVENT_REFUND_FAILED.equals(eventType)) {
|
||||
log.info("[refund.failed] 开始处理,refundId={},reason={}", refundId, refund.getFailureReason());
|
||||
RefundInfo refundInfo = refundInfoService.handleRefundFailed(refund);
|
||||
log.info("[refund.failed] 处理完成,refundId={},耗时={}ms",
|
||||
refundId, System.currentTimeMillis() - startTime);
|
||||
} else {
|
||||
log.warn("[RefundEventHandler] 未知事件类型,eventType={}", eventType);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[{}] 处理异常,refundId={},error={}", eventType, refundId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Stripe 事件分发器
|
||||
* 基于策略模式,根据事件类型自动路由到对应的处理器
|
||||
* 使用 Spring 的 @Order 注解控制处理器优先级
|
||||
*/
|
||||
@Component
|
||||
public class StripeEventDispatcher {
|
||||
|
||||
private final Map<String, StripeEventHandler> handlerMap;
|
||||
|
||||
public StripeEventDispatcher(List<StripeEventHandler> handlers) {
|
||||
// 相同 eventType 取优先级最高的 handler(@Order 小的优先)
|
||||
this.handlerMap = Collections.unmodifiableMap(
|
||||
handlers.stream()
|
||||
.sorted(Comparator.comparingInt(h -> {
|
||||
Order order = h.getClass().getAnnotation(Order.class);
|
||||
return order != null ? order.value() : Integer.MAX_VALUE;
|
||||
}))
|
||||
.flatMap(h -> h.getSupportedEventTypes().stream()
|
||||
.filter(et -> et != null && !et.isEmpty())
|
||||
.map(et -> new AbstractMap.SimpleEntry<>(et, h)))
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
Map.Entry::getValue,
|
||||
(existing, replacement) -> existing
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据事件类型分发处理
|
||||
* @param eventType 事件类型
|
||||
* @param stripeObject Stripe 对象
|
||||
* @return true=处理成功,false=处理失败
|
||||
*/
|
||||
public boolean dispatch(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
StripeEventHandler handler = handlerMap.get(eventType);
|
||||
if (handler == null) {
|
||||
return true; // 未注册的事件类型默认成功
|
||||
}
|
||||
|
||||
if (!handler.getSupportObjectType().isInstance(stripeObject)) {
|
||||
return true; // 类型不匹配但不影响业务
|
||||
}
|
||||
|
||||
return handler.handle(eventType, stripeObject);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取已注册的事件类型列表
|
||||
*/
|
||||
public List<String> getRegisteredEventTypes() {
|
||||
return List.copyOf(handlerMap.keySet());
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.stripe.model.StripeObject;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Stripe Webhook 事件处理器接口
|
||||
* 使用策略模式,支持 Spring 自动注册
|
||||
*/
|
||||
public interface StripeEventHandler {
|
||||
|
||||
/**
|
||||
* 获取该处理器支持的所有事件类型
|
||||
*/
|
||||
List<String> getSupportedEventTypes();
|
||||
|
||||
/**
|
||||
* 获取该处理器支持的对象类型
|
||||
*/
|
||||
Class<? extends StripeObject> getSupportObjectType();
|
||||
|
||||
/**
|
||||
* 处理事件
|
||||
* @param eventType 事件类型
|
||||
* @param stripeObject Stripe 对象
|
||||
* @return true=处理成功,false=处理失败(可重试)
|
||||
*/
|
||||
boolean handle(String eventType, StripeObject stripeObject);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package com.ai.da.service.stripe.handler;
|
||||
|
||||
import com.ai.da.common.utils.RedisUtil;
|
||||
import com.ai.da.mapper.primary.SubscriptionInfoMapper;
|
||||
import com.ai.da.mapper.primary.entity.SubscriptionInfo;
|
||||
import com.ai.da.service.StripeSubscriptionService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* customer.subscription.deleted 事件处理器
|
||||
* 业务场景:取消订阅(立即取消)
|
||||
* 业务动作:更新订阅状态、撤销权限、发送通知
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
@Order(40)
|
||||
public class SubscriptionDeletedHandler implements StripeEventHandler {
|
||||
|
||||
private static final String EVENT_TYPE = "customer.subscription.deleted";
|
||||
|
||||
@Resource
|
||||
private StripeSubscriptionService stripeSubscriptionService;
|
||||
@Resource
|
||||
private SubscriptionInfoMapper subscriptionInfoMapper;
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Override
|
||||
public List<String> getSupportedEventTypes() {
|
||||
return Collections.singletonList(EVENT_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<com.stripe.model.Subscription> getSupportObjectType() {
|
||||
return com.stripe.model.Subscription.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean handle(String eventType, com.stripe.model.StripeObject stripeObject) {
|
||||
com.stripe.model.Subscription subscription = (com.stripe.model.Subscription) stripeObject;
|
||||
String eventId = subscription.getId();
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
try {
|
||||
log.info("[customer.subscription.deleted] 开始处理,subscriptionId={}", eventId);
|
||||
|
||||
// 查找本地订阅记录
|
||||
List<SubscriptionInfo> subInfoList = subscriptionInfoMapper.selectList(
|
||||
new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<SubscriptionInfo>()
|
||||
.eq("subscription_id", eventId)
|
||||
);
|
||||
|
||||
if (subInfoList.isEmpty()) {
|
||||
log.info("[customer.subscription.deleted] 取消订阅未找到本地记录,跳过,subscriptionId={}", eventId);
|
||||
return true;
|
||||
}
|
||||
|
||||
SubscriptionInfo subscriptionInfo = subInfoList.getFirst();
|
||||
|
||||
// 发送取消订阅通知邮件
|
||||
if (subscriptionInfo.getCancelNotified() == 0) {
|
||||
boolean sent = stripeSubscriptionService.sendSubscriptionEmail(null, "cancel", subscriptionInfo.getOrderNo(), subscriptionInfo);
|
||||
if (sent) {
|
||||
subscriptionInfo.setCancelNotified((byte) 1);
|
||||
|
||||
log.info("[customer.subscription.deleted] 取消订阅通知已发送,subscriptionId={},accountId={}",
|
||||
eventId, subscriptionInfo.getAccountId());
|
||||
}
|
||||
}
|
||||
String reasonKey = "stripe:cancel:reason:" + subscriptionInfo.getSubscriptionId();
|
||||
String cancelReason = redisUtil.getFromString(reasonKey);
|
||||
subscriptionInfo.setStatus("canceled");
|
||||
subscriptionInfo.setNextPayDate("--");
|
||||
subscriptionInfo.setCancelReason(cancelReason);
|
||||
subscriptionInfo.setUpdateTime(java.time.LocalDateTime.now());
|
||||
subscriptionInfoMapper.updateById(subscriptionInfo);
|
||||
log.info("[customer.subscription.deleted] 处理完成,subscriptionId={},耗时={}ms",
|
||||
eventId, System.currentTimeMillis() - startTime);
|
||||
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("[customer.subscription.deleted] 处理异常,subscriptionId={},error={}", eventId, e.getMessage(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,7 +547,7 @@ public class UploadServiceImpl implements UploadService {
|
||||
/**
|
||||
* 清理过期上传任务(每小时执行一次)
|
||||
*/
|
||||
@Scheduled(fixedDelay = 3600000) // 1小时
|
||||
// @Scheduled(fixedDelay = 3600000) // 1小时
|
||||
public void cleanupExpiredUploads() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
uploadTasks.entrySet().removeIf(entry -> {
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
server.port=5567
|
||||
|
||||
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
#spring.datasource.primary.jdbcUrl=jdbc:mysql://18.167.251.121:33008/aida?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.primary.jdbcUrl=jdbc:mysql://18.167.251.121:33008/aida_back?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
#spring.datasource.primary.jdbcUrl=jdbc:mysql://18.167.251.121:33008/test_aida_3.1?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.primary.username=aida_con
|
||||
spring.datasource.primary.password=123456
|
||||
|
||||
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.secondary.jdbcUrl=jdbc:mysql://18.167.251.121:33008/attribute_retrieval_style?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.secondary.username=aida_con
|
||||
spring.datasource.secondary.password=123456
|
||||
|
||||
#security
|
||||
spring.security.jwtSecret=JWTSECRET
|
||||
spring.security.jwtTokenHeader=Authorization
|
||||
spring.security.jwtTokenPrefix=Bearer-
|
||||
## 24Сʱ
|
||||
spring.security.jwtExpiration=8640000000
|
||||
#spring security权限设置 认证了token还要认证权限 不然报错Full authentication is required to access this resource
|
||||
spring.security.ignorePaths=/,/favicon.ico,/doc.html,/webjars/**,/swagger-resources,/v2/api-docs,\
|
||||
/api/account/**,/api/element/**,/api/python/**,/api/design/**,/api/history/**,/api/library/**,/api/third/party/**,/api/generate/**,/api/workspace/**,/api/classification/**,\
|
||||
/api/product/**,/api/ali-pay/**,/api/order-info/**,/api/paypal/**,/api/credits/**,/api/inquiry/**,/api/tasks/**,/api/python/prepareForSR,/api/alipay-hk/**,/api/portfolio/**,\
|
||||
/api/stripe/**,/api/message/**,/api/tags/**,/notification/**,/api/affiliate/**,/api/project/**,/api/llm/**, /api/subscription_plan/**,/api/global-award/**
|
||||
spring.security.authApi=/auth/login
|
||||
|
||||
|
||||
rsa.private_key=MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A==
|
||||
|
||||
#mybatis
|
||||
mybatis-plus.global-config.banner=false
|
||||
mybatis-plus.mapper-locations=classpath:mapper/*/*.xml
|
||||
mybatis-plus.global-config.db-config.logic-delete-field=isDeleted
|
||||
mybatis-plus.global-config.db-config.logic-delete-value=1
|
||||
mybatis-plus.global-config.db-config.logic-not-delete-value=0
|
||||
|
||||
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
|
||||
|
||||
file.mac.path=~/file/
|
||||
file.linux.path=/workspace/home/aida/file/
|
||||
#linux服务器域名(预览和下载用)
|
||||
file.linuxDomain=https://www.aida.com.hk/download/
|
||||
file.windows.path=D:\\upload\\
|
||||
spring.servlet.multipart.max-file-size = 10MB
|
||||
spring.servlet.multipart.max-request-size= 10MB
|
||||
#访问python服务的ip(对应环境)
|
||||
access.python.ip=http://18.167.251.121
|
||||
access.python.port=9994
|
||||
access.python.generate_sr_port=9994
|
||||
access.python.address=http://18.167.251.121:9994
|
||||
|
||||
minio.endpoint=https://www.minio-api.aida.com.hk
|
||||
minio.accessKey=admin
|
||||
minio.secretKey=Aidlab123123!
|
||||
minio.bucketName.clothing=aida-clothing
|
||||
minio.bucketName.mannequins=aida-mannequins
|
||||
minio.bucketName.results=aida-results
|
||||
minio.bucketName.sysImage=aida-sys-image
|
||||
minio.bucketName.users=aida-users
|
||||
minio.bucketName.collectionElement=aida-collection-element
|
||||
minio.bucketName.gradient=aida-gradient
|
||||
minio.bucketName.modifiedSketch=aida-modified-sketch
|
||||
minio.bucketName.slogan=aida-slogan
|
||||
minio.bucketName.partialDesign=aida-partial-design
|
||||
minio.bucketName.globalAward=global-award
|
||||
redirect_url=http://18.167.251.121:7788
|
||||
|
||||
spring.rabbitmq.host=18.167.251.121
|
||||
spring.rabbitmq.port=5672
|
||||
spring.rabbitmq.username=rabbit
|
||||
spring.rabbitmq.password=123456
|
||||
spring.rabbitmq.virtual-host=/
|
||||
|
||||
spring.data.redis.host=172.31.11.32
|
||||
#spring.data.redis.host=18.167.251.121
|
||||
spring.data.redis.port=6379
|
||||
spring.data.redis.database=1
|
||||
spring.data.redis.password=Aidlab
|
||||
spring.data.redis.lettuce.pool.max-active=8
|
||||
spring.data.redis.lettuce.pool.max-idle=8
|
||||
spring.data.redis.lettuce.pool.min-idle=0
|
||||
spring.data.redis.lettuce.pool.max-wait=5
|
||||
|
||||
redis.key.orderForGenerate=OrderForGenerate
|
||||
redis.key.generateCancelSet=GenerateCancelSet
|
||||
redis.key.generateExceptionMap=Generate:Exception
|
||||
redis.key.resultMap=ResultMap
|
||||
redis.key.orderForSR=OrderForSR
|
||||
redis.key.SRCancelSet=SRCancelSet
|
||||
redis.key.SRExceptionMap=SRExceptionMap
|
||||
redis.key.taskList=TaskList
|
||||
redis.key.credits.pre-deduction=Credits:PreDeduction
|
||||
redis.key.generateResult=Generate:Result
|
||||
redis.key.toProductImageResultKey=ToProductImage:Result
|
||||
redis.key.relightResultKey=Relight:Result
|
||||
redis.key.newPosted=LastViewNewPostedTime
|
||||
redis.key.maximumUserId=CodeCreate:MaximumUserId
|
||||
|
||||
aws.s3.accessKeyId=AKIAVD3OJIMF6UJFLSHZ
|
||||
aws.s3.secretKey=LNIwFFB27/QedtZ+Q/viVUoX9F5x1DbuM8N0DkD8
|
||||
aws.s3.regionName=ap-east-1
|
||||
|
||||
# RabbitMQ Exchange and Queue configurations
|
||||
rabbitmq.queues.generate=generate-queue-dev
|
||||
rabbitmq.queues.sr=SR-queue-dev
|
||||
rabbitmq.queues.srResult=SuperResolution-dev
|
||||
rabbitmq.queues.generateResult=GenerateImage-dev
|
||||
rabbitmq.queues.toProductImageResult=ToProductImage-dev
|
||||
rabbitmq.queues.relightResult=Relight-dev
|
||||
rabbitmq.queues.poseTransform=PoseTransform-dev
|
||||
rabbitmq.exchange.generate=generate-exchange
|
||||
rabbitmq.queues.designBatch=DesignBatch-dev
|
||||
rabbitmq.queues.relightBatch=BatchRelight-dev
|
||||
rabbitmq.queues.toProductImageBatch=BatchToProductImage-dev
|
||||
rabbitmq.queues.poseTransformBatch=BatchPoseTransform-dev
|
||||
rabbitmq.queues.emailRetry=emailRetry-business
|
||||
# 死信队列配置
|
||||
rabbitmq.dead-letter.exchange=dlx.email-retry
|
||||
rabbitmq.dead-letter.queue=dlx.email-retry.queue
|
||||
rabbitmq.dead-letter.routing-key=dlx.email-retry.key
|
||||
|
||||
orderList.link=https://develop.aida.com.hk/home/homePage?order=
|
||||
|
||||
# 0 不发送邮件通知 1 发送邮件通知
|
||||
stripe.webhook.fail.reminder=0
|
||||
# kim test
|
||||
#stripe.paymentMethodConfiguration=pmc_1LywTWH7nPZ8bkrN6FvdCUWG
|
||||
# developer test
|
||||
stripe.paymentMethodConfiguration=pmc_1QIKyq02n1TEydyNKVEYvhW7
|
||||
#thymelea模板配置
|
||||
#控制 Thymeleaf 是否启用模板缓存 生产环境用true,以提高性能
|
||||
spring.thymeleaf.cache=false
|
||||
|
||||
#指定邮件服务器的地址。
|
||||
spring.mail.host=mail.aida.com.hk
|
||||
#指定邮件服务器的端口号。
|
||||
spring.mail.port=465
|
||||
#指定登录邮件服务器的用户名
|
||||
spring.mail.username=info@aida.com.hk
|
||||
#指定登录邮件服务器的密码 / 授权码
|
||||
spring.mail.password=AIdlab@2025
|
||||
spring.mail.default-encoding=UTF-8
|
||||
|
||||
# SSL 配置
|
||||
#启用 SSL 加密
|
||||
spring.mail.properties.mail.smtp.ssl.enable=true
|
||||
#指定 SSL 连接的端口号。通常与 spring.mail.port 一致
|
||||
spring.mail.properties.mail.smtp.socketFactory.port=465
|
||||
|
||||
ALIYUN_API_KEY=sk-dc3f88b7df844fc5a7d3616ebd8a589c
|
||||
DOUBAO_API_KEY=853b3c55-f1dd-406e-a356-64123637f635
|
||||
FREEPIK_API_KEY=FPSX94e5917d376a4facb87dabbaa0319c72
|
||||
|
||||
ollama.url=http://localhost:11434/api/chat
|
||||
|
||||
#google.client.id=29310152396-c44dcsoksjirhn7vbo29p8u8n0sg4qps.apps.googleusercontent.com
|
||||
google.client.id=157095842121-kdd1fdf8m8nudvj9sprstb2k2prnf9e4.apps.googleusercontent.com
|
||||
#google.client.secret=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
|
||||
google.client.secret=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
|
||||
google.redirect.uri=https://develop.api.aida.com.hk/api/third/party/auth/google_callback
|
||||
design.callback.url=https://develop.api.aida.com.hk/api/third/party/receiveDesignResults
|
||||
|
||||
# ===== 分片上传配置 =====
|
||||
|
||||
# 临时文件目录
|
||||
file.upload.temp.dir=temp/uploads
|
||||
|
||||
# 分片大小配置
|
||||
# PDF分片大小:1MB
|
||||
file.upload.chunk.size.pdf=1048576
|
||||
# 视频分片大小:2MB
|
||||
file.upload.chunk.size.video=2097152
|
||||
|
||||
# 文件大小限制
|
||||
# PDF最大文件大小:20MB
|
||||
file.upload.max.size.pdf=20971520
|
||||
# 视频最大文件大小:100MB
|
||||
file.upload.max.size.video=104857600
|
||||
|
||||
# 上传任务过期时间(小时)
|
||||
file.upload.task.expiry.hours=24
|
||||
|
||||
global.award.link=https://aida-global-design-awards.com.hk/contestants?id=
|
||||
|
||||
# merchant email receivers (comma-separated, multiple supported)
|
||||
# dev/local: developer.email 不配置,使用默认值 xupei3360@163.com
|
||||
# prod: 两个都配置
|
||||
merchant.email=
|
||||
developer.email=xupei@code-create.com.hk,yizhang@aidlab.hk
|
||||
@@ -1,187 +0,0 @@
|
||||
server.port=5567
|
||||
|
||||
#datasource
|
||||
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.primary.jdbcUrl=jdbc:mysql://18.167.251.121:3306/aida?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.primary.username=root
|
||||
spring.datasource.primary.password=QWa998345
|
||||
|
||||
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.secondary.jdbcUrl=jdbc:mysql://18.167.251.121:33008/attribute_retrieval_style?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.secondary.username=aida_con
|
||||
spring.datasource.secondary.password=123456
|
||||
|
||||
#security
|
||||
spring.security.jwtSecret=JWTSECRET
|
||||
spring.security.jwtTokenHeader=Authorization
|
||||
spring.security.jwtTokenPrefix=Bearer-
|
||||
## 24Сʱ
|
||||
#spring.security.jwtExpiration=8640000000
|
||||
spring.security.jwtExpiration=604800000
|
||||
#spring security权限设置 认证了token还要认证权限 不然报错Full authentication is required to access this resource
|
||||
spring.security.ignorePaths=/,/favicon.ico,/doc.html,/webjars/**,/swagger-resources,/v2/api-docs,\
|
||||
/api/account/**,/api/element/**,/api/python/**,/api/design/**,/api/history/**,/api/library/**,/api/third/party/**,/api/generate/**,/api/workspace/**,/api/classification/**,\
|
||||
/api/product/**,/api/ali-pay/**,/api/order-info/**,/api/paypal/**,/api/credits/**,/api/inquiry/**,/api/tasks/**,/api/python/prepareForSR,/api/alipay-hk/**,/api/portfolio/**,\
|
||||
/api/stripe/**,/api/message/**,/api/tags/**,/notification/**,/api/affiliate/**,/api/project/**,/api/llm/**, /api/subscription_plan/**,/api/global-award/**
|
||||
spring.security.authApi=/auth/login
|
||||
|
||||
|
||||
rsa.private_key=MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A==
|
||||
|
||||
#mybatis
|
||||
mybatis-plus.global-config.banner=false
|
||||
mybatis-plus.mapper-locations=classpath:mapper/*/*.xml
|
||||
mybatis-plus.global-config.db-config.logic-delete-field=isDeleted
|
||||
mybatis-plus.global-config.db-config.logic-delete-value=1
|
||||
mybatis-plus.global-config.db-config.logic-not-delete-value=0
|
||||
|
||||
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
|
||||
|
||||
file.mac.path=~/file/
|
||||
file.linux.path=/workspace/home/aida/file/
|
||||
#linux服务器域名(预览和下载用)
|
||||
file.linuxDomain=https://www.aida.com.hk/download/
|
||||
file.windows.path=D:\\upload\\
|
||||
spring.servlet.multipart.max-file-size = 10MB
|
||||
spring.servlet.multipart.max-request-size= 10MB
|
||||
#访问python服务的ip(对应环境)
|
||||
access.python.ip=http://18.167.251.121
|
||||
access.python.port=9990
|
||||
access.python.generate_sr_port=9990
|
||||
access.python.address=http://18.167.251.121:9990
|
||||
|
||||
minio.endpoint=https://www.minio-api.aida.com.hk
|
||||
minio.accessKey=admin
|
||||
minio.secretKey=Aidlab123123!
|
||||
minio.bucketName.clothing=aida-clothing
|
||||
minio.bucketName.mannequins=aida-mannequins
|
||||
minio.bucketName.results=aida-results
|
||||
minio.bucketName.sysImage=aida-sys-image
|
||||
minio.bucketName.users=aida-users
|
||||
minio.bucketName.collectionElement=aida-collection-element
|
||||
minio.bucketName.gradient=aida-gradient
|
||||
minio.bucketName.modifiedSketch=aida-modified-sketch
|
||||
minio.bucketName.slogan=aida-slogan
|
||||
minio.bucketName.partialDesign=aida-partial-design
|
||||
minio.bucketName.globalAward=global-award
|
||||
redirect_url=http://18.167.251.121:7788
|
||||
|
||||
spring.rabbitmq.host=18.167.251.121
|
||||
spring.rabbitmq.port=5672
|
||||
spring.rabbitmq.username=rabbit
|
||||
spring.rabbitmq.password=123456
|
||||
spring.rabbitmq.virtual-host=/
|
||||
|
||||
spring.data.redis.host=172.31.11.32
|
||||
#spring.data.redis.host=18.167.251.121
|
||||
spring.data.redis.port=6379
|
||||
spring.data.redis.database=2
|
||||
spring.data.redis.password=Aidlab
|
||||
spring.data.redis.lettuce.pool.max-active=8
|
||||
spring.data.redis.lettuce.pool.max-idle=8
|
||||
spring.data.redis.lettuce.pool.min-idle=0
|
||||
spring.data.redis.lettuce.pool.max-wait=5
|
||||
|
||||
redis.key.orderForGenerate=OrderForGenerate
|
||||
redis.key.generateCancelSet=GenerateCancelSet
|
||||
redis.key.generateExceptionMap=Generate:Exception
|
||||
redis.key.resultMap=ResultMap
|
||||
redis.key.orderForSR=OrderForSR
|
||||
redis.key.SRCancelSet=SRCancelSet
|
||||
redis.key.SRExceptionMap=SRExceptionMap
|
||||
redis.key.taskList=TaskList
|
||||
redis.key.credits.pre-deduction=Credits:PreDeduction
|
||||
redis.key.generateResult=Generate:Result
|
||||
redis.key.toProductImageResultKey=ToProductImage:Result
|
||||
redis.key.relightResultKey=Relight:Result
|
||||
redis.key.newPosted=LastViewNewPostedTime
|
||||
redis.key.maximumUserId=CodeCreate:MaximumUserId
|
||||
|
||||
aws.s3.accessKeyId=AKIAVD3OJIMF6UJFLSHZ
|
||||
aws.s3.secretKey=LNIwFFB27/QedtZ+Q/viVUoX9F5x1DbuM8N0DkD8
|
||||
aws.s3.regionName=ap-east-1
|
||||
|
||||
# RabbitMQ Exchange and Queue configurations
|
||||
rabbitmq.queues.generate=generate-queue-prod
|
||||
rabbitmq.queues.sr=SR-queue-prod
|
||||
rabbitmq.queues.srResult=SuperResolution-prod
|
||||
rabbitmq.queues.generateResult=GenerateImage-prod
|
||||
rabbitmq.queues.toProductImageResult=ToProductImage-prod
|
||||
rabbitmq.queues.relightResult=Relight-prod
|
||||
rabbitmq.queues.poseTransform=PoseTransform-prod
|
||||
rabbitmq.exchange.generate=generate-exchange
|
||||
rabbitmq.queues.designBatch=DesignBatch-dev
|
||||
rabbitmq.queues.relightBatch=BatchRelight-dev
|
||||
rabbitmq.queues.toProductImageBatch=BatchToProductImage-dev
|
||||
rabbitmq.queues.poseTransformBatch=BatchPoseTransform-dev
|
||||
rabbitmq.queues.emailRetry=emailRetry-business
|
||||
# 死信队列配置
|
||||
rabbitmq.dead-letter.exchange=dlx.email-retry
|
||||
rabbitmq.dead-letter.queue=dlx.email-retry.queue
|
||||
rabbitmq.dead-letter.routing-key=dlx.email-retry.key
|
||||
|
||||
orderList.link=https://aida.com.hk/home/homePage?order=
|
||||
|
||||
# 0 不发送邮件通知 1 发送邮件通知
|
||||
stripe.webhook.fail.reminder=1
|
||||
# kim live
|
||||
stripe.paymentMethodConfiguration=pmc_1Qu6yJH7nPZ8bkrNULYnFFPf
|
||||
# kim test
|
||||
#stripe.paymentMethodConfiguration=pmc_1LywTWH7nPZ8bkrN6FvdCUWG
|
||||
# developer test
|
||||
#stripe.paymentMethodConfiguration=pmc_1QIKyq02n1TEydyNKVEYvhW7
|
||||
#thymelea模板配置
|
||||
#控制 Thymeleaf 是否启用模板缓存 生产环境用true,以提高性能
|
||||
spring.thymeleaf.cache=false
|
||||
|
||||
#指定邮件服务器的地址。
|
||||
spring.mail.host=mail.aida.com.hk
|
||||
#指定邮件服务器的端口号。
|
||||
spring.mail.port=465
|
||||
#指定登录邮件服务器的用户名
|
||||
spring.mail.username=info@aida.com.hk
|
||||
#指定登录邮件服务器的密码 / 授权码
|
||||
spring.mail.password=AIdlab@2025
|
||||
spring.mail.default-encoding=UTF-8
|
||||
|
||||
# SSL 配置
|
||||
#启用 SSL 加密
|
||||
spring.mail.properties.mail.smtp.ssl.enable=true
|
||||
#指定 SSL 连接的端口号。通常与 spring.mail.port 一致
|
||||
spring.mail.properties.mail.smtp.socketFactory.port=465
|
||||
|
||||
ALIYUN_API_KEY=sk-dc3f88b7df844fc5a7d3616ebd8a589c
|
||||
DOUBAO_API_KEY=853b3c55-f1dd-406e-a356-64123637f635
|
||||
FREEPIK_API_KEY=FPSX94e5917d376a4facb87dabbaa0319c72
|
||||
|
||||
google.client.id=29310152396-nnsd3h533fld665oguu8ovrt1nukmt46.apps.googleusercontent.com
|
||||
google.client.secret=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
|
||||
google.redirect.uri=https://www.api.aida.com.hk/api/third/party/auth/google_callback
|
||||
design.callback.url=https://api.aida.com.hk/api/third/party/receiveDesignResults
|
||||
|
||||
# ===== 分片上传配置 =====
|
||||
|
||||
# 临时文件目录
|
||||
file.upload.temp.dir=temp/uploads
|
||||
|
||||
# 分片大小配置
|
||||
# PDF分片大小:1MB
|
||||
file.upload.chunk.size.pdf=1048576
|
||||
# 视频分片大小:2MB
|
||||
file.upload.chunk.size.video=2097152
|
||||
|
||||
# 文件大小限制
|
||||
# PDF最大文件大小:20MB
|
||||
file.upload.max.size.pdf=20971520
|
||||
# 视频最大文件大小:100MB
|
||||
file.upload.max.size.video=104857600
|
||||
|
||||
# 上传任务过期时间(小时)
|
||||
file.upload.task.expiry.hours=24
|
||||
|
||||
global.award.link=https://aida-global-design-awards.com.hk/contestants?id=
|
||||
|
||||
# merchant email receivers (comma-separated, multiple supported)
|
||||
# prod: includes merchant email
|
||||
merchant.email=kimwong@code-create.com.hk
|
||||
developer.email=xupei3360@163.com
|
||||
@@ -1,101 +0,0 @@
|
||||
server.port=5567
|
||||
|
||||
#datasource
|
||||
spring.datasource.primary.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.primary.jdbcUrl=jdbc:mysql://18.167.251.121:3306/aida?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.primary.username=root
|
||||
spring.datasource.primary.password=QWa998345
|
||||
|
||||
spring.datasource.secondary.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
spring.datasource.secondary.jdbcUrl=jdbc:mysql://18.167.251.121:33008/attribute_retrieval_new?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
spring.datasource.secondary.username=aida_con
|
||||
spring.datasource.secondary.password=123456
|
||||
|
||||
#security
|
||||
spring.security.jwtSecret=JWTSECRET
|
||||
spring.security.jwtTokenHeader=Authorization
|
||||
spring.security.jwtTokenPrefix=Bearer-
|
||||
## 24Сʱ
|
||||
spring.security.jwtExpiration=8640000000
|
||||
#spring security权限设置 认证了token还要认证权限 不然报错Full authentication is required to access this resource
|
||||
spring.security.ignorePaths=/,/favicon.ico,/doc.html,/webjars/**,/swagger-resources,/v2/api-docs,\
|
||||
/api/account/**,/api/element/**,/api/python/**,/api/design/**,/api/history/**,/api/library/**,/api/third/party/**,/api/generate/**,/api/workspace/**,/api/classification/**,\
|
||||
/api/product/**,/api/ali-pay/**,/api/order-info/**,/api/paypal/**,/api/credits/**,/api/inquiry/**,/api/tasks/**,/api/python/prepareForSR
|
||||
spring.security.authApi=/auth/login
|
||||
|
||||
|
||||
rsa.private_key=MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9pB6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZUBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3tTbklZkD2A==
|
||||
|
||||
#mybatis
|
||||
mybatis-plus.global-config.banner=false
|
||||
mybatis-plus.mapper-locations=classpath:mapper/*/*.xml
|
||||
mybatis-plus.global-config.db-config.logic-delete-field=isDeleted
|
||||
mybatis-plus.global-config.db-config.logic-delete-value=1
|
||||
mybatis-plus.global-config.db-config.logic-not-delete-value=0
|
||||
|
||||
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
|
||||
|
||||
file.mac.path=~/file/
|
||||
file.linux.path=/workspace/home/aida/file/
|
||||
#linux服务器域名(预览和下载用)
|
||||
file.linuxDomain=https://www.aida.com.hk/download/
|
||||
file.windows.path=D:\\upload\\
|
||||
spring.servlet.multipart.max-file-size = 10MB
|
||||
spring.servlet.multipart.max-request-size= 10MB
|
||||
#访问python服务的ip(对应环境)
|
||||
#access.python.ip=http://43.198.80.117
|
||||
access.python.ip=http://18.167.251.121
|
||||
#access.python.ip=http://18.167.251.121:9991/
|
||||
access.python.port=9992
|
||||
access.python.sr=http://18.167.251.121:9994
|
||||
|
||||
minio.endpoint=https://www.minio.aida.com.hk:12024
|
||||
minio.accessKey=admin
|
||||
minio.secretKey=Aidlab123123!
|
||||
minio.bucketName.clothing=aida-clothing
|
||||
minio.bucketName.mannequins=aida-mannequins
|
||||
minio.bucketName.results=aida-results
|
||||
minio.bucketName.sysImage=aida-sys-image
|
||||
minio.bucketName.users=aida-users
|
||||
minio.bucketName.collectionElement=aida-collection-element
|
||||
redirect_url=http://18.167.251.121:7788
|
||||
|
||||
spring.rabbitmq.host=18.167.251.121
|
||||
spring.rabbitmq.port=5672
|
||||
spring.rabbitmq.username=rabbit
|
||||
spring.rabbitmq.password=123456
|
||||
spring.rabbitmq.virtual-host=/
|
||||
|
||||
spring.redis.host=172.31.11.32
|
||||
#spring.redis.host=18.167.251.121
|
||||
spring.redis.port=6379
|
||||
spring.redis.database=1
|
||||
spring.redis.password=Aidlab
|
||||
spring.redis.lettuce.pool.max-active=8
|
||||
spring.redis.lettuce.pool.max-idle=8
|
||||
spring.redis.lettuce.pool.min-idle=0
|
||||
spring.redis.lettuce.pool.max-wait=5
|
||||
|
||||
redis.key.orderForGenerate=OrderForGenerate
|
||||
redis.key.generateCancelSet=GenerateCancelSet
|
||||
redis.key.generateExceptionMap=Generate:Exception
|
||||
redis.key.resultMap=ResultMap
|
||||
redis.key.orderForSR=OrderForSR
|
||||
redis.key.SRCancelSet=SRCancelSet
|
||||
redis.key.SRExceptionMap=SRExceptionMap
|
||||
redis.key.taskList=TaskList
|
||||
redis.key.credits.pre-deduction=Credits:PreDeduction
|
||||
redis.key.generateResult=Generate:Result
|
||||
|
||||
aws.s3.accessKeyId=AKIAVD3OJIMF6UJFLSHZ
|
||||
aws.s3.secretKey=LNIwFFB27/QedtZ+Q/viVUoX9F5x1DbuM8N0DkD8
|
||||
aws.s3.regionName=ap-east-1
|
||||
|
||||
# RabbitMQ Exchange and Queue configurations
|
||||
rabbitmq.queues.generate=generate-queue-test
|
||||
rabbitmq.queues.sr=SR-queue-test
|
||||
rabbitmq.queues.srResult=SuperResolution-test
|
||||
rabbitmq.queues.generateResult=GenerateImage-test
|
||||
rabbitmq.queues.toProductImageResult=ToProductImage-test
|
||||
rabbitmq.queues.relightResult=Relight-test
|
||||
rabbitmq.exchange.generate=generate-exchange
|
||||
@@ -1,8 +0,0 @@
|
||||
#<23><><EFBFBD><EFBFBD>application-test<73>ļ<EFBFBD>(<28><><EFBFBD>Ի<EFBFBD><D4BB><EFBFBD>)
|
||||
#spring.profiles.active=test
|
||||
|
||||
#<23><><EFBFBD><EFBFBD>application-prod<6F>ļ<EFBFBD>(<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
|
||||
#spring.profiles.active=prod
|
||||
|
||||
#<23><><EFBFBD><EFBFBD>application-dev<65>ļ<EFBFBD>(<28><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>)
|
||||
spring.profiles.active=dev
|
||||
114
src/main/resources/application.yml
Normal file
114
src/main/resources/application.yml
Normal file
@@ -0,0 +1,114 @@
|
||||
# ============================================================
|
||||
# aida-back - 本地配置(不区分环境)
|
||||
# 公共配置(DB、Redis、RabbitMQ、MinIO、API Keys 等)由 Nacos 统一管理
|
||||
# 此文件仅包含 back 服务私有的业务配置
|
||||
# ============================================================
|
||||
|
||||
server:
|
||||
port: 10092
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: aida-back
|
||||
|
||||
|
||||
# ---------- MinIO Buckets ----------
|
||||
minio:
|
||||
bucketName:
|
||||
clothing: aida-clothing
|
||||
mannequins: aida-mannequins
|
||||
results: aida-results
|
||||
sysImage: aida-sys-image
|
||||
users: aida-users
|
||||
collectionElement: aida-collection-element
|
||||
gradient: aida-gradient
|
||||
modifiedSketch: aida-modified-sketch
|
||||
slogan: aida-slogan
|
||||
partialDesign: aida-partial-design
|
||||
globalAward: global-award
|
||||
|
||||
# ---------- Redis Keys ----------
|
||||
redis:
|
||||
key:
|
||||
orderForGenerate: OrderForGenerate
|
||||
generateCancelSet: GenerateCancelSet
|
||||
generateExceptionMap: Generate:Exception
|
||||
resultMap: ResultMap
|
||||
orderForSR: OrderForSR
|
||||
SRCancelSet: SRCancelSet
|
||||
SRExceptionMap: SRExceptionMap
|
||||
taskList: TaskList
|
||||
credits:
|
||||
pre-deduction: Credits:PreDeduction
|
||||
generateResult: Generate:Result
|
||||
toProductImageResultKey: ToProductImage:Result
|
||||
relightResultKey: Relight:Result
|
||||
newPosted: LastViewNewPostedTime
|
||||
maximumUserId: CodeCreate:MaximumUserId
|
||||
|
||||
# ---------- RabbitMQ 队列 ----------
|
||||
rabbitmq:
|
||||
queues:
|
||||
generate: generate-queue
|
||||
sr: SR-queue
|
||||
srResult: SuperResolution
|
||||
generateResult: GenerateImage
|
||||
toProductImageResult: ToProductImage
|
||||
relightResult: Relight
|
||||
poseTransform: PoseTransform
|
||||
designBatch: DesignBatch
|
||||
relightBatch: BatchRelight
|
||||
toProductImageBatch: BatchToProductImage
|
||||
poseTransformBatch: BatchPoseTransform
|
||||
emailRetry: emailRetry-business
|
||||
exchange:
|
||||
generate: generate-exchange
|
||||
dead-letter:
|
||||
exchange: dlx.email-retry
|
||||
queue: dlx.email-retry.queue
|
||||
routing-key: dlx.email-retry.key
|
||||
|
||||
# ---------- 第三方服务 ----------
|
||||
orderList:
|
||||
link: https://develop.aida.com.hk/home/homePage?order=
|
||||
|
||||
stripe:
|
||||
webhook:
|
||||
fail:
|
||||
reminder: 0
|
||||
paymentMethodConfiguration: pmc_1QIKyq02n1TEydyNKVEYvhW7
|
||||
|
||||
google:
|
||||
client:
|
||||
id: 157095842121-kdd1fdf8m8nudvj9sprstb2k2prnf9e4.apps.googleusercontent.com
|
||||
secret: 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
|
||||
redirect:
|
||||
uri: https://develop.api.aida.com.hk/api/third/party/auth/google_callback
|
||||
|
||||
redirect:
|
||||
url: http://18.167.251.121:7788
|
||||
|
||||
global:
|
||||
award:
|
||||
link: https://aida-global-design-awards.com.hk/contestants?id=
|
||||
|
||||
# ---------- 文件上传 ----------
|
||||
file:
|
||||
upload:
|
||||
temp:
|
||||
dir: temp/uploads
|
||||
chunk:
|
||||
size:
|
||||
pdf: 1048576
|
||||
video: 2097152
|
||||
max:
|
||||
size:
|
||||
pdf: 20971520
|
||||
video: 104857600
|
||||
task:
|
||||
expiry:
|
||||
hours: 24
|
||||
|
||||
logging:
|
||||
level:
|
||||
com.aida: debug
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user