Compare commits
54 Commits
cf02b59722
...
dev/3.1_re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
c8dc38575a | ||
|
|
c00d906083 | ||
| 4df3f9cc53 | |||
|
|
b0343be544 | ||
|
|
d33cb9f0bf | ||
|
|
40f2735831 | ||
| 01d3806d5f | |||
| 107e4e9771 | |||
|
|
716d720782 | ||
|
|
6b5bacc49b | ||
|
|
409bc7b1fd | ||
|
|
ec6a5df8af | ||
|
|
029b96ae99 | ||
|
|
14002e7331 | ||
| 14dfe2806c | |||
| 798c7b0592 | |||
| 9bd10581f4 | |||
| 1f288fe5e3 | |||
| 72602eb245 | |||
| 983d53268d | |||
| f3aeeb3584 | |||
| 5d3692a204 |
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");
|
||||
// }
|
||||
|
||||
}
|
||||
|
||||
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,7 +10,7 @@ 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天过期
|
||||
|
||||
@@ -18,8 +18,8 @@ public class ModelConstants {
|
||||
|
||||
// 模型名称常量
|
||||
public static final String PRINTBOARD_ADVANCED_T2I = "qwen-image";
|
||||
public static final String MOODBOARD_ADVANCED = "doubao-seedream-3-0-t2i-250415";
|
||||
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-3-0-t2i-250415";
|
||||
public static final String MOODBOARD_ADVANCED = "doubao-seedream-4-5-251128";
|
||||
public static final String PRINTBOARD_HIGH_T2I = "doubao-seedream-4-0-250828-high";
|
||||
public static final String PRINTBOARD_HIGH_I2I = "doubao-seedream-4-0-250828-fast";
|
||||
public static final String PRINTBOARD_ADVANCED_I2I = "doubao-seedream-4-0-250828";
|
||||
public static final String IMAGEN_MODEL = "imagen-4.0-generate-001";
|
||||
|
||||
@@ -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,165 +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/uploads/pdf/init", "/api/global-award/uploads/pdf/chunk", "/api/global-award/uploads/pdf/complete", "/api/global-award/uploads/pdf/status",
|
||||
"/api/global-award/uploads/video/init", "/api/global-award/uploads/video/chunk", "/api/global-award/uploads/video/complete", "/api/global-award/uploads/video/status",
|
||||
"/api/global-award/contestants/save", "/api/global-award/contestants/by-email", "/api/global-award/checkEmail", "/api/global-award/checkCode","/api/global-award/contestants/export",
|
||||
"/api/global-award/contestants/export/files"
|
||||
);
|
||||
|
||||
@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,7 +28,7 @@ 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();
|
||||
@@ -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()){
|
||||
|
||||
@@ -104,7 +104,7 @@ public class PaymentTask {
|
||||
}
|
||||
|
||||
// 定时同步(每分钟一次)
|
||||
@Scheduled(fixedRate = 60000)
|
||||
// @Scheduled(fixedRate = 60000)
|
||||
public void syncLinkViewCountToDB(){
|
||||
affiliateService.syncLinkViewCountToDB();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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客户端实例
|
||||
*/
|
||||
@@ -958,6 +962,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;
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import com.ai.da.common.response.Response;
|
||||
import com.ai.da.model.dto.*;
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.service.GlobalAwardService;
|
||||
import com.ai.da.service.upload.UploadService;
|
||||
import com.ai.da.service.upload.UploadTask;
|
||||
@@ -176,10 +177,25 @@ public class GlobalAwardController {
|
||||
}
|
||||
|
||||
@PostMapping("/contestants/export/files")
|
||||
@ApiOperation(value = "导出参赛者文件到本地", notes = "根据参赛者编号范围导出PDF和视频文件到本地temp/uploads/contestants目录")
|
||||
public Response<Integer> exportContestantFiles(@ApiParam(value = "参赛者文件导出请求", required = true) @RequestBody ContestantExportRequest request) throws Exception {
|
||||
int exportedCount = globalAwardService.exportContestantFiles(request.getMinContestantNumber(), request.getMaxContestantNumber());
|
||||
return Response.success(exportedCount);
|
||||
@ApiOperation(value = "导出参赛者文件为ZIP", notes = "根据参赛者编号范围导出PDF、视频和信息文件为ZIP,直接响应给浏览器")
|
||||
public void exportContestantFiles(@ApiParam(value = "参赛者文件导出请求", required = true) @RequestBody ContestantExportRequest request, HttpServletResponse response) throws Exception {
|
||||
byte[] zipData = globalAwardService.exportContestantFilesAsZip(request.getMinContestantNumber(), request.getMaxContestantNumber());
|
||||
if (zipData.length == 0) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
response.getWriter().write("No contestants found in the specified range.");
|
||||
return;
|
||||
}
|
||||
response.setContentType("application/zip");
|
||||
response.setHeader("Content-Disposition", "attachment; filename=\"contestants.zip\"");
|
||||
response.setContentLength(zipData.length);
|
||||
response.getOutputStream().write(zipData);
|
||||
response.getOutputStream().flush();
|
||||
}
|
||||
|
||||
@GetMapping("/contestants/count")
|
||||
@ApiOperation(value = "查询参赛者总数", notes = "查询数据库中参赛者的总数量和最大参赛者编号")
|
||||
public Response<ContestantCountVO> getContestantCount() {
|
||||
return Response.success(globalAwardService.getContestantCount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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,21 @@
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
@@ -68,6 +68,9 @@ public class Contestant {
|
||||
@TableField("pdf_size")
|
||||
private Long pdfSize;
|
||||
|
||||
@TableField("portfolio_url")
|
||||
private String portfolioUrl;
|
||||
|
||||
@TableField("created_at")
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
17
src/main/java/com/ai/da/model/vo/ContestantCountVO.java
Normal file
17
src/main/java/com/ai/da/model/vo/ContestantCountVO.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package com.ai.da.model.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ContestantCountVO {
|
||||
|
||||
private Long count;
|
||||
|
||||
private Integer maxContestantNumber;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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 org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.Map;
|
||||
@@ -33,12 +34,18 @@ public interface GlobalAwardService {
|
||||
void saveContestantsToLocal() throws Exception;
|
||||
|
||||
/**
|
||||
* 根据参赛者编号范围导出参赛者文件到本地目录
|
||||
* 将参赛者文件打包为 ZIP 并返回字节数组(不落盘,直接响应给浏览器)
|
||||
* @param minContestantNumber 最小参赛者编号
|
||||
* @param maxContestantNumber 最大参赛者编号
|
||||
* @return 导出的参赛者数量
|
||||
* @return ZIP 文件的字节数组
|
||||
*/
|
||||
int exportContestantFiles(Integer minContestantNumber, Integer maxContestantNumber) throws Exception;
|
||||
byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception;
|
||||
|
||||
/**
|
||||
* 查询参赛者总数和最大参赛者编号
|
||||
* @return 参赛者数量和最大参赛者编号
|
||||
*/
|
||||
ContestantCountVO getContestantCount();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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,11 +358,13 @@ 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);
|
||||
return token2;
|
||||
}
|
||||
@@ -614,11 +621,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 +1665,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 +2172,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 +2182,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);
|
||||
|
||||
@@ -3401,35 +3419,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);
|
||||
|
||||
CreditsService creditsService = SpringUtils.getBean(CreditsService.class);
|
||||
// 添加积分变更记录(订单续订时的积分变更也需要记录) todo 重置的记录不太准确
|
||||
creditsService.insertToCreditsDetail(accountId,
|
||||
description + "--Stripe",
|
||||
String.valueOf(productCredits),
|
||||
"positive", 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);
|
||||
}
|
||||
|
||||
@@ -524,7 +524,7 @@ public class CollectionElementServiceImpl extends ServiceImpl<CollectionElementM
|
||||
elementVO.setPrintBoardElements(printBoardElements);
|
||||
if (!CollectionUtils.isEmpty(printBoardIds)) {
|
||||
// 从数据库批量查询printBoard元素
|
||||
printBoardElements = collectionElementMapper.selectBatchIds(printBoardIds);
|
||||
printBoardElements.addAll(collectionElementMapper.selectBatchIds(printBoardIds));
|
||||
// 验证查询结果的完整性
|
||||
if (CollectionUtil.isEmpty(printBoardElements) || printBoardElements.size() != printBoardIds.size()) {
|
||||
throw new BusinessException("get.printBoards.data.is.mismatch");
|
||||
|
||||
@@ -756,7 +756,7 @@ public class DesignServiceImpl extends ServiceImpl<DesignMapper, Design> impleme
|
||||
print.setPosition("[0.0,0.0]");
|
||||
// print.setScale(1d);
|
||||
// todo mark 将print默认scale置为0.3
|
||||
print.setScale(Arrays.toString(new Float[]{0.3f, 0.3f}));
|
||||
print.setScale(Arrays.toString(new Float[]{1.0f, 1.0f}));
|
||||
print.setAngle(0.0);
|
||||
print.setPriority(1);
|
||||
QueryWrapper<CollectionElement> getPrintboardLevel2TypeQw = new QueryWrapper<>();
|
||||
|
||||
@@ -1553,11 +1553,11 @@ public class GenerateServiceImpl extends ServiceImpl<GenerateMapper, Generate> i
|
||||
if (imagePath != null) {
|
||||
requestBuilder.image(finalImagePath1);
|
||||
}
|
||||
if (useModel.equals(ModelConstants.PRINTBOARD_HIGH_I2I)) {
|
||||
if (useModel.equals(ModelConstants.PRINTBOARD_HIGH_I2I)|| useModel.equals(ModelConstants.PRINTBOARD_HIGH_T2I)) {
|
||||
GenerateImagesRequest.OptimizePromptOptions optimizePromptOptions = new GenerateImagesRequest.OptimizePromptOptions();
|
||||
optimizePromptOptions.setMode("fast");
|
||||
requestBuilder.optimizePromptOptions(optimizePromptOptions);
|
||||
//由于PRINTBOARD_HIGH_I2I与PRINTBOARD_ADVANCED_I2I使用模型一致,为了区别积分扣除,PRINTBOARD_HIGH_I2I加入了-fast,但传入模型时需要去掉-fast,用PRINTBOARD_ADVANCED_I2I的常量做替代
|
||||
//由于PRINTBOARD_HIGH_T2I,PRINTBOARD_HIGH_I2I与PRINTBOARD_ADVANCED_I2I使用模型一致,为了区别积分扣除,PRINTBOARD_HIGH_I2I加入了-fast或者-high,但传入模型时需要去掉-fast或者-high,用PRINTBOARD_ADVANCED_I2I的常量做替代
|
||||
requestBuilder.model(ModelConstants.PRINTBOARD_ADVANCED_I2I);
|
||||
}
|
||||
|
||||
@@ -4225,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
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.ai.da.mapper.primary.entity.Notification;
|
||||
import com.ai.da.model.dto.ContestantDTO;
|
||||
import com.ai.da.model.dto.PublishSysNotificationDTO;
|
||||
import com.ai.da.model.vo.CheckOTPVO;
|
||||
import com.ai.da.model.vo.ContestantCountVO;
|
||||
import com.ai.da.service.GlobalAwardService;
|
||||
import com.ai.da.service.MessageCenterService;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
@@ -26,24 +27,22 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.io.FileOutputStream;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
@@ -187,6 +186,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
.videoSize(request.getVideoSize())
|
||||
.pdfSize(request.getPdfSize())
|
||||
.contestantNumber(nextNumber)
|
||||
.portfolioUrl(request.getPortfolioUrl())
|
||||
.createdAt(now)
|
||||
.updatedAt(now)
|
||||
.build();
|
||||
@@ -227,6 +227,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
existing.setVideoDuration(request.getVideoDuration());
|
||||
existing.setVideoSize(request.getVideoSize());
|
||||
existing.setPdfSize(request.getPdfSize());
|
||||
existing.setPortfolioUrl(request.getPortfolioUrl());
|
||||
existing.setUpdatedAt(now);
|
||||
contestantMapper.updateById(existing);
|
||||
resp.put("success", true);
|
||||
@@ -244,7 +245,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
String[] headers = new String[] {
|
||||
"contestantNumber", "email", "firstName", "lastName", "gender", "occupation",
|
||||
"age", "countryRegionCity", "phoneNumber", "designTitle", "designDescription",
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "createdAt", "updatedAt"
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "portfolioUrl", "createdAt", "updatedAt"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell c = header.createCell(i);
|
||||
@@ -265,11 +266,9 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
r.createCell(ci++).setCellValue(cst.getPhoneNumber() == null ? "" : cst.getPhoneNumber());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignTitle() == null ? "" : cst.getDesignTitle());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignDescription() == null ? "" : cst.getDesignDescription());
|
||||
// r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
// r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
// 视频时长(秒)
|
||||
r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoDuration() == null ? "" : cst.getVideoDuration().toString());
|
||||
// 视频大小、PDF 大小:以 MB 导出,保留两位小数
|
||||
if (cst.getVideoSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
@@ -282,6 +281,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
double pMb = cst.getPdfSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", pMb));
|
||||
}
|
||||
r.createCell(ci++).setCellValue(cst.getPortfolioUrl() == null ? "" : cst.getPortfolioUrl());
|
||||
r.createCell(ci++).setCellValue(cst.getCreatedAt() == null ? "" : cst.getCreatedAt().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getUpdatedAt() == null ? "" : cst.getUpdatedAt().toString());
|
||||
}
|
||||
@@ -311,7 +311,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
String[] headers = new String[] {
|
||||
"contestantNumber", "email", "firstName", "lastName", "gender", "occupation",
|
||||
"age", "countryRegionCity", "phoneNumber", "designTitle", "designDescription",
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "createdAt", "updatedAt"
|
||||
"pdfPath", "videoPath", "videoDuration", "videoSizeMB", "pdfSizeMB", "portfolioUrl", "createdAt", "updatedAt"
|
||||
};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell c = header.createCell(i);
|
||||
@@ -332,11 +332,9 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
r.createCell(ci++).setCellValue(cst.getPhoneNumber() == null ? "" : cst.getPhoneNumber());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignTitle() == null ? "" : cst.getDesignTitle());
|
||||
r.createCell(ci++).setCellValue(cst.getDesignDescription() == null ? "" : cst.getDesignDescription());
|
||||
// r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
// r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
// 视频时长(秒)
|
||||
r.createCell(ci++).setCellValue(cst.getPdfPath() == null ? "" : cst.getPdfPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoPath() == null ? "" : cst.getVideoPath());
|
||||
r.createCell(ci++).setCellValue(cst.getVideoDuration() == null ? "" : cst.getVideoDuration().toString());
|
||||
// 视频大小、PDF 大小:以 MB 导出,保留两位小数
|
||||
if (cst.getVideoSize() == null) {
|
||||
r.createCell(ci++).setCellValue("");
|
||||
} else {
|
||||
@@ -349,6 +347,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
double pMb = cst.getPdfSize() / 1024.0 / 1024.0;
|
||||
r.createCell(ci++).setCellValue(String.format("%.2f", pMb));
|
||||
}
|
||||
r.createCell(ci++).setCellValue(cst.getPortfolioUrl() == null ? "" : cst.getPortfolioUrl());
|
||||
r.createCell(ci++).setCellValue(cst.getCreatedAt() == null ? "" : cst.getCreatedAt().toString());
|
||||
r.createCell(ci++).setCellValue(cst.getUpdatedAt() == null ? "" : cst.getUpdatedAt().toString());
|
||||
}
|
||||
@@ -387,6 +386,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
dto.setVideoDuration(existing.getVideoDuration());
|
||||
dto.setPdfSize(existing.getPdfSize());
|
||||
dto.setVideoSize(existing.getVideoSize());
|
||||
dto.setPortfolioUrl(existing.getPortfolioUrl());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -480,7 +480,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int exportContestantFiles(Integer minContestantNumber, Integer maxContestantNumber) throws Exception {
|
||||
public byte[] exportContestantFilesAsZip(Integer minContestantNumber, Integer maxContestantNumber) throws Exception {
|
||||
if (minContestantNumber == null || maxContestantNumber == null) {
|
||||
throw new BusinessException("minContestantNumber and maxContestantNumber are required.");
|
||||
}
|
||||
@@ -488,7 +488,7 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
throw new BusinessException("minContestantNumber cannot be greater than maxContestantNumber.");
|
||||
}
|
||||
|
||||
// 1. 根据contestantNumber范围查询参赛者
|
||||
// 1. 根据 contestantNumber 范围查询参赛者
|
||||
QueryWrapper<Contestant> queryWrapper = new QueryWrapper<>();
|
||||
queryWrapper.lambda()
|
||||
.ge(Contestant::getContestantNumber, minContestantNumber)
|
||||
@@ -498,90 +498,126 @@ public class GlobalAwardServiceImpl implements GlobalAwardService {
|
||||
|
||||
if (contestants.isEmpty()) {
|
||||
log.info("No contestants found in range [{}, {}]", minContestantNumber, maxContestantNumber);
|
||||
return 0;
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
// 2. 创建基础目录
|
||||
String baseDir = uploadDir + "/contestants";
|
||||
Path basePath = Paths.get(baseDir).toAbsolutePath();
|
||||
Files.createDirectories(basePath);
|
||||
log.info("Base directory created: {}", basePath);
|
||||
// 2. 在内存中构建 ZIP
|
||||
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(baos)) {
|
||||
|
||||
int exportedCount = 0;
|
||||
|
||||
// 3. 遍历每个参赛者,下载文件
|
||||
for (Contestant contestant : contestants) {
|
||||
Integer contestantNumber = contestant.getContestantNumber();
|
||||
if (contestantNumber == null) {
|
||||
log.warn("Contestant {} has no contestantNumber, skipping", contestant.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建参赛者文件夹
|
||||
String contestantDir = baseDir + "/" + contestantNumber;
|
||||
Path contestantPath = Paths.get(contestantDir);
|
||||
Files.createDirectories(contestantPath);
|
||||
|
||||
// 下载PDF文件
|
||||
String pdfPath = contestant.getPdfPath();
|
||||
if (StringUtils.isNotBlank(pdfPath)) {
|
||||
try {
|
||||
String fileName = pdfPath.contains("/") ?
|
||||
pdfPath.substring(pdfPath.lastIndexOf("/") + 1) : "design.pdf";
|
||||
downloadFileFromMinio(pdfPath, contestantPath.toString(), "design.pdf");
|
||||
log.info("Downloaded PDF for contestant {}", fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to download PDF for contestant {}: {}", contestantNumber, e.getMessage());
|
||||
for (Contestant contestant : contestants) {
|
||||
Integer contestantNumber = contestant.getContestantNumber();
|
||||
if (contestantNumber == null) {
|
||||
log.warn("Contestant {} has no contestantNumber, skipping", contestant.getId());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 下载视频文件
|
||||
String videoPath = contestant.getVideoPath();
|
||||
if (StringUtils.isNotBlank(videoPath)) {
|
||||
try {
|
||||
// 根据路径判断视频格式
|
||||
String dirPrefix = contestantNumber + "/";
|
||||
|
||||
// 添加 PDF 文件
|
||||
String pdfPath = contestant.getPdfPath();
|
||||
if (StringUtils.isNotBlank(pdfPath)) {
|
||||
addMinioFileToZip(zos, pdfPath, dirPrefix + "design.pdf");
|
||||
}
|
||||
|
||||
// 添加视频文件
|
||||
String videoPath = contestant.getVideoPath();
|
||||
if (StringUtils.isNotBlank(videoPath)) {
|
||||
String fileName = videoPath.contains("/") ?
|
||||
videoPath.substring(videoPath.lastIndexOf("/") + 1) : "video.mp4";
|
||||
downloadFileFromMinio(videoPath, contestantPath.toString(), fileName);
|
||||
log.info("Downloaded video for contestant {}", contestantNumber);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to download video for contestant {}: {}", contestantNumber, e.getMessage());
|
||||
addMinioFileToZip(zos, videoPath, dirPrefix + fileName);
|
||||
}
|
||||
|
||||
// 添加参赛者信息 txt 文件
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("=== Contestant Information ===\n\n");
|
||||
sb.append("ID: ").append(nullSafe(contestant.getId())).append("\n");
|
||||
sb.append("Email: ").append(nullSafe(contestant.getEmail())).append("\n");
|
||||
sb.append("Contestant Number: ").append(contestantNumber).append("\n");
|
||||
sb.append("First Name: ").append(nullSafe(contestant.getFirstName())).append("\n");
|
||||
sb.append("Last Name: ").append(nullSafe(contestant.getLastName())).append("\n");
|
||||
sb.append("Gender: ").append(nullSafe(contestant.getGender())).append("\n");
|
||||
sb.append("Occupation: ").append(nullSafe(contestant.getOccupation())).append("\n");
|
||||
sb.append("Age: ").append(contestant.getAge() != null ? contestant.getAge() : "N/A").append("\n");
|
||||
sb.append("Country/Region/City: ").append(nullSafe(contestant.getCountryRegionCity())).append("\n");
|
||||
sb.append("Phone Number: ").append(nullSafe(contestant.getPhoneNumber())).append("\n");
|
||||
sb.append("Design Title: ").append(nullSafe(contestant.getDesignTitle())).append("\n");
|
||||
sb.append("Design Description: ").append(nullSafe(contestant.getDesignDescription())).append("\n");
|
||||
sb.append("PDF Path: ").append(nullSafe(pdfPath)).append("\n");
|
||||
sb.append("PDF Size (bytes): ").append(contestant.getPdfSize() != null ? contestant.getPdfSize() : "N/A").append("\n");
|
||||
sb.append("Video Path: ").append(nullSafe(videoPath)).append("\n");
|
||||
sb.append("Video Duration (seconds): ").append(contestant.getVideoDuration() != null ? contestant.getVideoDuration() : "N/A").append("\n");
|
||||
sb.append("Video Size (bytes): ").append(contestant.getVideoSize() != null ? contestant.getVideoSize() : "N/A").append("\n");
|
||||
sb.append("Portfolio URL: ").append(nullSafe(contestant.getPortfolioUrl())).append("\n");
|
||||
sb.append("Created At: ").append(contestant.getCreatedAt() != null ? contestant.getCreatedAt() : "N/A").append("\n");
|
||||
sb.append("Updated At: ").append(contestant.getUpdatedAt() != null ? contestant.getUpdatedAt() : "N/A").append("\n");
|
||||
|
||||
ZipEntry infoEntry = new ZipEntry(dirPrefix + "contestant_info.txt");
|
||||
zos.putNextEntry(infoEntry);
|
||||
zos.write(sb.toString().getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
zos.closeEntry();
|
||||
log.info("Added contestant {} info to zip", contestantNumber);
|
||||
}
|
||||
|
||||
exportedCount++;
|
||||
zos.finish();
|
||||
log.info("ZIP built for {} contestants, size: {} bytes", contestants.size(), baos.size());
|
||||
return baos.toByteArray();
|
||||
}
|
||||
|
||||
log.info("Exported {} contestants' files to {}", exportedCount, basePath);
|
||||
return exportedCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从MinIO下载文件到本地
|
||||
* @param minioPath MinIO路径 (格式: bucketName/objectPath)
|
||||
* @param localDir 本地目录
|
||||
* @param fileName 本地文件名
|
||||
* 将 MinIO 文件流式写入 ZIP,不落盘
|
||||
* @param zos ZIP 输出流
|
||||
* @param minioPath MinIO 路径(格式: bucketName/objectPath)
|
||||
* @param entryName ZIP 条目名称
|
||||
*/
|
||||
private void downloadFileFromMinio(String minioPath, String localDir, String fileName) {
|
||||
private void addMinioFileToZip(java.util.zip.ZipOutputStream zos, String minioPath, String entryName) {
|
||||
if (StringUtils.isBlank(minioPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 从路径中提取bucket名称和对象名称
|
||||
int index = minioPath.indexOf("/");
|
||||
if (index == -1) {
|
||||
log.warn("Invalid MinIO path: {}", minioPath);
|
||||
return;
|
||||
}
|
||||
|
||||
String bucketName = minioPath.substring(0, index);
|
||||
String objectName = minioPath.substring(index + 1);
|
||||
|
||||
// 构建本地文件完整路径
|
||||
Path localFilePath = Paths.get(localDir, fileName);
|
||||
try (InputStream in = minioUtil.download(bucketName, objectName)) {
|
||||
ZipEntry entry = new ZipEntry(entryName);
|
||||
zos.putNextEntry(entry);
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = in.read(buffer)) != -1) {
|
||||
zos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
zos.closeEntry();
|
||||
log.info("Added {} to zip ({} bytes)", entryName, entry.getSize());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to add {} to zip: {}", entryName, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 下载文件
|
||||
minioUtil.downloadMinioObjectToLocal(bucketName, objectName, localFilePath.toString());
|
||||
@Override
|
||||
public ContestantCountVO getContestantCount() {
|
||||
long count = contestantMapper.selectCount(null);
|
||||
Integer maxContestantNumber = null;
|
||||
QueryWrapper<Contestant> qMax = new QueryWrapper<>();
|
||||
qMax.isNotNull("contestant_number");
|
||||
qMax.orderByDesc("contestant_number");
|
||||
qMax.last("LIMIT 1");
|
||||
Contestant last = contestantMapper.selectOne(qMax);
|
||||
if (last != null) {
|
||||
maxContestantNumber = last.getContestantNumber();
|
||||
}
|
||||
return ContestantCountVO.builder()
|
||||
.count(count)
|
||||
.maxContestantNumber(maxContestantNumber)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String nullSafe(String value) {
|
||||
return value != null ? value : "N/A";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -451,11 +1060,12 @@ public class StripeServiceImpl implements StripeService {
|
||||
String periodEnd = DateUtil.changeTimeStampFormat(subscriptionInfo.getCurrentPeriodEnd(), "seconds", CommonConstant.TIME_FORMAT_yyyy_MM_dd_HH_mm_ss);
|
||||
|
||||
qwPI.lambda().eq(PaymentInfo::getOrderNo, subscriptionInfo.getOrderNo())
|
||||
.eq(PaymentInfo::getTradeState, "paid")
|
||||
.between(PaymentInfo::getCreateTime, periodStart, periodEnd)
|
||||
.orderByDesc(PaymentInfo::getId);
|
||||
List<PaymentInfo> paymentInfos = paymentInfoMapper.selectList(qwPI);
|
||||
if (paymentInfos.isEmpty()) {
|
||||
log.info("不发送邮件,原因:【根据order_no:{},查询到的paymentInfos为空】", orderNo);
|
||||
log.info("不发送邮件,原因:【根据order_no:{},查询到的成功的paymentInfos为空】", orderNo);
|
||||
return false;
|
||||
}
|
||||
PaymentInfo paymentInfo = paymentInfos.get(0);
|
||||
@@ -563,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;
|
||||
@@ -623,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) {
|
||||
@@ -693,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);
|
||||
@@ -706,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";
|
||||
@@ -918,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,598 +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) {
|
||||
return false;
|
||||
}
|
||||
OrderInfo orderByOrderNo = orderInfoService.getOrderByOrderNo(subscriptionInfo.getOrderNo());
|
||||
if (orderByOrderNo == null) {
|
||||
return false;
|
||||
}
|
||||
Account account = accountMapper.selectById(subscriptionInfo.getAccountId());
|
||||
if (account == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
PaymentInfo paymentInfo = resolvePaymentInfo(subscriptionInfo, orderNo, type);
|
||||
if (paymentInfo == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String resolvedType = resolveEmailType(type, paymentInfo);
|
||||
if (isEmailAlreadySent(subscriptionInfo, resolvedType, paymentInfo)) {
|
||||
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();
|
||||
// 限制当前时间在订阅区间内,避免处理上个周期内的回调而重复发送邮件
|
||||
if (now > passedInfo.getCurrentPeriodStart() && now < passedInfo.getCurrentPeriodEnd()
|
||||
&& "active".equals(passedInfo.getStatus())) {
|
||||
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");
|
||||
}
|
||||
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.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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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,292 +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);
|
||||
|
||||
stripeSubscriptionService.sendSubscriptionEmail(null, "new", orderNo, null);
|
||||
|
||||
log.info("[checkout.session.completed] 邮件通知完成 类型:new");
|
||||
|
||||
} 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,122 +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);
|
||||
} else {
|
||||
// 续费失败 todo 续费不走这里吧?
|
||||
stripeSubscriptionService.sendSubscriptionEmail(null, "fail_renewal", subInfoList.getFirst().getOrderNo(), null);
|
||||
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(), invoice.getPeriodEnd());
|
||||
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(), null);
|
||||
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,184 +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=
|
||||
@@ -1,182 +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=
|
||||
@@ -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
|
||||
34
src/main/resources/bootstrap.yml
Normal file
34
src/main/resources/bootstrap.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
# ============================================================
|
||||
# aida-back - Bootstrap
|
||||
# 通过 NACOS_NAMESPACE 环境变量切换命名空间(dev / test / prod)
|
||||
# 示例:docker run -e NACOS_NAMESPACE=prod ...
|
||||
# ============================================================
|
||||
|
||||
nacos:
|
||||
namespace: dev
|
||||
host: 18.167.251.121:28848
|
||||
username: nacos
|
||||
password: Aidlab123123!
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: aida-back
|
||||
config:
|
||||
import: optional:nacos:aida-public-${nacos.namespace}.yml
|
||||
cloud:
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: ${nacos.host}
|
||||
namespace: ${nacos.namespace}
|
||||
username: ${nacos.username}
|
||||
password: ${nacos.password}
|
||||
# ip: 18.167.251.121
|
||||
port: 10092
|
||||
# ip-type: ipv4
|
||||
# prefer-ip-address: true
|
||||
config:
|
||||
server-addr: ${nacos.host}
|
||||
namespace: ${nacos.namespace}
|
||||
file-extension: yaml
|
||||
username: ${nacos.username}
|
||||
password: ${nacos.password}
|
||||
@@ -62,10 +62,7 @@
|
||||
WHEN p.trade_state IN ( 'paid', 'COMPLETED', 'complete', 'liquidated' ) THEN
|
||||
'Success'
|
||||
WHEN p.trade_state IN ( 'failed', 'expired', 'VOIDED', 'void', 'uncollectible' ) THEN
|
||||
'Fail'
|
||||
WHEN p.trade_state IN ( 'Refunded' ) THEN
|
||||
'Refunded'
|
||||
ELSE 'Pending'
|
||||
'Fail' ELSE 'Pending'
|
||||
END AS status
|
||||
FROM
|
||||
t_payment_info p
|
||||
@@ -89,7 +86,6 @@
|
||||
CASE
|
||||
WHEN p.trade_state IN ('paid', 'COMPLETED', 'complete', 'liquidated') THEN 'Success'
|
||||
WHEN p.trade_state IN ('failed', 'expired', 'VOIDED', 'void', 'uncollectible') THEN 'Fail'
|
||||
WHEN p.trade_state IN ('Refunded') THEN 'Refunded'
|
||||
ELSE 'Pending'
|
||||
END = #{status}
|
||||
</if>
|
||||
@@ -136,7 +132,6 @@
|
||||
CASE
|
||||
WHEN p.trade_state IN ('paid', 'COMPLETED', 'complete', 'liquidated') THEN 'Success'
|
||||
WHEN p.trade_state IN ('failed', 'expired', 'VOIDED', 'void', 'uncollectible') THEN 'Fail'
|
||||
WHEN p.trade_state IN ('Refunded') THEN 'Refunded'
|
||||
ELSE 'Pending'
|
||||
END = #{status}
|
||||
</if>
|
||||
@@ -175,7 +170,6 @@
|
||||
CASE
|
||||
WHEN p.trade_state IN ('paid', 'COMPLETED', 'complete', 'liquidated') THEN 'Success'
|
||||
WHEN p.trade_state IN ('failed', 'expired', 'VOIDED', 'void', 'uncollectible') THEN 'Fail'
|
||||
WHEN p.trade_state IN ('Refunded') THEN 'Refunded'
|
||||
ELSE 'Pending'
|
||||
END = #{status}
|
||||
</if>
|
||||
|
||||
@@ -111,6 +111,7 @@ waistbandRight.cannot.be.empty=waistbandRight cannot be empty.
|
||||
handLeft.cannot.be.empty=handLeft cannot be empty.
|
||||
handRight.cannot.be.empty=handRight cannot be empty.
|
||||
id.cannot.be.empty=id cannot be empty.
|
||||
url.cannot.be.empty=url cannot be empty.
|
||||
type.cannot.be.empty=type cannot be empty.
|
||||
color.cannot.be.empty=color cannot be empty.
|
||||
generateDetailId.cannot.be.empty=generateDetailId cannot be empty.
|
||||
|
||||
@@ -110,6 +110,7 @@ waistbandRight.cannot.be.empty=waistbandRight不能为空。
|
||||
handLeft.cannot.be.empty=handLeft不能为空。
|
||||
handRight.cannot.be.empty=handRight不能为空。
|
||||
id.cannot.be.empty=id不能为空。
|
||||
url.cannot.be.empty=url不能为空。
|
||||
type.cannot.be.empty=type不能为空。
|
||||
color.cannot.be.empty=color不能为空。
|
||||
generateDetailId.cannot.be.empty=generateDetailId不能为空。
|
||||
|
||||
Reference in New Issue
Block a user