Compare commits
25 Commits
fe726537da
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fbfe30af7 | ||
|
|
056f2eca65 | ||
|
|
c2b42476bc | ||
| 07a71fb437 | |||
| 88b40ba4a2 | |||
| b7ad9b57c6 | |||
| d80a42a20d | |||
| 137729161f | |||
| 5d04837d46 | |||
| b0c2889ddf | |||
| 8b37cccb93 | |||
| 887fea1961 | |||
| e11d7b703f | |||
| 12e38067ab | |||
| 2a79fca22c | |||
| 5fb6350763 | |||
| a2a8547bca | |||
| e09987fbbb | |||
| dc17788678 | |||
| f467a1064e | |||
| 2df6ee04bc | |||
| 9f5d9f73a0 | |||
| d2a5416bea | |||
| fbb935ba0c | |||
| 2b6b0e1bb5 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -72,6 +72,7 @@ coverage/
|
||||
*.tmp
|
||||
hs_err_pid*
|
||||
replay_pid*
|
||||
target/
|
||||
|
||||
# ===================== IDE/编辑器配置 =====================
|
||||
# VS Code
|
||||
|
||||
1
broker.conf
Normal file
1
broker.conf
Normal file
@@ -0,0 +1 @@
|
||||
"brokerIP1=127.0.0.1"
|
||||
62
docker-compose-rocketmq.yml
Normal file
62
docker-compose-rocketmq.yml
Normal file
@@ -0,0 +1,62 @@
|
||||
services:
|
||||
rmqnamesrv:
|
||||
image: apache/rocketmq:5.3.2 # 或换成 5.2.0,如果你必须用这个版本
|
||||
container_name: rmqnamesrv
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9876:9876"
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- JAVA_OPT_EXT=-Xms512m -Xmx512m -Xmn256m
|
||||
command: sh mqnamesrv
|
||||
networks:
|
||||
- rmq
|
||||
|
||||
rmqbroker:
|
||||
image: apache/rocketmq:5.3.2
|
||||
container_name: rmqbroker
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "10911:10911" # FastRemoting 端口(客户端主要连接这里)
|
||||
- "10909:10909" # HA 端口(可选)
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- NAMESRV_ADDR=rmqnamesrv:9876
|
||||
- JAVA_OPT_EXT=-Xms1g -Xmx1g -Xmn512m
|
||||
depends_on:
|
||||
- rmqnamesrv
|
||||
command: >
|
||||
sh -c "
|
||||
echo '
|
||||
brokerClusterName = DefaultCluster
|
||||
brokerName = broker-a
|
||||
brokerId = 0
|
||||
deleteWhen = 04
|
||||
fileReservedTime = 48
|
||||
brokerRole = ASYNC_MASTER
|
||||
flushDiskType = ASYNC_FLUSH
|
||||
autoCreateTopicEnable = true
|
||||
autoCreateSubscriptionGroup = true
|
||||
' > /home/rocketmq/rocketmq-5.3.2/conf/broker.conf &&
|
||||
mqbroker -n rmqnamesrv:9876 -c /home/rocketmq/rocketmq-5.3.2/conf/broker.conf
|
||||
"
|
||||
networks:
|
||||
- rmq
|
||||
|
||||
rmqdashboard:
|
||||
image: apacherocketmq/rocketmq-dashboard:latest
|
||||
container_name: rmqdashboard
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "18080:8080" # 主机端口改成 18080,避免与你其他服务冲突
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- JAVA_OPTS=-Drocketmq.namesrv.addr=rmqnamesrv:9876 -Xms512m -Xmx512m
|
||||
depends_on:
|
||||
- rmqnamesrv
|
||||
networks:
|
||||
- rmq
|
||||
|
||||
networks:
|
||||
rmq:
|
||||
driver: bridge
|
||||
46
docker-compose-xxljob.yml
Normal file
46
docker-compose-xxljob.yml
Normal file
@@ -0,0 +1,46 @@
|
||||
services:
|
||||
# MySQL 服务(无数据持久化)
|
||||
mysql:
|
||||
image: mysql:8.0
|
||||
container_name: xxl-job-mysql
|
||||
ports:
|
||||
- "3307:3306"
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=123456
|
||||
- MYSQL_DATABASE=xxl_job
|
||||
- MYSQL_ROOT_HOST=%
|
||||
- TZ=Asia/Shanghai
|
||||
command: --default-authentication-plugin=mysql_native_password
|
||||
restart: always
|
||||
networks:
|
||||
- xxl-job-network
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p123456"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
start_period: 10s
|
||||
|
||||
# XXL-Job 调度中心(核心,无持久化)
|
||||
xxl-job-admin:
|
||||
image: xuxueli/xxl-job-admin:2.4.1
|
||||
container_name: xxl-job-admin
|
||||
ports:
|
||||
- "8085:8080"
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
|
||||
- SPRING_DATASOURCE_USERNAME=root
|
||||
- SPRING_DATASOURCE_PASSWORD=123456
|
||||
- XXL_JOB_ACCESS_TOKEN=default_token
|
||||
- XXL_JOB_LOG_RETENTION_DAYS=30
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
networks:
|
||||
- xxl-job-network
|
||||
|
||||
networks:
|
||||
xxl-job-network:
|
||||
driver: bridge
|
||||
39
javamemories-common/pom.xml
Normal file
39
javamemories-common/pom.xml
Normal file
@@ -0,0 +1,39 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>javamemories-common</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>javamemories-common</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains</groupId>
|
||||
<artifactId>annotations</artifactId>
|
||||
<version>13.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
14
javamemories-common/src/main/java/cn/mayiming/App.java
Normal file
14
javamemories-common/src/main/java/cn/mayiming/App.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package cn.mayiming;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
System.out.println( "Hello World!" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.mayiming.Common.Entity;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Data
|
||||
public class StockDeductDTO {
|
||||
@NotNull
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
private Integer deductNum;
|
||||
}
|
||||
28
javamemories-concurrency/pom.xml
Normal file
28
javamemories-concurrency/pom.xml
Normal file
@@ -0,0 +1,28 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>javamemories-concurrency</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>javamemories-concurrency</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
13
javamemories-concurrency/src/main/java/cn/mayiming/App.java
Normal file
13
javamemories-concurrency/src/main/java/cn/mayiming/App.java
Normal file
@@ -0,0 +1,13 @@
|
||||
package cn.mayiming;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
System.out.println( "Hello World!" );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
/**
|
||||
* Unit test for simple App.
|
||||
*/
|
||||
public class AppTest
|
||||
extends TestCase
|
||||
{
|
||||
/**
|
||||
* Create the test case
|
||||
*
|
||||
* @param testName name of the test case
|
||||
*/
|
||||
public AppTest( String testName )
|
||||
{
|
||||
super( testName );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the suite of tests being tested
|
||||
*/
|
||||
public static Test suite()
|
||||
{
|
||||
return new TestSuite( AppTest.class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigourous Test :-)
|
||||
*/
|
||||
public void testApp()
|
||||
{
|
||||
assertTrue( true );
|
||||
}
|
||||
}
|
||||
58
javamemories-gateway/pom.xml
Normal file
58
javamemories-gateway/pom.xml
Normal file
@@ -0,0 +1,58 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>javamemories-gateway</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>javamemories-gateway</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- 网关 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-gateway</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 注册到 Nacos -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
|
||||
</dependency>
|
||||
<!-- Redis 客户端(可选,增强兼容性) -->
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Sentinel 网关适配依赖(关键) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -3,12 +3,15 @@ package cn.mayiming;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class App
|
||||
public class App
|
||||
{
|
||||
|
||||
public static void main( String[] args )
|
||||
{
|
||||
SpringApplication.run(App.class, args);
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cn.mayiming.Config;
|
||||
|
||||
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 网关限流配置类
|
||||
* 定义基于IP的限流解析器
|
||||
*/
|
||||
@Configuration
|
||||
public class GatewayRateLimitConfig {
|
||||
|
||||
/**
|
||||
* IP限流解析器
|
||||
* 作用:从请求中提取客户端IP作为限流的key
|
||||
*/
|
||||
@Bean
|
||||
public KeyResolver ipKeyResolver() {
|
||||
return exchange -> {
|
||||
// 获取客户端IP地址
|
||||
String ip = getClientIp(exchange);
|
||||
return Mono.just(ip);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理X-Forwarded-For头,获取真实客户端IP(适配反向代理场景)
|
||||
*/
|
||||
private String getClientIp(ServerWebExchange exchange) {
|
||||
String ip = exchange.getRequest().getHeaders().getFirst("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = exchange.getRequest().getHeaders().getFirst("Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = exchange.getRequest().getHeaders().getFirst("WL-Proxy-Client-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
|
||||
}
|
||||
// 处理多个IP的情况(X-Forwarded-For可能包含多个IP,取第一个)
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package cn.mayiming.Config;
|
||||
|
||||
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
|
||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
|
||||
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
|
||||
import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Sentinel 熔断规则配置
|
||||
* 为下游服务配置熔断策略
|
||||
*/
|
||||
@Configuration
|
||||
public class SentinelDegradeConfig {
|
||||
|
||||
/**
|
||||
* 初始化熔断规则
|
||||
* @PostConstruct:Spring 容器启动后执行
|
||||
*/
|
||||
@PostConstruct
|
||||
public void initDegradeRules() {
|
||||
List<DegradeRule> rules = new ArrayList<>();
|
||||
|
||||
DegradeRule orderServiceRule = new DegradeRule();
|
||||
orderServiceRule.setResource("order-service"); // 网关路由ID/服务名
|
||||
// 关键:改为慢请求比例触发熔断
|
||||
|
||||
orderServiceRule.setCount(3000); // 慢请求阈值:3000毫秒(3秒),超过3秒即为慢请求
|
||||
orderServiceRule.setSlowRatioThreshold(0.5); // 慢请求比例阈值:50%(超过50%的请求是慢请求则触发熔断)
|
||||
orderServiceRule.setTimeWindow(10); // 熔断后保持打开状态10秒
|
||||
orderServiceRule.setMinRequestAmount(5); // 最小请求数:累计5次请求后才计算慢请求比例
|
||||
|
||||
rules.add(orderServiceRule);
|
||||
DegradeRuleManager.loadRules(rules);
|
||||
}
|
||||
}
|
||||
91
javamemories-gateway/src/main/resources/application.yml
Normal file
91
javamemories-gateway/src/main/resources/application.yml
Normal file
@@ -0,0 +1,91 @@
|
||||
server:
|
||||
port: 8081
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: gateway-service
|
||||
cloud:
|
||||
gateway:
|
||||
discovery:
|
||||
locator:
|
||||
enabled: true # 自动从 nacos 发现服务
|
||||
routes:
|
||||
# 路由1:user-service
|
||||
- id: user-service
|
||||
uri: lb://user-service # lb = 负载均衡
|
||||
predicates:
|
||||
- Path=/user/**
|
||||
filters:
|
||||
- RewritePath=/user/(?<segment>.*), /${segment}
|
||||
# 全局限流配置
|
||||
- name: RequestRateLimiter
|
||||
args:
|
||||
# 令牌桶填充速率:每秒生成 1 个令牌(即 10 秒 10 个)
|
||||
redis-rate-limiter.replenishRate: 1
|
||||
# 令牌桶最大容量:最多存 10 个令牌(允许突发 10 次请求)
|
||||
redis-rate-limiter.burstCapacity: 10
|
||||
# 按 IP 限流(默认)
|
||||
key-resolver: "#{@ipKeyResolver}"
|
||||
|
||||
# 路由2:order-service
|
||||
- id: order-service
|
||||
uri: lb://order-service
|
||||
predicates:
|
||||
- Path=/order/**
|
||||
filters:
|
||||
- RewritePath=/user/(?<segment>.*), /${segment}
|
||||
# 全局限流配置
|
||||
- name: RequestRateLimiter
|
||||
args:
|
||||
# 令牌桶填充速率:每秒生成 1 个令牌(即 10 秒 10 个)
|
||||
redis-rate-limiter.replenishRate: 1
|
||||
# 令牌桶最大容量:最多存 10 个令牌(允许突发 10 次请求)
|
||||
redis-rate-limiter.burstCapacity: 10
|
||||
# 按 IP 限流(默认)
|
||||
key-resolver: "#{@ipKeyResolver}"
|
||||
|
||||
- id: stock-service
|
||||
uri: lb://stock-service
|
||||
predicates:
|
||||
- Path=/stock/**
|
||||
filters:
|
||||
- RewritePath=/stock/(?<segment>.*), /${segment}
|
||||
# 全局限流配置
|
||||
- name: RequestRateLimiter
|
||||
args:
|
||||
# 令牌桶填充速率:每秒生成 1 个令牌(即 10 秒 10 个)
|
||||
redis-rate-limiter.replenishRate: 1
|
||||
# 令牌桶最大容量:最多存 10 个令牌(允许突发 10 次请求)
|
||||
redis-rate-limiter.burstCapacity: 10
|
||||
# 按 IP 限流(默认)
|
||||
key-resolver: "#{@ipKeyResolver}"
|
||||
|
||||
sentinel:
|
||||
# Sentinel 控制台地址(如果启动了控制台,用于可视化配置)
|
||||
transport:
|
||||
dashboard: localhost:8080 # Sentinel 控制台端口,默认8080
|
||||
port: 8719 # 客户端和控制台通信的端口
|
||||
# 网关熔断配置
|
||||
gateway:
|
||||
enabled: true # 开启 Sentinel 网关适配
|
||||
# 熔断后默认的降级响应
|
||||
fallback:
|
||||
mode: response # 降级方式:返回自定义响应
|
||||
response-status: 503 # 降级响应状态码
|
||||
response-body: "{\"code\":503,\"msg\":\"服务暂时不可用,请稍后重试\",\"data\":null}" # 降级响应体
|
||||
response-content-type: application/json
|
||||
|
||||
# Nacos 注册
|
||||
nacos:
|
||||
discovery:
|
||||
server-addr: localhost:8848 # Nacos 服务地址(默认端口 8848)
|
||||
namespace: public # 命名空间(默认 public,自定义需先在 Nacos 控制台创建)
|
||||
group: DEFAULT_GROUP # 分组(默认 DEFAULT_GROUP)
|
||||
service: gateway-service
|
||||
data:
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
password: ''
|
||||
database: 0
|
||||
timeout: 2000ms
|
||||
38
javamemories-gateway/src/test/java/cn/mayiming/AppTest.java
Normal file
38
javamemories-gateway/src/test/java/cn/mayiming/AppTest.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
/**
|
||||
* Unit test for simple App.
|
||||
*/
|
||||
public class AppTest
|
||||
extends TestCase
|
||||
{
|
||||
/**
|
||||
* Create the test case
|
||||
*
|
||||
* @param testName name of the test case
|
||||
*/
|
||||
public AppTest( String testName )
|
||||
{
|
||||
super( testName );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the suite of tests being tested
|
||||
*/
|
||||
public static Test suite()
|
||||
{
|
||||
return new TestSuite( AppTest.class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigourous Test :-)
|
||||
*/
|
||||
public void testApp()
|
||||
{
|
||||
assertTrue( true );
|
||||
}
|
||||
}
|
||||
90
order-service/pom.xml
Normal file
90
order-service/pom.xml
Normal file
@@ -0,0 +1,90 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>order-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>order-service</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 可选:Nacos 配置管理(从 Nacos 读取配置文件) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<!-- 负载均衡(Feign 内置,但显式引入更清晰) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>3.5.19</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xuxueli</groupId>
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
21
order-service/src/main/java/cn/mayiming/App.java
Normal file
21
order-service/src/main/java/cn/mayiming/App.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableFeignClients
|
||||
@MapperScan("cn.mayiming.Mapper")
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.mayiming.Config;
|
||||
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class XxlJobConfig {
|
||||
|
||||
@Value("${xxl.job.admin.addresses}")
|
||||
private String adminAddresses;
|
||||
|
||||
@Value("${xxl.job.executor.appname}")
|
||||
private String appname;
|
||||
|
||||
@Value("${xxl.job.accessToken}")
|
||||
private String accessToken;
|
||||
|
||||
@Value("${xxl.job.executor.port}")
|
||||
private int port;
|
||||
|
||||
// 初始化XXL-Job执行器
|
||||
@Bean
|
||||
public XxlJobSpringExecutor xxlJobExecutor() {
|
||||
XxlJobSpringExecutor executor = new XxlJobSpringExecutor();
|
||||
executor.setAdminAddresses(adminAddresses);
|
||||
executor.setAppname(appname);
|
||||
executor.setAccessToken(accessToken);
|
||||
executor.setPort(port);
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package cn.mayiming.Consumer;
|
||||
|
||||
import cn.mayiming.Mapper.OrderMapper;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
topic = "pay_success_topic",
|
||||
consumerGroup = "order-pay-success-consumer"
|
||||
)
|
||||
public class PaySuccessConsumer implements RocketMQListener<Map<String, Object>> {
|
||||
|
||||
@Autowired
|
||||
private OrderMapper orderMapper;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void onMessage(Map<String, Object> msg) {
|
||||
try {
|
||||
String orderNo = msg.get("orderNo").toString();
|
||||
System.out.println("========== 订单服务接收支付成功消息 ==========");
|
||||
System.out.println("订单号:" + orderNo);
|
||||
System.out.println("支付金额:" + msg.get("payAmount"));
|
||||
System.out.println("===========================================");
|
||||
|
||||
// 核心:更新订单状态为「已支付」(假设1=已支付)
|
||||
int updateResult = orderMapper.updateOrderStatus(orderNo, 0, 1);
|
||||
if (updateResult == 0) {
|
||||
throw new RuntimeException("更新订单状态失败,订单号:" + orderNo);
|
||||
}
|
||||
System.out.println("订单" + orderNo + "已更新为「已支付」状态");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
System.err.println("消费支付成功消息失败:" + e.getMessage());
|
||||
// 生产环境:抛出异常触发MQ重试,确保订单状态最终一致
|
||||
throw new RuntimeException("消费失败,触发重试", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package cn.mayiming.Controller;
|
||||
|
||||
|
||||
import cn.mayiming.Service.OrderService;
|
||||
import cn.mayiming.entity.Order;
|
||||
import cn.mayiming.entity.Result;
|
||||
import cn.mayiming.entity.StockDeductDTO;
|
||||
import cn.mayiming.entity.User;
|
||||
import com.alibaba.csp.sentinel.annotation.SentinelResource;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/order")
|
||||
public class OrderController {
|
||||
|
||||
@Autowired
|
||||
private OrderService orderService;
|
||||
|
||||
@PostMapping("/search")
|
||||
public User getUserOrder(@RequestBody User user){
|
||||
//throw new RuntimeException("Cuowu ");
|
||||
return orderService.SearchUserbyname(user);
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public List<Order> getUserOrder(@RequestParam Integer id){
|
||||
return orderService.OrderListById(id);
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
public Result<Object> createOrder(@RequestBody Map<String, Object> jsonMap) {
|
||||
// ========== 1. 校验businessId(原有逻辑保留) ==========
|
||||
Object businessIdObj = jsonMap.get("businessId");
|
||||
String businessId = null;
|
||||
if (businessIdObj != null) {
|
||||
businessId = businessIdObj.toString().trim();
|
||||
}
|
||||
if (businessId == null || businessId.isEmpty()) {
|
||||
return Result.fail(400, "businessId不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
// ========== 2. 安全解析参数(原有逻辑保留,仅优化提示) ==========
|
||||
// 2.1 解析userId
|
||||
Number userIdNum = (Number) jsonMap.get("userId");
|
||||
if (userIdNum == null) {
|
||||
return Result.fail(400, "userId不能为空");
|
||||
}
|
||||
int userId = userIdNum.intValue();
|
||||
|
||||
// 2.2 解析stockid
|
||||
Number stockIdNum = (Number) jsonMap.get("stockid");
|
||||
if (stockIdNum == null) {
|
||||
return Result.fail(400, "stockid不能为空");
|
||||
}
|
||||
Long stockId = stockIdNum.longValue();
|
||||
|
||||
// 2.3 解析deductnum
|
||||
Number deductNumNum = (Number) jsonMap.get("deductnum");
|
||||
if (deductNumNum == null) {
|
||||
return Result.fail(400, "deductnum不能为空");
|
||||
}
|
||||
int deductNum = deductNumNum.intValue();
|
||||
|
||||
// ========== 3. 组装DTO ==========
|
||||
StockDeductDTO stockDeductDTO = new StockDeductDTO();
|
||||
stockDeductDTO.setId(stockId);
|
||||
stockDeductDTO.setDeductNum(deductNum);
|
||||
|
||||
// ========== 4. 调用Service(核心改动:接收Map结果,获取订单号) ==========
|
||||
// 注意:原Service返回int,需改为返回Map(包含success、msg、orderNo)
|
||||
Map<String, Object> serviceResult = orderService.createOrder(stockDeductDTO, userId, businessId);
|
||||
|
||||
// 4.1 解析Service返回结果
|
||||
boolean isSuccess = (boolean) serviceResult.get("success");
|
||||
if (!isSuccess) {
|
||||
return Result.fail(500, serviceResult.get("msg").toString());
|
||||
}
|
||||
|
||||
// 4.2 成功:返回订单号(供前端轮询支付链接)
|
||||
String orderNo = serviceResult.get("orderNo").toString();
|
||||
return Result.success(
|
||||
Map.of(
|
||||
"msg", "订单创建成功,已触发支付流程",
|
||||
"orderNo", orderNo, // 核心:返回订单号
|
||||
"tips", "请轮询 /order/detail?orderNo=" + orderNo + " 获取支付链接"
|
||||
)
|
||||
);
|
||||
|
||||
} catch (ClassCastException e) {
|
||||
// 补充类型转换异常处理(前端传参类型错误)
|
||||
e.printStackTrace();
|
||||
return Result.fail(400, "参数类型错误:userId/stockid/deductnum需为数字");
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return Result.fail(500, "订单创建异常:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package cn.mayiming.JobHandler;
|
||||
|
||||
import com.xxl.job.core.handler.annotation.XxlJob;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class executorDemo {
|
||||
@XxlJob("demoJobHandler")
|
||||
public void demoJobHandler() {
|
||||
// 这里写你的业务逻辑,比如:
|
||||
System.out.println("XXL-Job 执行器任务执行成功!当前时间:" + System.currentTimeMillis());
|
||||
|
||||
// 示例:定时订单超时取消、数据同步、日志清理等都可以写在这里
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package cn.mayiming.Mapper;
|
||||
|
||||
|
||||
import cn.mayiming.entity.Order;
|
||||
import feign.Param;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface OrderMapper {
|
||||
|
||||
/**
|
||||
* 根据用户ID查询所有订单
|
||||
* @param userId 用户ID(@Param 注解解决参数名绑定问题)
|
||||
* @return 该用户的所有订单列表(按创建时间倒序)
|
||||
*/
|
||||
@Select("SELECT id, " +
|
||||
"order_no AS orderNo, " +
|
||||
"user_id AS userId, " +
|
||||
"product_name AS productName, " +
|
||||
"product_price AS productPrice, " +
|
||||
"count, " +
|
||||
"total_amount AS totalAmount, " +
|
||||
"status, " +
|
||||
"create_time AS createTime, " +
|
||||
"update_time AS updateTime " +
|
||||
"FROM t_order " +
|
||||
"WHERE user_id = #{userId} " +
|
||||
"ORDER BY create_time DESC")
|
||||
List<Order> selectByUserId(@Param("userId") Integer userId);
|
||||
|
||||
/**
|
||||
* 判断businessId是否存在于订单表中
|
||||
* @param businessId 业务唯一标识(如幂等请求ID/外部订单号等)
|
||||
* @return 存在返回true,不存在返回false
|
||||
*/
|
||||
@Select("SELECT COUNT(1) FROM idempotent_record WHERE business_id = #{businessId}")
|
||||
Integer countByBusinessId(@Param("businessId") String businessId);
|
||||
|
||||
/**
|
||||
* 插入幂等记录(核心:利用唯一索引uk_request_id防重复)
|
||||
* @param businessId 业务唯一标识(UUID)
|
||||
* @param status 处理状态:0-处理中 1-处理成功 2-处理失败
|
||||
* @return 插入成功返回1,失败返回0(唯一索引冲突时)
|
||||
*/
|
||||
@Insert("INSERT INTO idempotent_record (business_id, status) " +
|
||||
"VALUES (#{businessId}, #{status}) " +
|
||||
"ON DUPLICATE KEY UPDATE update_time = CURRENT_TIMESTAMP")
|
||||
Integer insertIdempotentRecord(@Param("businessId") String businessId,
|
||||
@Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 更新幂等记录状态(业务成功/失败后调用)
|
||||
* @param businessId 业务唯一标识(UUID)
|
||||
* @param status 处理状态:1-处理成功 2-处理失败
|
||||
* @return 更新成功返回1,无匹配记录返回0
|
||||
*/
|
||||
@Update("UPDATE idempotent_record SET status = #{status}, update_time = CURRENT_TIMESTAMP " +
|
||||
"WHERE business_id = #{businessId}")
|
||||
Integer updateIdempotentStatus(@Param("businessId") String businessId,
|
||||
@Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 插入订单记录(核心业务操作)
|
||||
* @param order 订单对象
|
||||
* @return 插入成功返回1,失败返回0
|
||||
*/
|
||||
@Insert("INSERT INTO t_order (order_no, user_id, goods_id, order_amount, order_status) " +
|
||||
"VALUES (#{orderNo}, #{userId}, #{goodsId}, #{orderAmount}, #{orderStatus})")
|
||||
Integer insertOrder(Order order);
|
||||
|
||||
// 可选:生成唯一订单号(也可在代码中生成)
|
||||
default String generateOrderNo() {
|
||||
// 简单生成规则:时间戳 + 6位随机数
|
||||
return System.currentTimeMillis() + "" + (int)(Math.random() * 900000 + 100000);
|
||||
}
|
||||
|
||||
// OrderMapper补充更新订单状态方法
|
||||
@Update("UPDATE t_order SET order_status = #{newStatus}, update_time = CURRENT_TIMESTAMP WHERE order_no = #{orderNo} AND order_status = #{oldStatus}")
|
||||
int updateOrderStatus(@Param("orderNo") String orderNo, @Param("oldStatus") Integer oldStatus, @Param("newStatus") Integer newStatus);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package cn.mayiming.Service;
|
||||
|
||||
import cn.mayiming.Mapper.OrderMapper;
|
||||
import cn.mayiming.entity.Order;
|
||||
import cn.mayiming.entity.PayTriggerMsgDTO;
|
||||
import cn.mayiming.entity.StockDeductDTO;
|
||||
import cn.mayiming.entity.User;
|
||||
import cn.mayiming.feign.StockFeignClient;
|
||||
import cn.mayiming.feign.UserFeignClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class OrderService {
|
||||
@Autowired
|
||||
private UserFeignClient userFeignClient;
|
||||
|
||||
@Autowired
|
||||
private OrderMapper orderMapper;
|
||||
|
||||
@Autowired
|
||||
private StockFeignClient stockFeignClient;
|
||||
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
private static final String PAY_TRIGGER_TOPIC = "pay_topic";
|
||||
|
||||
public User SearchUserbyname (User user) {
|
||||
return userFeignClient.selectByUsername(user);
|
||||
}
|
||||
|
||||
public List<Order> OrderListById(Integer id) {
|
||||
User user = userFeignClient.GetUserByid(id);
|
||||
|
||||
if(user == null) {
|
||||
throw new RuntimeException("错误!");
|
||||
}
|
||||
return orderMapper.selectByUserId(id);
|
||||
|
||||
}
|
||||
|
||||
public void sendPaySuccessMessage(String msg) {
|
||||
|
||||
// 2. 发送消息:格式为 "主题:标签"
|
||||
// 同步发送:等待 Broker 确认,返回发送结果(可靠)
|
||||
try {
|
||||
// 参数1:topic:tag(对应消费者的 topic + selectorExpression)
|
||||
// 参数2:消息体(自动序列化)
|
||||
rocketMQTemplate.convertAndSend("pay_topic:pay_success", msg);
|
||||
} catch (Exception e) {
|
||||
// 发送失败处理(如记录日志、重试、告警)
|
||||
System.err.println("消息发送失败:" + ",原因:" + e.getMessage());
|
||||
throw new RuntimeException("消息发送失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Map<String, Object> createOrder(StockDeductDTO stockDeductDTO, int userId, String businessId) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// ========== 原有核心逻辑(幂等+用户+扣库存+插订单)==========
|
||||
Integer count = orderMapper.countByBusinessId(businessId);
|
||||
if (count != null && count > 0) {
|
||||
result.put("success", false);
|
||||
result.put("msg", "重复请求,下单失败");
|
||||
return result;
|
||||
}
|
||||
if (userFeignClient.GetUserByid(userId) == null) {
|
||||
result.put("success", false);
|
||||
result.put("msg", "用户不存在,下单失败");
|
||||
return result;
|
||||
}
|
||||
|
||||
Order order = null; // 声明订单对象,方便后续发送消息
|
||||
try {
|
||||
int stockResult = stockFeignClient.deductstock(stockDeductDTO);
|
||||
if (stockResult == 0) {
|
||||
throw new RuntimeException("库存不足,扣减失败");
|
||||
}
|
||||
|
||||
// 构建并插入订单
|
||||
order = new Order();
|
||||
order.setOrderNo(orderMapper.generateOrderNo());
|
||||
order.setUserId(Long.valueOf(userId));
|
||||
order.setGoodsId(stockDeductDTO.getId());
|
||||
order.setOrderAmount(new BigDecimal("99.99"));
|
||||
order.setOrderStatus(0); // 0-待支付
|
||||
|
||||
Integer orderInsertResult = orderMapper.insertOrder(order);
|
||||
if (orderInsertResult == null || orderInsertResult == 0) {
|
||||
throw new RuntimeException("订单插入失败");
|
||||
}
|
||||
|
||||
// 插入幂等记录
|
||||
Integer idempotentInsertResult = orderMapper.insertIdempotentRecord(businessId, 1);
|
||||
if (idempotentInsertResult == null || idempotentInsertResult == 0) {
|
||||
throw new RuntimeException("幂等记录插入失败");
|
||||
}
|
||||
|
||||
// ========== 核心新增:发送RocketMQ支付触发消息 ==========
|
||||
PayTriggerMsgDTO payMsg = new PayTriggerMsgDTO();
|
||||
payMsg.setOrderNo(order.getOrderNo());
|
||||
payMsg.setUserId(order.getUserId());
|
||||
payMsg.setPayAmount(order.getOrderAmount());
|
||||
payMsg.setBusinessId(businessId);
|
||||
payMsg.setGoodsId(order.getGoodsId());
|
||||
|
||||
// 发送异步消息(本地模拟用同步发送,确保消息投递成功)
|
||||
try {
|
||||
rocketMQTemplate.convertAndSend(PAY_TRIGGER_TOPIC, payMsg);
|
||||
System.out.println("订单" + order.getOrderNo() + "支付触发消息已发送到RocketMQ");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("发送支付触发消息失败", e);
|
||||
}
|
||||
|
||||
// 返回结果(包含订单号,前端后续用订单号调用模拟支付接口)
|
||||
result.put("success", true);
|
||||
result.put("msg", "订单创建成功,已触发支付流程");
|
||||
result.put("orderNo", order.getOrderNo());
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
orderMapper.updateIdempotentStatus(businessId, 2);
|
||||
result.put("success", false);
|
||||
result.put("msg", "下单失败:" + e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
17
order-service/src/main/java/cn/mayiming/entity/Order.java
Normal file
17
order-service/src/main/java/cn/mayiming/entity/Order.java
Normal file
@@ -0,0 +1,17 @@
|
||||
package cn.mayiming.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class Order {
|
||||
private Long id; // 主键ID
|
||||
private String orderNo; // 订单号(唯一)
|
||||
private Long userId; // 用户ID
|
||||
private Long goodsId; // 商品ID(对应库存扣减的stockId)
|
||||
private BigDecimal orderAmount; // 订单金额
|
||||
private Integer orderStatus; // 订单状态:0-待支付 1-已支付 2-已取消
|
||||
private Date createTime; // 创建时间
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.mayiming.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class PayTriggerMsgDTO {
|
||||
private String orderNo; // 订单号(核心关联)
|
||||
private Long userId; // 用户ID
|
||||
private BigDecimal payAmount; // 支付金额
|
||||
private String businessId; // 幂等ID(透传)
|
||||
private Long goodsId; // 商品ID(用于关单恢复库存)
|
||||
}
|
||||
69
order-service/src/main/java/cn/mayiming/entity/Result.java
Normal file
69
order-service/src/main/java/cn/mayiming/entity/Result.java
Normal file
@@ -0,0 +1,69 @@
|
||||
package cn.mayiming.entity;
|
||||
|
||||
/**
|
||||
* 通用接口响应结果类
|
||||
* @param <T> 响应数据类型
|
||||
*/
|
||||
public class Result<T> {
|
||||
// 响应码:200成功,500失败,400参数错误等
|
||||
private Integer code;
|
||||
// 响应消息
|
||||
private String msg;
|
||||
// 响应数据
|
||||
private T data;
|
||||
|
||||
// 静态构造方法(推荐)
|
||||
// 成功(无数据)
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(200, "操作成功", null);
|
||||
}
|
||||
|
||||
// 成功(带数据)
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>(200, "操作成功", data);
|
||||
}
|
||||
|
||||
// 失败
|
||||
public static <T> Result<T> fail(String msg) {
|
||||
return new Result<>(500, msg, null);
|
||||
}
|
||||
|
||||
// 失败(自定义码+消息)
|
||||
public static <T> Result<T> fail(Integer code, String msg) {
|
||||
return new Result<>(code, msg, null);
|
||||
}
|
||||
|
||||
// 构造器、getter/setter
|
||||
public Result() {}
|
||||
|
||||
public Result(Integer code, String msg, T data) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
// getter和setter
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(Integer code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.mayiming.entity;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StockDeductDTO {
|
||||
@NotNull
|
||||
@JsonProperty("id")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@JsonProperty("deductnum")
|
||||
private Integer deductNum;
|
||||
}
|
||||
22
order-service/src/main/java/cn/mayiming/entity/User.java
Normal file
22
order-service/src/main/java/cn/mayiming/entity/User.java
Normal file
@@ -0,0 +1,22 @@
|
||||
package cn.mayiming.entity;
|
||||
|
||||
public class User {
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
String username;
|
||||
String password;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.mayiming.feign;
|
||||
|
||||
|
||||
import cn.mayiming.entity.StockDeductDTO;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
@FeignClient(name = "stock-service")
|
||||
public interface StockFeignClient {
|
||||
@PostMapping("/stock/deduct")
|
||||
int deductstock(@RequestBody StockDeductDTO stockDeductDTO);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.mayiming.feign;
|
||||
|
||||
import cn.mayiming.entity.User;
|
||||
import feign.Param;
|
||||
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;
|
||||
|
||||
@FeignClient(name = "user-service")
|
||||
public interface UserFeignClient {
|
||||
@PostMapping("/user")
|
||||
User selectByUsername(User user);
|
||||
|
||||
@GetMapping("/user")
|
||||
User GetUserByid(@RequestParam("id") Integer id);
|
||||
|
||||
}
|
||||
64
order-service/src/main/resources/application.yml
Normal file
64
order-service/src/main/resources/application.yml
Normal file
@@ -0,0 +1,64 @@
|
||||
server:
|
||||
port: 9092
|
||||
spring:
|
||||
application:
|
||||
name: order-service
|
||||
cloud:
|
||||
nacos:
|
||||
# 服务注册发现配置
|
||||
discovery:
|
||||
server-addr: localhost:8848 # Nacos 服务地址(默认端口 8848)
|
||||
namespace: public # 命名空间(默认 public,自定义需先在 Nacos 控制台创建)
|
||||
group: DEFAULT_GROUP # 分组(默认 DEFAULT_GROUP)
|
||||
service: order-service # 注册到 Nacos 的服务名(建议和子项目 artifactId 一致)
|
||||
# 配置管理配置(如果引入了 config 依赖才需要)
|
||||
config:
|
||||
server-addr: localhost:8848 # 和 discovery 一致
|
||||
file-extension: yaml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
import: nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}?server-addr=${spring.cloud.nacos.config.server-addr}
|
||||
|
||||
datasource:
|
||||
# 数据库驱动类(MySQL 8.x 用 com.mysql.cj.jdbc.Driver,5.x 用 com.mysql.jdbc.Driver)
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# 数据库连接 URL(替换为你的数据库地址、端口、库名,如 user_db)
|
||||
url: jdbc:mysql://rm-f8z6oc5a03331500p8o.mysql.rds.aliyuncs.com:3306/order_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
# 数据库用户名(默认 root,根据实际情况修改)
|
||||
username: root
|
||||
# 数据库密码(替换为你的 MySQL 密码)
|
||||
password: Root123456
|
||||
# 可选:连接池配置(推荐使用 HikariCP,Spring Boot 2.x 默认)
|
||||
hikari:
|
||||
# 连接池最大连接数
|
||||
maximum-pool-size: 10
|
||||
# 连接池最小空闲连接数
|
||||
minimum-idle: 2
|
||||
# 连接超时时间(毫秒)
|
||||
connection-timeout: 30000
|
||||
# 连接最大存活时间(毫秒)
|
||||
max-lifetime: 1800000
|
||||
rocketmq:
|
||||
# NameServer 地址(替换为你的 RocketMQ 服务器IP)
|
||||
name-server: 127.0.0.1:9876
|
||||
producer:
|
||||
# 生产者组名(必须唯一,建议:服务名+producer)
|
||||
group: pay-service-producer
|
||||
# 发送失败重试次数(默认2次)
|
||||
retry-times-when-send-failed: 3
|
||||
# 发送超时时间(默认3000ms)
|
||||
send-message-timeout: 5000
|
||||
# 消息最大长度(默认4M)
|
||||
max-message-size: 4194304
|
||||
# 压缩消息阈值(超过4K自动压缩)
|
||||
compress-message-body-threshold: 4096
|
||||
|
||||
xxl:
|
||||
job:
|
||||
admin:
|
||||
addresses: http://localhost:8085/xxl-job-admin # 调度中心访问地址(端口按你实际配置的来)
|
||||
executor:
|
||||
appname: xxl-job-executor-demo # 必须和控制台创建的执行器AppName完全一致!
|
||||
port: 9999 # 执行器端口(默认9999,可改)
|
||||
accessToken: default_token # 和调度中心的token一致(默认default_token)
|
||||
38
order-service/src/test/java/cn/mayiming/AppTest.java
Normal file
38
order-service/src/test/java/cn/mayiming/AppTest.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
/**
|
||||
* Unit test for simple App.
|
||||
*/
|
||||
public class AppTest
|
||||
extends TestCase
|
||||
{
|
||||
/**
|
||||
* Create the test case
|
||||
*
|
||||
* @param testName name of the test case
|
||||
*/
|
||||
public AppTest( String testName )
|
||||
{
|
||||
super( testName );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the suite of tests being tested
|
||||
*/
|
||||
public static Test suite()
|
||||
{
|
||||
return new TestSuite( AppTest.class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigourous Test :-)
|
||||
*/
|
||||
public void testApp()
|
||||
{
|
||||
assertTrue( true );
|
||||
}
|
||||
}
|
||||
90
pay-service/pom.xml
Normal file
90
pay-service/pom.xml
Normal file
@@ -0,0 +1,90 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>pay-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>pay-service</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 可选:Nacos 配置管理(从 Nacos 读取配置文件) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<!-- 负载均衡(Feign 内置,但显式引入更清晰) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>3.5.19</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-client</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
21
pay-service/src/main/java/cn/mayiming/App.java
Normal file
21
pay-service/src/main/java/cn/mayiming/App.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableTransactionManagement
|
||||
@MapperScan("cn.mayiming.Mapper")
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package cn.mayiming.Consumer;
|
||||
|
||||
import cn.mayiming.Entity.PayTriggerMsgDTO;
|
||||
import cn.mayiming.Service.PayService;
|
||||
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
|
||||
import org.apache.rocketmq.spring.core.RocketMQListener;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RocketMQMessageListener(
|
||||
topic = "pay_topic",
|
||||
consumerGroup = "pay-service-consumer",
|
||||
selectorExpression = "*"
|
||||
)
|
||||
public class payConsumer implements RocketMQListener<PayTriggerMsgDTO> {
|
||||
|
||||
@Autowired
|
||||
private PayService payService;
|
||||
|
||||
@Override
|
||||
public void onMessage(PayTriggerMsgDTO msg) {
|
||||
// 消费者接收到消息,模拟支付流程(本地仅生成支付链接,记录到日志/订单)
|
||||
try {
|
||||
System.out.println("========== 接收到支付触发消息 ==========");
|
||||
System.out.println("订单号:" + msg.getOrderNo());
|
||||
System.out.println("用户ID:" + msg.getUserId());
|
||||
System.out.println("支付金额:" + msg.getPayAmount());
|
||||
System.out.println("模拟支付链接:http://localhost:8080/order/mock/pay?orderNo=" + msg.getOrderNo());
|
||||
System.out.println("=======================================");
|
||||
|
||||
// 核心:调用PayService处理(写入幂等记录+支付记录)
|
||||
payService.handlePayTrigger(msg);
|
||||
|
||||
// 生成模拟支付链接(包含订单号+幂等ID,供前端调用)
|
||||
String mockPayUrl = "http://localhost:8080/pay/mock/success?orderNo=" + msg.getOrderNo() + "&businessId=" + msg.getBusinessId();
|
||||
System.out.println("模拟支付链接:" + mockPayUrl);
|
||||
System.out.println("=============================================");
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// 本地模拟:消息消费失败可记录日志,生产环境需配置重试
|
||||
System.err.println("消费支付触发消息失败,订单号:" + msg.getOrderNo() + ",原因:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package cn.mayiming.Controller;
|
||||
|
||||
import cn.mayiming.Entity.checkpayDTO;
|
||||
import cn.mayiming.Service.PayService;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/pay")
|
||||
public class payController {
|
||||
@Autowired
|
||||
PayService payService;
|
||||
|
||||
@PostMapping("/paycheck")
|
||||
public Map<String, Object> checkpay(@RequestBody checkpayDTO checkpayDTO) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
boolean isSuccess = payService.isPaySuccess(checkpayDTO.getOrderno());
|
||||
result.put("success", true);
|
||||
result.put("orderNo", checkpayDTO.getOrderno());
|
||||
result.put("isPaySuccess", isSuccess);
|
||||
result.put("msg", isSuccess ? "该订单已支付成功" : "该订单未支付或不存在");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.mayiming.Entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class IdempotentRecord {
|
||||
private Long id;
|
||||
private String requestId; // 对应表的request_id(前端唯一请求ID/订单号)
|
||||
private Integer status; // 0-处理中 1-处理成功 2-处理失败
|
||||
private Date createTime;
|
||||
private Date updateTime;
|
||||
}
|
||||
15
pay-service/src/main/java/cn/mayiming/Entity/PayRecord.java
Normal file
15
pay-service/src/main/java/cn/mayiming/Entity/PayRecord.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package cn.mayiming.Entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class PayRecord {
|
||||
private Long id;
|
||||
private String orderNo; // 关联订单号
|
||||
private BigDecimal payAmount; // 支付金额
|
||||
private Integer payStatus; // 0-未支付 1-支付成功
|
||||
private Date createTime;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.mayiming.Entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Data
|
||||
public class PayTriggerMsgDTO {
|
||||
private String orderNo; // 订单号(核心关联)
|
||||
private Long userId; // 用户ID
|
||||
private BigDecimal payAmount; // 支付金额
|
||||
private String businessId; // 幂等ID(透传)
|
||||
private Long goodsId; // 商品ID(用于关单恢复库存)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package cn.mayiming.Entity;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class checkpayDTO {
|
||||
@NotNull
|
||||
private String orderno;
|
||||
}
|
||||
82
pay-service/src/main/java/cn/mayiming/Mapper/payMapper.java
Normal file
82
pay-service/src/main/java/cn/mayiming/Mapper/payMapper.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package cn.mayiming.Mapper;
|
||||
|
||||
import cn.mayiming.Entity.IdempotentRecord;
|
||||
import cn.mayiming.Entity.PayRecord;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Repository
|
||||
public interface payMapper {
|
||||
|
||||
// ==================== 幂等记录(idempotent_record)相关操作 ====================
|
||||
/**
|
||||
* 根据requestId查询幂等记录
|
||||
* @param requestId 前端唯一请求ID(对应businessId)
|
||||
* @return 幂等记录对象
|
||||
*/
|
||||
@Select("SELECT id, request_id AS requestId, status, create_time AS createTime, update_time AS updateTime " +
|
||||
"FROM idempotent_record WHERE request_id = #{requestId}")
|
||||
IdempotentRecord selectIdempotentByRequestId(@Param("requestId") String requestId);
|
||||
|
||||
/**
|
||||
* 插入幂等记录(初始状态:0-处理中)
|
||||
* @param requestId 唯一请求ID
|
||||
* @param status 处理状态
|
||||
* @return 插入成功返回1,失败返回0
|
||||
*/
|
||||
@Insert("INSERT INTO idempotent_record (request_id, status, create_time, update_time) " +
|
||||
"VALUES (#{requestId}, #{status}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)")
|
||||
int insertIdempotentRecord(@Param("requestId") String requestId, @Param("status") Integer status);
|
||||
|
||||
/**
|
||||
* 更新幂等记录状态
|
||||
* @param requestId 唯一请求ID
|
||||
* @param status 新状态(1-成功/2-失败)
|
||||
* @return 更新成功返回1,无匹配记录返回0
|
||||
*/
|
||||
@Update("UPDATE idempotent_record SET status = #{status}, update_time = CURRENT_TIMESTAMP " +
|
||||
"WHERE request_id = #{requestId}")
|
||||
int updateIdempotentStatus(@Param("requestId") String requestId, @Param("status") Integer status);
|
||||
|
||||
// ==================== 支付记录(t_pay_record)相关操作 ====================
|
||||
/**
|
||||
* 根据订单号查询支付记录
|
||||
* @param orderNo 订单号
|
||||
* @return 支付记录对象
|
||||
*/
|
||||
@Select("SELECT id, order_no AS orderNo, pay_amount AS payAmount, pay_status AS payStatus, create_time AS createTime " +
|
||||
"FROM t_pay_record WHERE order_no = #{orderNo}")
|
||||
PayRecord selectPayRecordByOrderNo(@Param("orderNo") String orderNo);
|
||||
|
||||
/**
|
||||
* 插入支付记录(初始状态:0-未支付)
|
||||
* @param orderNo 订单号
|
||||
* @param payAmount 支付金额
|
||||
* @param payStatus 支付状态
|
||||
* @return 插入成功返回1,失败返回0
|
||||
*/
|
||||
@Insert("INSERT INTO t_pay_record (order_no, pay_amount, pay_status, create_time) " +
|
||||
"VALUES (#{orderNo}, #{payAmount}, #{payStatus}, CURRENT_TIMESTAMP)")
|
||||
int insertPayRecord(@Param("orderNo") String orderNo,
|
||||
@Param("payAmount") BigDecimal payAmount,
|
||||
@Param("payStatus") Integer payStatus);
|
||||
|
||||
/**
|
||||
* 更新支付记录状态(模拟支付成功时调用)
|
||||
* @param orderNo 订单号
|
||||
* @param payStatus 新状态(1-支付成功)
|
||||
* @return 更新成功返回1,无匹配记录返回0
|
||||
*/
|
||||
@Update("UPDATE t_pay_record SET pay_status = #{payStatus}, create_time = CURRENT_TIMESTAMP " +
|
||||
"WHERE order_no = #{orderNo} AND pay_status = 0")
|
||||
int updatePayRecordStatus(@Param("orderNo") String orderNo, @Param("payStatus") Integer payStatus);
|
||||
|
||||
|
||||
@Select("SELECT COUNT(1) FROM t_pay_record WHERE order_no = #{orderNo} AND pay_status = 1")
|
||||
int checkPaySuccessByOrderNo(@Param("orderNo") String orderNo);
|
||||
}
|
||||
140
pay-service/src/main/java/cn/mayiming/Service/PayService.java
Normal file
140
pay-service/src/main/java/cn/mayiming/Service/PayService.java
Normal file
@@ -0,0 +1,140 @@
|
||||
package cn.mayiming.Service;
|
||||
|
||||
import cn.mayiming.Entity.IdempotentRecord;
|
||||
import cn.mayiming.Entity.PayRecord;
|
||||
import cn.mayiming.Entity.PayTriggerMsgDTO;
|
||||
import cn.mayiming.Mapper.payMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.rocketmq.spring.core.RocketMQTemplate;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class PayService {
|
||||
// 替换为你实际的 payMapper(整合了幂等+支付记录操作)
|
||||
@Autowired
|
||||
private payMapper payMapper;
|
||||
|
||||
|
||||
@Resource
|
||||
private RocketMQTemplate rocketMQTemplate;
|
||||
|
||||
private static final String PAY_SUCCESS_TOPIC = "pay_success_topic";
|
||||
|
||||
|
||||
/**
|
||||
* 处理订单支付触发消息(核心:事务+幂等)
|
||||
* @param msg 订单服务发送的支付触发消息
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void handlePayTrigger(PayTriggerMsgDTO msg) {
|
||||
// 1. 幂等校验:用businessId作为requestId(唯一标识)
|
||||
String requestId = msg.getBusinessId();
|
||||
IdempotentRecord existRecord = payMapper.selectIdempotentByRequestId(requestId);
|
||||
if (existRecord != null) {
|
||||
// 已存在幂等记录,直接返回(避免重复处理)
|
||||
System.out.println("幂等记录已存在,requestId:" + requestId + ",无需重复处理");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 2. 写入幂等记录(初始状态:0-处理中)
|
||||
int insertIdempotentResult = payMapper.insertIdempotentRecord(requestId, 0);
|
||||
if (insertIdempotentResult == 0) {
|
||||
throw new RuntimeException("幂等记录插入失败,requestId:" + requestId);
|
||||
}
|
||||
|
||||
// 3. 校验支付记录是否已存在(双重幂等保障)
|
||||
PayRecord existPayRecord = payMapper.selectPayRecordByOrderNo(msg.getOrderNo());
|
||||
if (existPayRecord != null) {
|
||||
// 更新幂等记录为成功,返回
|
||||
payMapper.updateIdempotentStatus(requestId, 1);
|
||||
System.out.println("支付记录已存在,订单号:" + msg.getOrderNo());
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 写入支付记录(初始状态:0-未支付)
|
||||
BigDecimal payAmount = msg.getPayAmount() == null ? new BigDecimal("0.00") : msg.getPayAmount();
|
||||
int insertPayResult = payMapper.insertPayRecord(msg.getOrderNo(), payAmount, 0);
|
||||
if (insertPayResult == 0) {
|
||||
throw new RuntimeException("支付记录插入失败,订单号:" + msg.getOrderNo());
|
||||
}
|
||||
|
||||
// 5. 所有操作成功,更新幂等记录为处理成功
|
||||
payMapper.updateIdempotentStatus(requestId, 1);
|
||||
System.out.println("支付记录创建成功,订单号:" + msg.getOrderNo() + ",幂等ID:" + requestId);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// 6. 操作失败,更新幂等记录为处理失败
|
||||
try {
|
||||
payMapper.updateIdempotentStatus(requestId, 2);
|
||||
} catch (Exception ex) {
|
||||
System.err.println("更新幂等记录为失败状态失败,requestId:" + requestId);
|
||||
}
|
||||
throw new RuntimeException("处理支付触发消息失败,订单号:" + msg.getOrderNo(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟支付成功(更新支付记录状态)
|
||||
* @param orderNo 订单号
|
||||
* @param businessId 幂等ID
|
||||
* @return 操作结果
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean mockPaySuccess(String orderNo, String businessId) {
|
||||
// 1. 原有校验逻辑
|
||||
IdempotentRecord idempotentRecord = payMapper.selectIdempotentByRequestId(businessId);
|
||||
if (idempotentRecord == null || idempotentRecord.getStatus() != 1) {
|
||||
System.err.println("幂等记录异常,requestId:" + businessId);
|
||||
return false;
|
||||
}
|
||||
PayRecord payRecord = payMapper.selectPayRecordByOrderNo(orderNo);
|
||||
if (payRecord == null || payRecord.getPayStatus() != 0) {
|
||||
System.err.println("支付记录异常,订单号:" + orderNo);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 更新支付记录为成功
|
||||
int updateResult = payMapper.updatePayRecordStatus(orderNo, 1);
|
||||
if (updateResult == 0) {
|
||||
throw new RuntimeException("更新支付状态失败,订单号:" + orderNo);
|
||||
}
|
||||
|
||||
// ========== 新增:支付成功后发送MQ消息给订单服务 ==========
|
||||
try {
|
||||
// 构建支付成功消息体
|
||||
Map<String, Object> paySuccessMsg = new HashMap<>();
|
||||
paySuccessMsg.put("orderNo", orderNo);
|
||||
paySuccessMsg.put("payAmount", payRecord.getPayAmount());
|
||||
paySuccessMsg.put("payTime", new Date());
|
||||
paySuccessMsg.put("businessId", businessId);
|
||||
|
||||
// 发送消息(同步发送,确保订单服务能收到)
|
||||
rocketMQTemplate.convertAndSend(PAY_SUCCESS_TOPIC, paySuccessMsg);
|
||||
System.out.println("支付成功消息已发送,订单号:" + orderNo);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
// 生产环境:消息发送失败需重试/告警,避免订单状态不一致
|
||||
throw new RuntimeException("发送支付成功消息失败,订单号:" + orderNo, e);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public boolean isPaySuccess(String orderNo) {
|
||||
// 调用新增的mapper方法,返回1则表示支付成功
|
||||
int count = payMapper.checkPaySuccessByOrderNo(orderNo);
|
||||
return count > 0;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# RocketMQ 2.2.3 version does not adapt to SpringBoot3
|
||||
org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration
|
||||
54
pay-service/src/main/resources/application.yml
Normal file
54
pay-service/src/main/resources/application.yml
Normal file
@@ -0,0 +1,54 @@
|
||||
server:
|
||||
port: 9093
|
||||
spring:
|
||||
application:
|
||||
name: pay-service
|
||||
cloud:
|
||||
nacos:
|
||||
# 服务注册发现配置
|
||||
discovery:
|
||||
server-addr: localhost:8848 # Nacos 服务地址(默认端口 8848)
|
||||
namespace: public # 命名空间(默认 public,自定义需先在 Nacos 控制台创建)
|
||||
group: DEFAULT_GROUP # 分组(默认 DEFAULT_GROUP)
|
||||
service: pay-service # 注册到 Nacos 的服务名(建议和子项目 artifactId 一致)
|
||||
# 配置管理配置(如果引入了 config 依赖才需要)
|
||||
config:
|
||||
server-addr: localhost:8848 # 和 discovery 一致
|
||||
file-extension: yaml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
import: nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}?server-addr=${spring.cloud.nacos.config.server-addr}
|
||||
datasource:
|
||||
# 数据库驱动类(MySQL 8.x 用 com.mysql.cj.jdbc.Driver,5.x 用 com.mysql.jdbc.Driver)
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# 数据库连接 URL(替换为你的数据库地址、端口、库名,如 user_db)
|
||||
url: jdbc:mysql://rm-f8z6oc5a03331500p8o.mysql.rds.aliyuncs.com:3306/pay_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
# 数据库用户名(默认 root,根据实际情况修改)
|
||||
username: root
|
||||
# 数据库密码(替换为你的 MySQL 密码)
|
||||
password: Root123456
|
||||
# 可选:连接池配置(推荐使用 HikariCP,Spring Boot 2.x 默认)
|
||||
hikari:
|
||||
# 连接池最大连接数
|
||||
maximum-pool-size: 10
|
||||
# 连接池最小空闲连接数
|
||||
minimum-idle: 2
|
||||
# 连接超时时间(毫秒)
|
||||
connection-timeout: 30000
|
||||
# 连接最大存活时间(毫秒)
|
||||
max-lifetime: 1800000
|
||||
rocketmq:
|
||||
# NameServer 地址(替换为你的 RocketMQ 服务器IP)
|
||||
name-server: 127.0.0.1:9876
|
||||
producer:
|
||||
group: pay-producer-group
|
||||
consumer:
|
||||
# 消费者组名(必须唯一,建议按服务+用途命名)
|
||||
group: pay-service-consumer
|
||||
# 消费模式:CONCURRENTLY(并发消费,默认)/ORDERLY(顺序消费)
|
||||
consume-mode: CONCURRENTLY
|
||||
# 批量消费最大条数(默认1,单条消费)
|
||||
consume-message-batch-max-size: 1
|
||||
# 最大重试次数(消费失败后自动重试,超过次数进入死信队列)
|
||||
max-reconsume-times: 3
|
||||
38
pay-service/src/test/java/cn/mayiming/AppTest.java
Normal file
38
pay-service/src/test/java/cn/mayiming/AppTest.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
/**
|
||||
* Unit test for simple App.
|
||||
*/
|
||||
public class AppTest
|
||||
extends TestCase
|
||||
{
|
||||
/**
|
||||
* Create the test case
|
||||
*
|
||||
* @param testName name of the test case
|
||||
*/
|
||||
public AppTest( String testName )
|
||||
{
|
||||
super( testName );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the suite of tests being tested
|
||||
*/
|
||||
public static Test suite()
|
||||
{
|
||||
return new TestSuite( AppTest.class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigourous Test :-)
|
||||
*/
|
||||
public void testApp()
|
||||
{
|
||||
assertTrue( true );
|
||||
}
|
||||
}
|
||||
159
pom.xml
159
pom.xml
@@ -1,47 +1,150 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<!-- 父工程坐标 -->
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories</artifactId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>javamemories</name>
|
||||
<name>javamemories-parent</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<!-- 继承 Spring Boot 官方父依赖 -->
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<!-- Spring Boot 3最新稳定版,可在官网确认最新版本 -->
|
||||
<version>3.2.3</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>apache-snapshots</id>
|
||||
<name>Apache Snapshots</name>
|
||||
<url>https://repository.apache.org/content/repositories/releases/</url>
|
||||
<releases>
|
||||
<enabled>true</enabled>
|
||||
</releases>
|
||||
<snapshots>
|
||||
<enabled>false</enabled>
|
||||
</snapshots>
|
||||
</repository>
|
||||
<!-- 保留中央仓库 -->
|
||||
<repository>
|
||||
<id>central</id>
|
||||
<url>https://repo.maven.apache.org/maven2</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<!-- 统一版本变量(新增 spring-boot.version 兜底) -->
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<java.version>22</java.version>
|
||||
<spring-cloud.version>2023.0.1</spring-cloud.version>
|
||||
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version>
|
||||
<mybatis-spring-boot.version>3.0.3</mybatis-spring-boot.version>
|
||||
<!-- 核心新增:显式定义 Spring Boot 版本(和 parent 版本一致) -->
|
||||
<spring-boot.version>3.2.3</spring-boot.version>
|
||||
<!-- 新增 MySQL 驱动版本(避免版本未知) -->
|
||||
<mysql.version>8.0.33</mysql.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.33</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
||||
|
||||
<!-- 依赖管理(子模块继承版本,仅管理不引入) -->
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<!-- 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>
|
||||
<!-- MyBatis 版本管理 -->
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>${mybatis-spring-boot.version}</version>
|
||||
</dependency>
|
||||
<!-- MySQL 驱动(显式绑定版本) -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<version>${mysql.version}</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<!-- Spring Boot Web(显式绑定版本,解决 unknown 问题) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</dependency>
|
||||
<!-- Spring Boot Redis(显式绑定版本) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</dependency>
|
||||
<!-- Spring Boot 测试(显式绑定版本) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-client</artifactId>
|
||||
<version>5.3.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
<version>2.3.4</version> <!-- 适配 RocketMQ 5.x,建议2.2.x+ -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.xuxueli</groupId>
|
||||
<artifactId>xxl-job-core</artifactId>
|
||||
<version>2.4.1</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<!-- 子模块声明 -->
|
||||
<modules>
|
||||
<module>javamemories-common</module>
|
||||
<module>user-service</module>
|
||||
<module>order-service</module>
|
||||
<module>javamemories-gateway</module>
|
||||
<module>request-test</module>
|
||||
<module>pay-service</module>
|
||||
<module>stock-service</module>
|
||||
<module>javamemories-concurrency</module>
|
||||
</modules>
|
||||
|
||||
<!-- 编译插件(确保 Java 版本兼容) -->
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>${java.version}</source>
|
||||
<target>${java.version}</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
37
request-test/pom.xml
Normal file
37
request-test/pom.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>request-test</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>request-test</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- Spring Boot Redis(显式绑定版本) -->
|
||||
<dependency>
|
||||
<groupId>redis.clients</groupId>
|
||||
<artifactId>jedis</artifactId>
|
||||
<version>4.4.6</version> <!-- 推荐使用稳定版 -->
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
35
request-test/src/main/java/cn/mayiming/App.java
Normal file
35
request-test/src/main/java/cn/mayiming/App.java
Normal file
@@ -0,0 +1,35 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import cn.mayiming.Properties.UserProperties;
|
||||
import cn.mayiming.Serivce.UserSerivce;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import redis.clients.jedis.Jedis;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
public class App
|
||||
{
|
||||
|
||||
public static void main(String[] args) {
|
||||
ApplicationContext context = SpringApplication.run(App.class, args);
|
||||
UserSerivce userSerivce = context.getBean(UserSerivce.class);
|
||||
System.out.println("UserSerivce 对象:" + userSerivce);
|
||||
userSerivce.getUser(); // 输出:mayiming
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package cn.mayiming.Properties;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@ConfigurationProperties(prefix = "user")
|
||||
@Component
|
||||
@Data
|
||||
public class UserProperties {
|
||||
private String name;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.mayiming.Serivce;
|
||||
|
||||
|
||||
import cn.mayiming.Properties.UserProperties;
|
||||
import org.apache.catalina.User;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserSerivce {
|
||||
@Autowired
|
||||
UserProperties userProperties;
|
||||
public void getUser() {
|
||||
System.out.println(userProperties.getName());
|
||||
}
|
||||
}
|
||||
2
request-test/src/main/resources/properties.yml
Normal file
2
request-test/src/main/resources/properties.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
user:
|
||||
name: mayiming
|
||||
38
request-test/src/test/java/cn/mayiming/AppTest.java
Normal file
38
request-test/src/test/java/cn/mayiming/AppTest.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
/**
|
||||
* Unit test for simple App.
|
||||
*/
|
||||
public class AppTest
|
||||
extends TestCase
|
||||
{
|
||||
/**
|
||||
* Create the test case
|
||||
*
|
||||
* @param testName name of the test case
|
||||
*/
|
||||
public AppTest( String testName )
|
||||
{
|
||||
super( testName );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the suite of tests being tested
|
||||
*/
|
||||
public static Test suite()
|
||||
{
|
||||
return new TestSuite( AppTest.class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigourous Test :-)
|
||||
*/
|
||||
public void testApp()
|
||||
{
|
||||
assertTrue( true );
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package cn.mayiming.Controller.Redis;
|
||||
|
||||
public class RedisParam {
|
||||
private String key;
|
||||
private String value;
|
||||
|
||||
// 必须加无参构造器(JSON解析需要)
|
||||
public RedisParam() {}
|
||||
|
||||
// GET/SET方法(必须加,否则JSON解析不到值)
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package cn.mayiming.Controller.Redis;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
|
||||
@RestController
|
||||
public class Redistest {
|
||||
@Autowired
|
||||
private StringRedisTemplate stringRedisTemplate;
|
||||
@PostMapping("/redis/put")
|
||||
public String put(@RequestBody RedisParam param) {
|
||||
String key = param.getKey();
|
||||
stringRedisTemplate.opsForValue().set(key, "123", 10, TimeUnit.MINUTES);
|
||||
String redisValue = stringRedisTemplate.opsForValue().get(key);
|
||||
return "Redis写入成功!key=" + key + ",value=" + redisValue;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package cn.mayiming.Controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class User {
|
||||
@PostMapping("/login")
|
||||
public Map<String, String> login() {
|
||||
Map<String, String> map = new HashMap<>();
|
||||
map.put("username", "admin");
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
server:
|
||||
port: 9090
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
port: 6379
|
||||
password: ""
|
||||
database: 0
|
||||
timeout: 10000
|
||||
88
stock-service/pom.xml
Normal file
88
stock-service/pom.xml
Normal file
@@ -0,0 +1,88 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>stock-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>stock-service</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 可选:Nacos 配置管理(从 Nacos 读取配置文件) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
<!-- 负载均衡(Feign 内置,但显式引入更清晰) -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.csp</groupId>
|
||||
<artifactId>sentinel-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis</artifactId>
|
||||
<version>3.5.19</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis</groupId>
|
||||
<artifactId>mybatis-spring</artifactId>
|
||||
<version>4.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
</dependencies>
|
||||
</project>
|
||||
21
stock-service/src/main/java/cn/mayiming/App.java
Normal file
21
stock-service/src/main/java/cn/mayiming/App.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@EnableTransactionManagement
|
||||
@MapperScan("cn.mayiming.Mapper")
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package cn.mayiming.Controller;
|
||||
|
||||
|
||||
import cn.mayiming.Entity.StockDeductDTO;
|
||||
import cn.mayiming.Service.stockService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/stock")
|
||||
public class stockController {
|
||||
@Autowired
|
||||
stockService stockService;
|
||||
|
||||
@PostMapping("/deduct")
|
||||
public int deductstock(@Valid @RequestBody StockDeductDTO stockDeductDTO) {
|
||||
return stockService.deductStock(stockDeductDTO.getId(),stockDeductDTO.getDeductNum());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package cn.mayiming.Entity;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class StockDeductDTO {
|
||||
@NotNull
|
||||
@JsonProperty("id")
|
||||
private Long id;
|
||||
|
||||
@NotNull
|
||||
@JsonProperty("deductnum")
|
||||
private Integer deductNum;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package cn.mayiming.Mapper;
|
||||
|
||||
import feign.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface StockMapper {
|
||||
|
||||
@Select("SELECT stock_num FROM t_stock WHERE goods_id = #{goodsId}")
|
||||
Integer selectStockByGoodsId(@Param("goodsId") Long goodsId);
|
||||
|
||||
@Update("UPDATE t_stock SET stock_num = stock_num - #{deductNum} WHERE goods_id = #{goodsId} AND stock_num >= #{deductNum}")
|
||||
int deductStock(@Param("goodsId") Long goodsId, @Param("deductNum") Integer deductNum);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package cn.mayiming.Service;
|
||||
|
||||
|
||||
import cn.mayiming.Mapper.StockMapper;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Service
|
||||
public class stockService {
|
||||
@Autowired
|
||||
StockMapper stockMapper;
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int deductStock(Long goodsId, int i) {
|
||||
return stockMapper.deductStock(goodsId, i);
|
||||
}
|
||||
}
|
||||
52
stock-service/src/main/resources/application.yml
Normal file
52
stock-service/src/main/resources/application.yml
Normal file
@@ -0,0 +1,52 @@
|
||||
server:
|
||||
port: 9094
|
||||
spring:
|
||||
application:
|
||||
name: stock-service
|
||||
cloud:
|
||||
nacos:
|
||||
# 服务注册发现配置
|
||||
discovery:
|
||||
server-addr: localhost:8848 # Nacos 服务地址(默认端口 8848)
|
||||
namespace: public # 命名空间(默认 public,自定义需先在 Nacos 控制台创建)
|
||||
group: DEFAULT_GROUP # 分组(默认 DEFAULT_GROUP)
|
||||
service: stock-service # 注册到 Nacos 的服务名(建议和子项目 artifactId 一致)
|
||||
# 配置管理配置(如果引入了 config 依赖才需要)
|
||||
config:
|
||||
server-addr: localhost:8848 # 和 discovery 一致
|
||||
file-extension: yaml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
import: nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}?server-addr=${spring.cloud.nacos.config.server-addr}
|
||||
datasource:
|
||||
# 数据库驱动类(MySQL 8.x 用 com.mysql.cj.jdbc.Driver,5.x 用 com.mysql.jdbc.Driver)
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# 数据库连接 URL(替换为你的数据库地址、端口、库名,如 user_db)
|
||||
url: jdbc:mysql://rm-f8z6oc5a03331500p8o.mysql.rds.aliyuncs.com:3306/stock_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
# 数据库用户名(默认 root,根据实际情况修改)
|
||||
username: root
|
||||
# 数据库密码(替换为你的 MySQL 密码)
|
||||
password: Root123456
|
||||
# 可选:连接池配置(推荐使用 HikariCP,Spring Boot 2.x 默认)
|
||||
hikari:
|
||||
# 连接池最大连接数
|
||||
maximum-pool-size: 10
|
||||
# 连接池最小空闲连接数
|
||||
minimum-idle: 2
|
||||
# 连接超时时间(毫秒)
|
||||
connection-timeout: 30000
|
||||
# 连接最大存活时间(毫秒)
|
||||
max-lifetime: 1800000
|
||||
#rocketmq:
|
||||
# # NameServer 地址(替换为你的 RocketMQ 服务器IP)
|
||||
# name-server: 127.0.0.1:9876
|
||||
# consumer:
|
||||
# # 消费者组名(必须唯一,建议按服务+用途命名)
|
||||
# group: order-service-consumer
|
||||
# # 消费模式:CONCURRENTLY(并发消费,默认)/ORDERLY(顺序消费)
|
||||
# consume-mode: CONCURRENTLY
|
||||
# # 批量消费最大条数(默认1,单条消费)
|
||||
# consume-message-batch-max-size: 1
|
||||
# # 最大重试次数(消费失败后自动重试,超过次数进入死信队列)
|
||||
# max-reconsume-times: 3
|
||||
38
stock-service/src/test/java/cn/mayiming/AppTest.java
Normal file
38
stock-service/src/test/java/cn/mayiming/AppTest.java
Normal file
@@ -0,0 +1,38 @@
|
||||
package cn.mayiming;
|
||||
|
||||
import junit.framework.Test;
|
||||
import junit.framework.TestCase;
|
||||
import junit.framework.TestSuite;
|
||||
|
||||
/**
|
||||
* Unit test for simple App.
|
||||
*/
|
||||
public class AppTest
|
||||
extends TestCase
|
||||
{
|
||||
/**
|
||||
* Create the test case
|
||||
*
|
||||
* @param testName name of the test case
|
||||
*/
|
||||
public AppTest( String testName )
|
||||
{
|
||||
super( testName );
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the suite of tests being tested
|
||||
*/
|
||||
public static Test suite()
|
||||
{
|
||||
return new TestSuite( AppTest.class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Rigourous Test :-)
|
||||
*/
|
||||
public void testApp()
|
||||
{
|
||||
assertTrue( true );
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
server:
|
||||
port: 9090
|
||||
spring:
|
||||
data:
|
||||
redis:
|
||||
port: 6379
|
||||
password: ""
|
||||
database: 0
|
||||
timeout: 10000
|
||||
69
user-service/pom.xml
Normal file
69
user-service/pom.xml
Normal file
@@ -0,0 +1,69 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.mayiming</groupId>
|
||||
<artifactId>javamemories-parent</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>user-service</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>user-service</name>
|
||||
<url>http://maven.apache.org</url>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<mysql.version>8.0.33</mysql.version>
|
||||
<!-- 显式指定 MyBatis 版本(与 Spring Boot 3.2.3 兼容) -->
|
||||
<mybatis-spring-boot-starter.version>3.0.3</mybatis-spring-boot-starter.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 可选:Nacos 配置管理(从 Nacos 读取配置文件) -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- MyBatis 版本管理 -->
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<!-- MySQL 驱动(显式绑定版本) -->
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<!-- 构建配置:确保编译和打包正常 -->
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>3.2.3</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
23
user-service/src/main/java/cn/mayiming/App.java
Normal file
23
user-service/src/main/java/cn/mayiming/App.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package cn.mayiming;
|
||||
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
|
||||
/**
|
||||
* Hello world!
|
||||
*
|
||||
*/
|
||||
|
||||
@SpringBootApplication
|
||||
@MapperScan("cn.mayiming.Mapper")
|
||||
@EnableTransactionManagement
|
||||
public class App
|
||||
{
|
||||
public static void main( String[] args )
|
||||
{
|
||||
SpringApplication.run(App.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package cn.mayiming.Controller;
|
||||
|
||||
import cn.mayiming.Mapper.UserMapper;
|
||||
import cn.mayiming.Service.UserService;
|
||||
import cn.mayiming.entity.User;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping("/user")
|
||||
public User getUser(@RequestBody User user) {
|
||||
return userService.getUserByUsername(user.getUsername());
|
||||
}
|
||||
|
||||
@PutMapping("/user")
|
||||
public int updateUser(@RequestBody User user) {
|
||||
return userService.updateUser(user);
|
||||
}
|
||||
|
||||
@GetMapping("/user")
|
||||
public User GetUserByid(@RequestParam Integer id) {
|
||||
return userService.getUserById(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package cn.mayiming.Mapper;
|
||||
|
||||
import cn.mayiming.entity.User;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface UserMapper {
|
||||
/**
|
||||
* 根据ID查询用户
|
||||
* @param id 用户ID
|
||||
* @return 用户信息
|
||||
*/
|
||||
@Select("SELECT id, username, password, nickname FROM user WHERE id = #{id}")
|
||||
User selectById(@Param("id") Integer id); // 注意:id类型改为Integer(对应表的int)
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
* @param username 用户名
|
||||
* @return 用户信息
|
||||
*/
|
||||
@Select("SELECT id, username, password, nickname FROM user WHERE username = #{username}")
|
||||
User selectByUsername(@Param("username") String username);
|
||||
|
||||
/**
|
||||
* 新增用户(自动回填自增ID)
|
||||
* @param user 用户对象
|
||||
* @return 影响行数
|
||||
*/
|
||||
@Insert("INSERT INTO user (username, password, nickname) " +
|
||||
"VALUES (#{username}, #{password}, #{nickname})")
|
||||
// 适配int类型自增主键,keyProperty对应实体类的id属性
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
|
||||
int insert(User user);
|
||||
|
||||
/**
|
||||
* 更新用户信息(全字段更新)
|
||||
* @param user 用户对象(含要更新的字段)
|
||||
* @return 影响行数
|
||||
*/
|
||||
@Update({
|
||||
"<script>",
|
||||
"UPDATE user",
|
||||
"<set>",
|
||||
" <if test='username != null'>username = #{username},</if>",
|
||||
" <if test='password != null'>password = #{password},</if>",
|
||||
" <if test='nickname != null'>nickname = #{nickname}</if>",
|
||||
"</set>",
|
||||
"WHERE id = #{id}",
|
||||
"</script>"
|
||||
})
|
||||
int updateById(User user);
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
* @param id 用户ID
|
||||
* @return 影响行数
|
||||
*/
|
||||
@Delete("DELETE FROM user WHERE id = #{id}")
|
||||
int deleteById(@Param("id") Integer id);
|
||||
|
||||
/**
|
||||
* 查询所有用户
|
||||
* @return 用户列表
|
||||
*/
|
||||
@Select("SELECT id, username, password, nickname FROM user")
|
||||
List<User> selectAll();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package cn.mayiming.Service;
|
||||
|
||||
import cn.mayiming.Mapper.UserMapper;
|
||||
import cn.mayiming.entity.User;
|
||||
import org.apache.ibatis.jdbc.Null;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
public int addUser() {
|
||||
User user = new User();
|
||||
user.setUsername("test01");
|
||||
user.setPassword("123456");
|
||||
// 新增后id会自动回填
|
||||
int rows = userMapper.insert(user);
|
||||
return rows;
|
||||
}
|
||||
|
||||
|
||||
// 根据用户名查询
|
||||
public User getUserByUsername(String username) {
|
||||
return userMapper.selectByUsername(username);
|
||||
}
|
||||
|
||||
// 查询所有用户
|
||||
public List<User> getAllUsers() {
|
||||
return userMapper.selectAll();
|
||||
}
|
||||
|
||||
public User getUserById(Integer id) {
|
||||
return userMapper.selectById(id);
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public int updateUser(User user) {
|
||||
if (user.getId() == null){
|
||||
throw new IllegalArgumentException("更新失败:用户ID不能为空");
|
||||
}
|
||||
return userMapper.updateById(user);
|
||||
}
|
||||
}
|
||||
42
user-service/src/main/java/cn/mayiming/entity/User.java
Normal file
42
user-service/src/main/java/cn/mayiming/entity/User.java
Normal file
@@ -0,0 +1,42 @@
|
||||
package cn.mayiming.entity;
|
||||
|
||||
public class User {
|
||||
public String getUsername() {
|
||||
return username;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
String username;
|
||||
String password;
|
||||
|
||||
public Integer getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Integer id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
Integer id;
|
||||
|
||||
public String getNickname() {
|
||||
return nickname;
|
||||
}
|
||||
|
||||
public void setNickname(String nickname) {
|
||||
this.nickname = nickname;
|
||||
}
|
||||
|
||||
String nickname;
|
||||
}
|
||||
40
user-service/src/main/resources/application.yml
Normal file
40
user-service/src/main/resources/application.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
server:
|
||||
port: 9091
|
||||
spring:
|
||||
application:
|
||||
name: user-service
|
||||
cloud:
|
||||
nacos:
|
||||
# 服务注册发现配置
|
||||
discovery:
|
||||
server-addr: localhost:8848 # Nacos 服务地址(默认端口 8848)
|
||||
namespace: public # 命名空间(默认 public,自定义需先在 Nacos 控制台创建)
|
||||
group: DEFAULT_GROUP # 分组(默认 DEFAULT_GROUP)
|
||||
service: user-service # 注册到 Nacos 的服务名(建议和子项目 artifactId 一致)
|
||||
# 配置管理配置(如果引入了 config 依赖才需要)
|
||||
config:
|
||||
server-addr: localhost:8848 # 和 discovery 一致
|
||||
file-extension: yaml
|
||||
namespace: public
|
||||
group: DEFAULT_GROUP
|
||||
config:
|
||||
import: nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}?server-addr=${spring.cloud.nacos.config.server-addr}
|
||||
datasource:
|
||||
# 数据库驱动类(MySQL 8.x 用 com.mysql.cj.jdbc.Driver,5.x 用 com.mysql.jdbc.Driver)
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
# 数据库连接 URL(替换为你的数据库地址、端口、库名,如 user_db)
|
||||
url: jdbc:mysql://rm-f8z6oc5a03331500p8o.mysql.rds.aliyuncs.com:3306/test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
|
||||
# 数据库用户名(默认 root,根据实际情况修改)
|
||||
username: root
|
||||
# 数据库密码(替换为你的 MySQL 密码)
|
||||
password: Root123456
|
||||
# 可选:连接池配置(推荐使用 HikariCP,Spring Boot 2.x 默认)
|
||||
hikari:
|
||||
# 连接池最大连接数
|
||||
maximum-pool-size: 10
|
||||
# 连接池最小空闲连接数
|
||||
minimum-idle: 2
|
||||
# 连接超时时间(毫秒)
|
||||
connection-timeout: 30000
|
||||
# 连接最大存活时间(毫秒)
|
||||
max-lifetime: 1800000
|
||||
Reference in New Issue
Block a user