Compare commits

...

25 Commits

Author SHA1 Message Date
JACKYMYPERSON
0fbfe30af7 学习JDK动态代理 2026-03-12 10:04:13 +08:00
JACKYMYPERSON
056f2eca65 Merge remote-tracking branch 'origin/main' 2026-03-09 16:36:00 +08:00
JACKYMYPERSON
c2b42476bc 学习迭代器 2026-03-09 16:35:54 +08:00
07a71fb437 添加java并发子项目 2026-03-07 19:09:41 +08:00
88b40ba4a2 完成学习redis数据结构 2026-03-07 14:43:43 +08:00
b7ad9b57c6 完成学习xxl-job 2026-03-04 12:29:38 +08:00
d80a42a20d 修复pay-service中rocketMq实例无法注入 2026-03-03 23:14:22 +08:00
137729161f 商城商店步骤学习完成 2026-03-03 20:51:53 +08:00
5d04837d46 学习到支付服务 2026-03-03 20:10:22 +08:00
b0c2889ddf 下单逻辑学习完毕 2026-03-03 19:39:54 +08:00
8b37cccb93 完成基本下单的学习 2026-03-03 18:02:03 +08:00
887fea1961 添加rocketmq依赖 2026-03-03 14:23:10 +08:00
e11d7b703f 添加支付服务和库存服务 2026-03-03 13:57:05 +08:00
12e38067ab 添加rocketMQ的docker容器 2026-03-03 13:56:46 +08:00
2a79fca22c 完成学习事务 2026-03-03 13:14:15 +08:00
5fb6350763 测试网关限流 2026-03-02 19:17:34 +08:00
a2a8547bca 完成gateway网关的路由转发学习 2026-03-02 19:14:58 +08:00
e09987fbbb 完成学习nacos请求转发 2026-03-02 18:04:06 +08:00
dc17788678 增加nacos中间件 2026-03-02 17:07:12 +08:00
f467a1064e 升级微服务项目 2026-03-02 16:08:33 +08:00
2df6ee04bc 升级微服务项目 2026-03-02 16:07:08 +08:00
9f5d9f73a0 spring boot项目完成1 2026-03-02 15:30:12 +08:00
d2a5416bea spring boot项目完成1 2026-03-02 15:29:54 +08:00
fbb935ba0c Merge remote-tracking branch 'origin/main'
# Conflicts:
#	pom.xml
2026-03-02 15:28:38 +08:00
2b6b0e1bb5 spring boot项目完成 2026-03-02 15:27:52 +08:00
74 changed files with 2770 additions and 115 deletions

1
.gitignore vendored
View File

@@ -72,6 +72,7 @@ coverage/
*.tmp *.tmp
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
target/
# ===================== IDE/编辑器配置 ===================== # ===================== IDE/编辑器配置 =====================
# VS Code # VS Code

1
broker.conf Normal file
View File

@@ -0,0 +1 @@
"brokerIP1=127.0.0.1"

View 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
View 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

View 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>

View File

@@ -0,0 +1,14 @@
package cn.mayiming;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}

View File

@@ -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;
}

View 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>

View File

@@ -0,0 +1,13 @@
package cn.mayiming;
/**
* Hello world!
*
*/
public class App
{
public static void main( String[] args )
{
System.out.println( "Hello World!" );
}
}

View 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 );
}
}

View 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>

View File

@@ -3,12 +3,15 @@ package cn.mayiming;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Hello world!
*
*/
@SpringBootApplication @SpringBootApplication
public class App public class App
{ {
public static void main( String[] args ) public static void main( String[] args )
{ {
SpringApplication.run(App.class, args); SpringApplication.run(App.class, args);
} }
} }

View File

@@ -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;
}
}

View File

@@ -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 {
/**
* 初始化熔断规则
* @PostConstructSpring 容器启动后执行
*/
@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);
}
}

View File

@@ -0,0 +1,91 @@
server:
port: 8081
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # 自动从 nacos 发现服务
routes:
# 路由1user-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}"
# 路由2order-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

View 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
View 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>

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}
}

View File

@@ -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());
// 示例:定时订单超时取消、数据同步、日志清理等都可以写在这里
}
}

View File

@@ -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);
}

View File

@@ -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 {
// 参数1topic: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;
}
}

View 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; // 创建时间
}

View File

@@ -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用于关单恢复库存
}

View 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;
}
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View 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.Driver5.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
# 可选:连接池配置(推荐使用 HikariCPSpring 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

View 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
View 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>

View 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);
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View 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;
}

View File

@@ -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用于关单恢复库存
}

View File

@@ -0,0 +1,10 @@
package cn.mayiming.Entity;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class checkpayDTO {
@NotNull
private String orderno;
}

View 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);
}

View 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;
}
}

View File

@@ -0,0 +1,2 @@
# RocketMQ 2.2.3 version does not adapt to SpringBoot3
org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration

View 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.Driver5.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
# 可选:连接池配置(推荐使用 HikariCPSpring 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

View 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
View File

@@ -1,47 +1,150 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <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> <modelVersion>4.0.0</modelVersion>
<!-- 父工程坐标 -->
<groupId>cn.mayiming</groupId> <groupId>cn.mayiming</groupId>
<artifactId>javamemories</artifactId> <artifactId>javamemories-parent</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<packaging>jar</packaging> <packaging>pom</packaging>
<name>javamemories</name> <name>javamemories-parent</name>
<url>http://maven.apache.org</url> <url>http://maven.apache.org</url>
<!-- 继承 Spring Boot 官方父依赖 -->
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<!-- Spring Boot 3最新稳定版可在官网确认最新版本 -->
<version>3.2.3</version> <version>3.2.3</version>
<relativePath/> <relativePath/>
</parent> </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> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <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> </properties>
<dependencies>
<dependency>
<groupId>junit</groupId> <!-- 依赖管理(子模块继承版本,仅管理不引入) -->
<artifactId>junit</artifactId> <dependencyManagement>
<version>3.8.1</version> <dependencies>
<scope>test</scope> <!-- Spring Cloud 核心版本管理 -->
</dependency> <dependency>
<dependency> <groupId>org.springframework.cloud</groupId>
<groupId>org.springframework.boot</groupId> <artifactId>spring-cloud-dependencies</artifactId>
<artifactId>spring-boot-starter-web</artifactId> <version>${spring-cloud.version}</version>
</dependency> <type>pom</type>
<dependency> <scope>import</scope>
<groupId>org.springframework.boot</groupId> </dependency>
<artifactId>spring-boot-starter-data-redis</artifactId> <!-- Spring Cloud Alibaba 版本管理 -->
</dependency> <dependency>
<dependency> <groupId>com.alibaba.cloud</groupId>
<groupId>mysql</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId>
<artifactId>mysql-connector-java</artifactId> <version>${spring-cloud-alibaba.version}</version>
<version>8.0.33</version> <type>pom</type>
<scope>runtime</scope> <scope>import</scope>
</dependency> </dependency>
</dependencies> <!-- MyBatis 版本管理 -->
</project> <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
View 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>

View 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
}
}

View File

@@ -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;
}

View File

@@ -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());
}
}

View File

@@ -0,0 +1,2 @@
user:
name: mayiming

View 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 );
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -1,9 +0,0 @@
server:
port: 9090
spring:
data:
redis:
port: 6379
password: ""
database: 0
timeout: 10000

88
stock-service/pom.xml Normal file
View 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>

View 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);
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View 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.Driver5.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
# 可选:连接池配置(推荐使用 HikariCPSpring 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

View 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 );
}
}

View File

@@ -1,9 +0,0 @@
server:
port: 9090
spring:
data:
redis:
port: 6379
password: ""
database: 0
timeout: 10000

69
user-service/pom.xml Normal file
View 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>

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View 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;
}

View 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.Driver5.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
# 可选:连接池配置(推荐使用 HikariCPSpring Boot 2.x 默认)
hikari:
# 连接池最大连接数
maximum-pool-size: 10
# 连接池最小空闲连接数
minimum-idle: 2
# 连接超时时间(毫秒)
connection-timeout: 30000
# 连接最大存活时间(毫秒)
max-lifetime: 1800000