第一章:项目架构设计与技术选型
1.1 电商系统核心业务分析
传统单体电商系统的痛点:
java
// 传统单体电商架构的问题
单点故障:商品服务崩溃 → 整个网站不可用
技术栈固化:所有模块必须使用相同技术
扩展困难:促销时只能整体扩容
发布风险:小改动需要全站发布
团队协作:代码冲突、沟通成本高
// 电商系统核心业务流程:
用户浏览 → 加入购物车 → 下单 → 支付 → 发货 → 收货 → 评价
↓ ↓ ↓ ↓ ↓ ↓ ↓
商品服务 购物车服务 订单服务 支付服务 库存服务 物流服务 评价服务
微服务拆分策略:
text
电商微服务拆分原则:
- 按业务领域拆分(DDD领域驱动设计)
- 按功能独立性拆分
- 按数据边界拆分
- 按团队结构拆分
- 按性能需求拆分
核心服务划分:
用户域:用户服务、认证服务、会员服务
商品域:商品服务、类目服务、搜索服务
交易域:购物车服务、订单服务、支付服务
库存域:库存服务、仓库服务
物流域:物流服务、地址服务
营销域:优惠券服务、活动服务
运营域:统计服务、通知服务
1.2 技术栈全景图
完整技术选型:
xml
核心技术:Spring Boot 3.1 + Spring Cloud 2022 + JDK 17
注册中心:Nacos 2.2(服务发现+配置中心)
API网关:Spring Cloud Gateway + Sentinel
服务通信:OpenFeign + LoadBalancer
熔断降级:Resilience4j + Sentinel
链路追踪:Sleuth + Zipkin + SkyWalking
配置中心:Nacos Config
分布式事务:Seata
消息队列:RocketMQ 5.0
缓存:Redis 7 + Redisson
数据库:MySQL 8.0 + MyBatis-Plus
搜索引擎:Elasticsearch 8.0
对象存储:MinIO
监控告警:Prometheus + Grafana + AlertManager
容器化:Docker + Kubernetes
CI/CD:Jenkins + GitLab
第二章:项目初始化与基础架构
2.1 创建父工程与模块划分
项目整体结构:
text
e-commerce-microservices/
├── README.md
├── pom.xml # 父工程
├── docker-compose.yml
├── sql/ # 数据库脚本
├── config/ # 配置文件
├── scripts/ # 部署脚本
├── docs/ # 文档
├── e-commerce-parent/ # 父模块
│ ├── e-commerce-common/ # 公共模块
│ │ ├── common-core/ # 核心工具
│ │ ├── common-dto/ # 通用DTO
│ │ ├── common-feign/ # Feign客户端
│ │ ├── common-redis/ # Redis配置
│ │ ├── common-mybatis/ # MyBatis配置
│ │ └── common-security/ # 安全配置
│ │
│ ├── e-commerce-gateway/ # API网关
│ ├── e-commerce-auth/ # 认证中心
│ ├── e-commerce-user/ # 用户服务
│ ├── e-commerce-product/ # 商品服务
│ ├── e-commerce-cart/ # 购物车服务
│ ├── e-commerce-order/ # 订单服务
│ ├── e-commerce-payment/ # 支付服务
│ ├── e-commerce-inventory/ # 库存服务
│ ├── e-commerce-coupon/ # 优惠券服务
│ ├── e-commerce-search/ # 搜索服务
│ ├── e-commerce-notification/ # 通知服务
│ └── e-commerce-monitor/ # 监控服务
└── infrastructure/ # 基础设施
├── nacos/ # Nacos配置
├── seata/ # Seata配置
├── sentinel/ # Sentinel配置
└── prometheus/ # Prometheus配置
父工程pom.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<modelVersion>4.0.0</modelVersion>
<groupId>com.ecommerce</groupId>
<artifactId>e-commerce-microservices</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<name>e-commerce-microservices</name>
<description>电商微服务系统</description>
<!-- 模块定义 -->
<modules>
<module>e-commerce-parent/e-commerce-common/common-core</module>
<module>e-commerce-parent/e-commerce-common/common-dto</module>
<module>e-commerce-parent/e-commerce-common/common-feign</module>
<module>e-commerce-parent/e-commerce-common/common-redis</module>
<module>e-commerce-parent/e-commerce-common/common-mybatis</module>
<module>e-commerce-parent/e-commerce-common/common-security</module>
<module>e-commerce-parent/e-commerce-gateway</module>
<module>e-commerce-parent/e-commerce-auth</module>
<module>e-commerce-parent/e-commerce-user</module>
<module>e-commerce-parent/e-commerce-product</module>
<module>e-commerce-parent/e-commerce-cart</module>
<module>e-commerce-parent/e-commerce-order</module>
<module>e-commerce-parent/e-commerce-payment</module>
<module>e-commerce-parent/e-commerce-inventory</module>
<module>e-commerce-parent/e-commerce-coupon</module>
<module>e-commerce-parent/e-commerce-search</module>
<module>e-commerce-parent/e-commerce-notification</module>
<module>e-commerce-parent/e-commerce-monitor</module>
</modules>
<!-- 统一版本管理 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<properties>
<!-- 基础版本 -->
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Spring Cloud -->
<spring-cloud.version>2022.0.3</spring-cloud.version>
<spring-cloud-alibaba.version>2022.0.0.0</spring-cloud-alibaba.version>
<!-- 持久层 -->
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
<druid.version>1.2.18</druid.version>
<!-- 工具 -->
<mapstruct.version>1.5.5.Final</mapstruct.version>
<lombok.version>1.18.28</lombok.version>
<hutool.version>5.8.20</hutool.version>
<!-- 缓存 -->
<redisson.version>3.20.0</redisson.version>
<!-- 分布式事务 -->
<seata.version>1.7.1</seata.version>
<!-- 消息队列 -->
<rocketmq.version>2.2.3</rocketmq.version>
<!-- 服务端口 -->
<gateway.port>8080</gateway.port>
<auth.port>8081</auth.port>
<user.port>8082</user.port>
<product.port>8083</product.port>
<cart.port>8084</cart.port>
<order.port>8085</order.port>
<payment.port>8086</payment.port>
<inventory.port>8087</inventory.port>
</properties>
<!-- 依赖管理 -->
<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 Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!-- MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
<!-- Redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
</dependency>
<!-- Hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- Seata -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>${seata.version}</version>
</dependency>
<!-- RocketMQ -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- 通用依赖 -->
<dependencies>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- 构建配置 -->
<build>
<plugins>
<!-- 编译器插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Spring Boot Maven插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
2.2 公共模块设计 common-core 核心工具模块:
java
// 1. 统一响应类
package com.ecommerce.common.core.response;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class ApiResponse implements Serializable {
private Integer code;
private String message;
private T data;
private Long timestamp;
public static <T> ApiResponse<T> success() {
return success(null);
}
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<T>()
.setCode(200)
.setMessage("成功")
.setData(data)
.setTimestamp(System.currentTimeMillis());
}
public static <T> ApiResponse<T> success(String message, T data) {
return new ApiResponse<T>()
.setCode(200)
.setMessage(message)
.setData(data)
.setTimestamp(System.currentTimeMillis());
}
public static <T> ApiResponse<T> error(Integer code, String message) {
return new ApiResponse<T>()
.setCode(code)
.setMessage(message)
.setTimestamp(System.currentTimeMillis());
}
public static <T> ApiResponse<T> error(String message) {
return error(500, message);
}
}
// 2. 业务异常类
package com.ecommerce.common.core.exception;
import lombok.Getter;
@Getter
public class BusinessException extends RuntimeException {
private final Integer code;
private final String message;
public BusinessException(String message) {
super(message);
this.code = 500;
this.message = message;
}
public BusinessException(Integer code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
}
// 3. 错误码枚举
package com.ecommerce.common.core.exception;
import lombok.Getter;
@Getter
public enum ErrorCode {
// 通用错误码
SUCCESS(200, "成功"),
PARAM_ERROR(400, "参数错误"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
NOT_FOUND(404, "资源不存在"),
METHOD_NOT_ALLOWED(405, "方法不允许"),
SYSTEM_ERROR(500, "系统错误"),
SERVICE_UNAVAILABLE(503, "服务不可用"),
// 业务错误码 1000-1999
USER_NOT_EXIST(1001, "用户不存在"),
USER_DISABLED(1002, "用户已被禁用"),
USER_PASSWORD_ERROR(1003, "密码错误"),
USER_EXISTS(1004, "用户已存在"),
// 商品错误码 2000-2999
PRODUCT_NOT_EXIST(2001, "商品不存在"),
PRODUCT_OFF_SHELF(2002, "商品已下架"),
PRODUCT_INSUFFICIENT_STOCK(2003, "商品库存不足"),
// 订单错误码 3000-3999
ORDER_NOT_EXIST(3001, "订单不存在"),
ORDER_STATUS_ERROR(3002, "订单状态错误"),
ORDER_CANCEL_FAILED(3003, "订单取消失败"),
// 支付错误码 4000-4999
PAYMENT_FAILED(4001, "支付失败"),
PAYMENT_NOT_EXIST(4002, "支付记录不存在"),
// 库存错误码 5000-5999
INVENTORY_LOCK_FAILED(5001, "库存锁定失败"),
INVENTORY_RELEASE_FAILED(5002, "库存释放失败"),
// 优惠券错误码 6000-6999
COUPON_NOT_EXIST(6001, "优惠券不存在"),
COUPON_EXPIRED(6002, "优惠券已过期"),
COUPON_USED(6003, "优惠券已使用");
private final Integer code;
private final String message;
ErrorCode(Integer code, String message) {
this.code = code;
this.message = message;
}
}
// 4. 统一异常处理器
package com.ecommerce.common.core.handler;
import com.ecommerce.common.core.response.ApiResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ApiResponse<?> handleBusinessException(BusinessException e) {
log.error("业务异常: code={}, message={}", e.getCode(), e.getMessage());
return ApiResponse.error(e.getCode(), e.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<?> handleValidationException(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getAllErrors().forEach(error -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
log.error("参数验证失败: {}", errors);
return ApiResponse.error(400, "参数验证失败").setData(errors);
}
@ExceptionHandler(Exception.class)
public ApiResponse<?> handleGlobalException(Exception e) {
log.error("系统异常: ", e);
return ApiResponse.error(500, "系统异常,请稍后重试");
}
}
common-dto 数据传输对象:
java
// 1. 分页请求DTO
package com.ecommerce.common.dto;
import lombok.Data;
@Data
public class PageRequest {
private Integer pageNum = 1;
private Integer pageSize = 10;
private String orderBy;
private Boolean asc = true;
public Integer getOffset() {
return (pageNum - 1) * pageSize;
}
}
// 2. 分页响应DTO
package com.ecommerce.common.dto;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
@NoArgsConstructor
public class PageResult {
private Integer pageNum;
private Integer pageSize;
private Long total;
private Integer pages;
private List<T> list;
public PageResult(Integer pageNum, Integer pageSize, Long total, List<T> list) {
this.pageNum = pageNum;
this.pageSize = pageSize;
this.total = total;
this.pages = (int) Math.ceil((double) total / pageSize);
this.list = list;
}
public static <T> PageResult<T> of(Integer pageNum, Integer pageSize, Long total, List<T> list) {
return new PageResult<>(pageNum, pageSize, total, list);
}
}
// 3. 用户上下文DTO
package com.ecommerce.common.dto;
import lombok.Data;
@Data
public class UserContext {
private Long userId;
private String username;
private String nickname;
private String phone;
private String email;
private Integer userType;
private String token;
public static UserContext empty() {
return new UserContext();
}
public boolean isLogin() {
return userId != null;
}
}
common-feign Feign客户端配置:
java
// 1. Feign配置类
package com.ecommerce.common.feign.config;
import com.ecommerce.common.feign.interceptor.FeignAuthInterceptor;
import feign.Logger;
import feign.Request;
import feign.Retryer;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
@Bean
public Request.Options options() {
return new Request.Options(
5, TimeUnit.SECONDS, // 连接超时
30, TimeUnit.SECONDS, // 读取超时
true // 跟随重定向
);
}
@Bean
public Retryer feignRetryer() {
// 最大重试3次,初始间隔100ms
return new Retryer.Default(100, TimeUnit.SECONDS.toMillis(1), 3);
}
@Bean
public FeignAuthInterceptor feignAuthInterceptor() {
return new FeignAuthInterceptor();
}
@Bean
public ErrorDecoder errorDecoder() {
return new FeignErrorDecoder();
}
}
// 2. Feign认证拦截器
package com.ecommerce.common.feign.interceptor;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Enumeration;
@Slf4j
public class FeignAuthInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
// 传递认证信息
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
if (headerName.startsWith("x-") ||
headerName.equalsIgnoreCase("authorization")) {
String headerValue = request.getHeader(headerName);
template.header(headerName, headerValue);
}
}
}
// 添加Feign调用标识
template.header("X-Feign-Request", "true");
template.header("X-Request-ID", generateRequestId());
log.debug("Feign请求: {} {}, Headers: {}",
template.method(), template.url(), template.headers());
}
private String generateRequestId() {
return java.util.UUID.randomUUID().toString();
}
}
// 3. Feign降级工厂
package com.ecommerce.common.feign.fallback;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class CommonFallbackFactory implements FallbackFactory {
private final Class<T> targetType;
public CommonFallbackFactory(Class<T> targetType) {
this.targetType = targetType;
}
@Override
public T create(Throwable cause) {
log.error("Feign调用失败,服务: {}, 错误: {}",
targetType.getSimpleName(), cause.getMessage());
return new FallbackProxy<>(targetType, cause).createProxy();
}
}
2.3 数据库设计与初始化
数据库设计原则:
sql
-- 1. 用户数据库 (user_db)
CREATE DATABASE IF NOT EXISTS user_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 用户表
CREATE TABLE user_db.tb_user (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID',
username VARCHAR(50) NOT NULL COMMENT '用户名',
password VARCHAR(100) NOT NULL COMMENT '密码',
nickname VARCHAR(50) COMMENT '昵称',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(100) COMMENT '邮箱',
avatar VARCHAR(500) COMMENT '头像',
gender TINYINT DEFAULT 0 COMMENT '性别 0-未知 1-男 2-女',
birthday DATE COMMENT '生日',
status TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常',
user_type TINYINT DEFAULT 1 COMMENT '用户类型 1-普通用户 2-会员 3-管理员',
last_login_time DATETIME COMMENT '最后登录时间',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_username (username),
UNIQUE KEY uk_phone (phone),
UNIQUE KEY uk_email (email),
KEY idx_status (status),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 用户地址表
CREATE TABLE user_db.tb_user_address (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '地址ID',
user_id BIGINT NOT NULL COMMENT '用户ID',
receiver_name VARCHAR(50) NOT NULL COMMENT '收货人姓名',
receiver_phone VARCHAR(20) NOT NULL COMMENT '收货人电话',
province VARCHAR(50) COMMENT '省',
city VARCHAR(50) COMMENT '市',
district VARCHAR(50) COMMENT '区',
detail VARCHAR(200) COMMENT '详细地址',
postal_code VARCHAR(10) COMMENT '邮政编码',
is_default TINYINT DEFAULT 0 COMMENT '是否默认地址 0-否 1-是',
status TINYINT DEFAULT 1 COMMENT '状态 0-删除 1-正常',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
KEY idx_user_id (user_id),
KEY idx_is_default (is_default),
CONSTRAINT fk_user_address_user FOREIGN KEY (user_id) REFERENCES tb_user (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户地址表';
-- 2. 商品数据库 (product_db)
CREATE DATABASE IF NOT EXISTS product_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 商品表
CREATE TABLE product_db.tb_product (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品ID',
spu_code VARCHAR(50) NOT NULL COMMENT 'SPU编码',
name VARCHAR(200) NOT NULL COMMENT '商品名称',
sub_title VARCHAR(500) COMMENT '副标题',
category_id BIGINT NOT NULL COMMENT '分类ID',
brand_id BIGINT COMMENT '品牌ID',
main_image VARCHAR(500) COMMENT '主图',
images TEXT COMMENT '商品图片JSON数组',
detail TEXT COMMENT '商品详情',
specifications TEXT COMMENT '规格参数JSON',
price DECIMAL(10,2) NOT NULL COMMENT '价格',
market_price DECIMAL(10,2) COMMENT '市场价格',
cost_price DECIMAL(10,2) COMMENT '成本价',
stock INT NOT NULL DEFAULT 0 COMMENT '库存',
sales INT DEFAULT 0 COMMENT '销量',
status TINYINT DEFAULT 1 COMMENT '状态 0-下架 1-上架 2-待审核',
weight DECIMAL(10,3) COMMENT '重量(kg)',
volume DECIMAL(10,3) COMMENT '体积(m³)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_spu_code (spu_code),
KEY idx_category_id (category_id),
KEY idx_brand_id (brand_id),
KEY idx_status (status),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';
-- 商品SKU表
CREATE TABLE product_db.tb_product_sku (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT 'SKU ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
sku_code VARCHAR(50) NOT NULL COMMENT 'SKU编码',
specifications VARCHAR(500) COMMENT '规格JSON',
price DECIMAL(10,2) NOT NULL COMMENT '价格',
market_price DECIMAL(10,2) COMMENT '市场价格',
stock INT NOT NULL DEFAULT 0 COMMENT '库存',
sales INT DEFAULT 0 COMMENT '销量',
image VARCHAR(500) COMMENT 'SKU图片',
status TINYINT DEFAULT 1 COMMENT '状态 0-禁用 1-正常',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_sku_code (sku_code),
KEY idx_product_id (product_id),
KEY idx_status (status),
CONSTRAINT fk_product_sku_product FOREIGN KEY (product_id) REFERENCES tb_product (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品SKU表';
-- 3. 订单数据库 (order_db)
CREATE DATABASE IF NOT EXISTS order_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 订单表
CREATE TABLE order_db.tb_order (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID',
order_no VARCHAR(32) NOT NULL COMMENT '订单号',
user_id BIGINT NOT NULL COMMENT '用户ID',
total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额',
discount_amount DECIMAL(10,2) DEFAULT 0 COMMENT '优惠金额',
shipping_amount DECIMAL(10,2) DEFAULT 0 COMMENT '运费',
pay_amount DECIMAL(10,2) NOT NULL COMMENT '实付金额',
payment_type TINYINT COMMENT '支付方式 1-微信 2-支付宝',
status TINYINT NOT NULL COMMENT '订单状态 1-待付款 2-待发货 3-已发货 4-已完成 5-已取消',
payment_time DATETIME COMMENT '支付时间',
shipping_time DATETIME COMMENT '发货时间',
receive_time DATETIME COMMENT '收货时间',
cancel_time DATETIME COMMENT '取消时间',
cancel_reason VARCHAR(200) COMMENT '取消原因',
remark VARCHAR(500) COMMENT '订单备注',
receiver_name VARCHAR(50) NOT NULL COMMENT '收货人姓名',
receiver_phone VARCHAR(20) NOT NULL COMMENT '收货人电话',
receiver_address VARCHAR(500) NOT NULL COMMENT '收货地址',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id),
KEY idx_status (status),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
-- 订单商品表
CREATE TABLE order_db.tb_order_item (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单商品ID',
order_id BIGINT NOT NULL COMMENT '订单ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
product_name VARCHAR(200) NOT NULL COMMENT '商品名称',
product_image VARCHAR(500) COMMENT '商品图片',
sku_id BIGINT COMMENT 'SKU ID',
sku_code VARCHAR(50) COMMENT 'SKU编码',
specifications VARCHAR(500) COMMENT '规格',
price DECIMAL(10,2) NOT NULL COMMENT '单价',
quantity INT NOT NULL COMMENT '数量',
total_amount DECIMAL(10,2) NOT NULL COMMENT '总金额',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
KEY idx_order_id (order_id),
KEY idx_product_id (product_id),
CONSTRAINT fk_order_item_order FOREIGN KEY (order_id) REFERENCES tb_order (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单商品表';
-- 4. 库存数据库 (inventory_db)
CREATE DATABASE IF NOT EXISTS inventory_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 库存表
CREATE TABLE inventory_db.tb_inventory (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '库存ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
sku_id BIGINT COMMENT 'SKU ID',
warehouse_id BIGINT NOT NULL COMMENT '仓库ID',
total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存',
available_stock INT NOT NULL DEFAULT 0 COMMENT '可用库存',
locked_stock INT DEFAULT 0 COMMENT '锁定库存',
occupied_stock INT DEFAULT 0 COMMENT '占用库存',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (id),
UNIQUE KEY uk_product_warehouse (product_id, warehouse_id),
KEY idx_sku_id (sku_id),
KEY idx_warehouse_id (warehouse_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表';
-- 库存操作记录表
CREATE TABLE inventory_db.tb_inventory_log (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '日志ID',
product_id BIGINT NOT NULL COMMENT '商品ID',
sku_id BIGINT COMMENT 'SKU ID',
warehouse_id BIGINT NOT NULL COMMENT '仓库ID',
order_no VARCHAR(32) COMMENT '订单号',
operation_type TINYINT NOT NULL COMMENT '操作类型 1-入库 2-出库 3-锁定 4-解锁 5-占用 6-释放',
quantity INT NOT NULL COMMENT '操作数量',
before_stock INT NOT NULL COMMENT '操作前库存',
after_stock INT NOT NULL COMMENT '操作后库存',
remark VARCHAR(200) COMMENT '备注',
operator VARCHAR(50) COMMENT '操作人',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (id),
KEY idx_product_id (product_id),
KEY idx_order_no (order_no),
KEY idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存操作记录表';
第三章:用户服务实现
3.1 用户服务核心功能
用户服务pom.xml:
xml
<?xml version="1.0" encoding="UTF-8"?>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ecommerce</groupId>
<artifactId>e-commerce-microservices</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>e-commerce-user</artifactId>
<name>e-commerce-user</name>
<description>用户服务</description>
<properties>
<service.port>8082</service.port>
</properties>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.ecommerce</groupId>
<artifactId>common-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.ecommerce</groupId>
<artifactId>common-dto</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Cloud -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 持久层 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- 数据库驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<!-- 监控 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
用户服务配置文件:
yaml
bootstrap.yml
spring:
application:
name: user-service
profiles:
active: dev
cloud:
nacos:
discovery:
server-addr: N A C O S H O S T : l o c a l h o s t : {NACOS_HOST:localhost}: NACOSHOST:localhost:{NACOS_PORT:8848}
namespace: ${NACOS_NAMESPACE:public}
group: ${NACOS_GROUP:DEFAULT_GROUP}
metadata:
version: 1.0.0
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
file-extension: yaml
shared-configs:
- data-id: common.yaml
group: ${spring.cloud.nacos.discovery.group}
refresh: true
- data-id: datasource.yaml
group: ${spring.cloud.nacos.discovery.group}
refresh: true
- data-id: redis.yaml
group: ${spring.cloud.nacos.discovery.group}
refresh: true
application-dev.yml
server:
port: 8082
servlet:
context-path: /user
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/user_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
druid:
initial-size: 5
min-idle: 5
max-active: 20
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
redis:
host: localhost
port: 6379
password:
database: 0
lettuce:
pool:
max-active: 20
max-wait: -1ms
max-idle: 10
min-idle: 0
shutdown-timeout: 100ms
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.ecommerce.user.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
JWT配置
jwt:
secret: ecommerce-user-secret-key-2023-spring-cloud-microservices
expiration: 86400000 # 24小时
header: Authorization
token-prefix: Bearer
用户服务配置
user:
password:
salt: ecommerce2023
max-retry-count: 5
lock-duration: 30 # 分钟
日志配置
logging:
level:
com.ecommerce.user: DEBUG
org.springframework.cloud: INFO
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
file:
name: logs/user-service.log
max-size: 10MB
max-history: 30
管理端点
management:
endpoints:
web:
exposure:
include: "health,info,metrics,prometheus"
endpoint:
health:
show-details: always
probes:
enabled: true
用户服务核心代码:
- 实体类:
java
// 用户实体
package com.ecommerce.user.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("tb_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String password;
private String nickname;
private String phone;
private String email;
private String avatar;
private Integer gender;
private LocalDateTime birthday;
private Integer status;
private Integer userType;
private LocalDateTime lastLoginTime;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
// 用户地址实体
package com.ecommerce.user.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("tb_user_address")
public class UserAddress {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private String receiverName;
private String receiverPhone;
private String province;
private String city;
private String district;
private String detail;
private String postalCode;
private Integer isDefault;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
- Mapper接口:
java
// 用户Mapper
package com.ecommerce.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ecommerce.user.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserMapper extends BaseMapper {
@Select("SELECT * FROM tb_user WHERE username = #{username} AND deleted = 0")
User selectByUsername(String username);
@Select("SELECT * FROM tb_user WHERE phone = #{phone} AND deleted = 0")
User selectByPhone(String phone);
@Select("SELECT * FROM tb_user WHERE email = #{email} AND deleted = 0")
User selectByEmail(String email);
}
// 用户地址Mapper
package com.ecommerce.user.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ecommerce.user.entity.UserAddress;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface UserAddressMapper extends BaseMapper {
@Select("SELECT * FROM tb_user_address WHERE user_id = #{userId} AND status = 1 ORDER BY is_default DESC, create_time DESC")
List<UserAddress> selectByUserId(Long userId);
@Select("SELECT * FROM tb_user_address WHERE user_id = #{userId} AND is_default = 1 AND status = 1")
UserAddress selectDefaultByUserId(Long userId);
}
- Service层:
java
// 用户服务接口
package com.ecommerce.user.service;
import com.ecommerce.common.dto.PageRequest;
import com.ecommerce.common.dto.PageResult;
import com.ecommerce.user.dto.*;
public interface UserService {
// 用户注册
UserDTO register(UserRegisterDTO registerDTO);
// 用户登录
UserLoginResult login(UserLoginDTO loginDTO);
// 获取用户信息
UserDTO getUserInfo(Long userId);
// 更新用户信息
UserDTO updateUserInfo(Long userId, UserUpdateDTO updateDTO);
// 修改密码
void changePassword(Long userId, ChangePasswordDTO changePasswordDTO);
// 分页查询用户
PageResult<UserDTO> listUsers(PageRequest pageRequest, UserQueryDTO queryDTO);
// 检查用户名是否存在
boolean checkUsername(String username);
// 检查手机号是否存在
boolean checkPhone(String phone);
// 检查邮箱是否存在
boolean checkEmail(String email);
}
// 用户服务实现
package com.ecommerce.user.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ecommerce.common.core.exception.BusinessException;
import com.ecommerce.common.core.exception.ErrorCode;
import com.ecommerce.common.dto.PageRequest;
import com.ecommerce.common.dto.PageResult;
import com.ecommerce.user.dto.*;
import com.ecommerce.user.entity.User;
import com.ecommerce.user.mapper.UserMapper;
import com.ecommerce.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class UserServiceImpl implements UserService {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
private final RedisTemplate<String, Object> redisTemplate;
private final UserMapper userMapper;
@Override
public UserDTO register(UserRegisterDTO registerDTO) {
log.info("用户注册: username={}, phone={}",
registerDTO.getUsername(), registerDTO.getPhone());
// 1. 验证参数
validateRegisterDTO(registerDTO);
// 2. 检查用户名、手机号、邮箱是否已存在
if (checkUsername(registerDTO.getUsername())) {
throw new BusinessException(ErrorCode.USER_EXISTS);
}
if (StrUtil.isNotBlank(registerDTO.getPhone()) &&
checkPhone(registerDTO.getPhone())) {
throw new BusinessException("手机号已存在");
}
if (StrUtil.isNotBlank(registerDTO.getEmail()) &&
checkEmail(registerDTO.getEmail())) {
throw new BusinessException("邮箱已存在");
}
// 3. 创建用户
User user = new User();
BeanUtil.copyProperties(registerDTO, user);
// 密码加密
user.setPassword(passwordEncoder.encode(registerDTO.getPassword()));
// 设置默认值
user.setStatus(1); // 正常状态
user.setUserType(1); // 普通用户
user.setCreateTime(LocalDateTime.now());
user.setUpdateTime(LocalDateTime.now());
// 4. 保存用户
userMapper.insert(user);
log.info("用户注册成功: userId={}, username={}", user.getId(), user.getUsername());
// 5. 清除相关缓存
clearUserCache(user.getId(), user.getUsername());
return convertToDTO(user);
}
@Override
public UserLoginResult login(UserLoginDTO loginDTO) {
log.info("用户登录: identifier={}", loginDTO.getIdentifier());
// 1. 获取用户
User user = getUserByIdentifier(loginDTO.getIdentifier());
if (user == null) {
throw new BusinessException(ErrorCode.USER_NOT_EXIST);
}
// 2. 检查用户状态
if (user.getStatus() != 1) {
throw new BusinessException(ErrorCode.USER_DISABLED);
}
// 3. 验证密码
if (!passwordEncoder.matches(loginDTO.getPassword(), user.getPassword())) {
// 记录登录失败次数
recordLoginFailure(user.getId());
throw new BusinessException(ErrorCode.USER_PASSWORD_ERROR);
}
// 4. 清除登录失败记录
clearLoginFailure(user.getId());
// 5. 更新最后登录时间
user.setLastLoginTime(LocalDateTime.now());
userMapper.updateById(user);
// 6. 生成token
String token = generateToken(user);
// 7. 返回结果
UserLoginResult result = new UserLoginResult();
result.setUser(convertToDTO(user));
result.setToken(token);
result.setExpireTime(System.currentTimeMillis() + 86400000); // 24小时
log.info("用户登录成功: userId={}, username={}", user.getId(), user.getUsername());
return result;
}
@Override
@Transactional(readOnly = true)
public UserDTO getUserInfo(Long userId) {
log.debug("获取用户信息: userId={}", userId);
// 先从缓存获取
String cacheKey = "user:info:" + userId;
UserDTO cachedUser = (UserDTO) redisTemplate.opsForValue().get(cacheKey);
if (cachedUser != null) {
return cachedUser;
}
// 从数据库获取
User user = userMapper.selectById(userId);
if (user == null || user.getDeleted() == 1) {
throw new BusinessException(ErrorCode.USER_NOT_EXIST);
}
UserDTO userDTO = convertToDTO(user);
// 存入缓存
redisTemplate.opsForValue().set(cacheKey, userDTO, 30, TimeUnit.MINUTES);
return userDTO;
}
@Override
public PageResult<UserDTO> listUsers(PageRequest pageRequest, UserQueryDTO queryDTO) {
log.debug("分页查询用户: pageNum={}, pageSize={}",
pageRequest.getPageNum(), pageRequest.getPageSize());
// 构建查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getDeleted, 0);
if (StrUtil.isNotBlank(queryDTO.getUsername())) {
wrapper.like(User::getUsername, queryDTO.getUsername());
}
if (StrUtil.isNotBlank(queryDTO.getPhone())) {
wrapper.like(User::getPhone, queryDTO.getPhone());
}
if (queryDTO.getStatus() != null) {
wrapper.eq(User::getStatus, queryDTO.getStatus());
}
if (queryDTO.getUserType() != null) {
wrapper.eq(User::getUserType, queryDTO.getUserType());
}
// 排序
if (StrUtil.isNotBlank(pageRequest.getOrderBy())) {
if (pageRequest.getAsc()) {
wrapper.orderByAsc(User::getCreateTime);
} else {
wrapper.orderByDesc(User::getCreateTime);
}
} else {
wrapper.orderByDesc(User::getCreateTime);
}
// 分页查询
Page<User> page = new Page<>(pageRequest.getPageNum(), pageRequest.getPageSize());
Page<User> userPage = userMapper.selectPage(page, wrapper);
// 转换为DTO
List<UserDTO> userDTOs = userPage.getRecords().stream()
.map(this::convertToDTO)
.collect(Collectors.toList());
return PageResult.of(
(int) userPage.getCurrent(),
(int) userPage.getSize(),
userPage.getTotal(),
userDTOs
);
}
private User getUserByIdentifier(String identifier) {
// 根据用户名、手机号、邮箱获取用户
User user = userMapper.selectByUsername(identifier);
if (user == null) {
user = userMapper.selectByPhone(identifier);
}
if (user == null) {
user = userMapper.selectByEmail(identifier);
}
return user;
}
private void validateRegisterDTO(UserRegisterDTO registerDTO) {
if (StrUtil.isBlank(registerDTO.getUsername())) {
throw new BusinessException("用户名不能为空");
}
if (StrUtil.isBlank(registerDTO.getPassword())) {
throw new BusinessException("密码不能为空");
}
if (StrUtil.isBlank(registerDTO.getPhone())) {
throw new BusinessException("手机号不能为空");
}
// 验证手机号格式
if (!registerDTO.getPhone().matches("^1[3-9]\\d{9}$")) {
throw new BusinessException("手机号格式不正确");
}
// 验证密码强度
if (registerDTO.getPassword().length() < 6) {
throw new BusinessException("密码长度不能少于6位");
}
}
private void recordLoginFailure(Long userId) {
String key = "user:login:fail:" + userId;
Long failCount = redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, 30, TimeUnit.MINUTES);
// 如果失败次数超过5次,锁定账户30分钟
if (failCount != null && failCount >= 5) {
String lockKey = "user:lock:" + userId;
redisTemplate.opsForValue().set(lockKey, "locked", 30, TimeUnit.MINUTES);
log.warn("用户登录失败次数过多,账户锁定: userId={}, failCount={}", userId, failCount);
}
}
private void clearLoginFailure(Long userId) {
String key = "user:login:fail:" + userId;
redisTemplate.delete(key);
String lockKey = "user:lock:" + userId;
redisTemplate.delete(lockKey);
}
private String generateToken(User user) {
// 使用JWT生成token
Map<String, Object> claims = new HashMap<>();
claims.put("userId", user.getId());
claims.put("username", user.getUsername());
claims.put("userType", user.getUserType());
return JwtUtil.generateToken(claims, user.getUsername());
}
private UserDTO convertToDTO(User user) {
UserDTO userDTO = new UserDTO();
BeanUtil.copyProperties(user, userDTO);
return userDTO;
}
private void clearUserCache(Long userId, String username) {
// 清除用户信息缓存
redisTemplate.delete("user:info:" + userId);
redisTemplate.delete("user:info:username:" + username);
}
}
// 用户地址服务
package com.ecommerce.user.service;
import com.ecommerce.user.dto.UserAddressDTO;
import java.util.List;
public interface UserAddressService {
// 添加地址
UserAddressDTO addAddress(Long userId, UserAddressDTO addressDTO);
// 更新地址
UserAddressDTO updateAddress(Long userId, Long addressId, UserAddressDTO addressDTO);
// 删除地址
void deleteAddress(Long userId, Long addressId);
// 获取地址列表
List<UserAddressDTO> listAddresses(Long userId);
// 获取默认地址
UserAddressDTO getDefaultAddress(Long userId);
// 设置默认地址
void setDefaultAddress(Long userId, Long addressId);
}
- 控制器层:
java
// 用户控制器
package com.ecommerce.user.controller;
import com.ecommerce.common.core.response.ApiResponse;
import com.ecommerce.common.dto.PageRequest;
import com.ecommerce.common.dto.PageResult;
import com.ecommerce.user.dto.;
import com.ecommerce.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.;
@Slf4j
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
@Tag(name = "用户管理", description = "用户管理相关接口")
public class UserController {
private final UserService userService;
@PostMapping("/register")
@Operation(summary = "用户注册", description = "新用户注册接口")
public ApiResponse<UserDTO> register(@Valid @RequestBody UserRegisterDTO registerDTO) {
UserDTO userDTO = userService.register(registerDTO);
return ApiResponse.success("注册成功", userDTO);
}
@PostMapping("/login")
@Operation(summary = "用户登录", description = "用户登录接口")
public ApiResponse<UserLoginResult> login(@Valid @RequestBody UserLoginDTO loginDTO) {
UserLoginResult result = userService.login(loginDTO);
return ApiResponse.success("登录成功", result);
}
@GetMapping("/info")
@Operation(summary = "获取用户信息", description = "获取当前登录用户信息")
public ApiResponse<UserDTO> getUserInfo(@RequestHeader("X-User-Id") Long userId) {
UserDTO userDTO = userService.getUserInfo(userId);
return ApiResponse.success(userDTO);
}
@PutMapping("/info")
@Operation(summary = "更新用户信息", description = "更新用户基本信息")
public ApiResponse<UserDTO> updateUserInfo(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody UserUpdateDTO updateDTO) {
UserDTO userDTO = userService.updateUserInfo(userId, updateDTO);
return ApiResponse.success("更新成功", userDTO);
}
@PutMapping("/password")
@Operation(summary = "修改密码", description = "修改登录密码")
public ApiResponse<Void> changePassword(
@RequestHeader("X-User-Id") Long userId,
@Valid @RequestBody ChangePasswordDTO changePasswordDTO) {
userService.changePassword(userId, changePasswordDTO);
return ApiResponse.success("密码修改成功");
}
@GetMapping("/list")
@Operation(summary = "分页查询用户", description = "管理员分页查询用户列表")
public ApiResponse<PageResult<UserDTO>> listUsers(
@Valid PageRequest pageRequest,
UserQueryDTO queryDTO) {
PageResult<UserDTO> result = userService.listUsers(pageRequest, queryDTO);
return ApiResponse.success(result);
}
@GetMapping("/check/username")
@Operation(summary = "检查用户名", description = "检查用户名是否已存在")
public ApiResponse<Boolean> checkUsername(@RequestParam String username) {
boolean exists = userService.checkUsername(username);
return ApiResponse.success(!exists);
}
@GetMapping("/check/phone")
@Operation(summary = "检查手机号", description = "检查手机号是否已存在")
public ApiResponse<Boolean> checkPhone(@RequestParam String phone) {
boolean exists = userService.checkPhone(phone);
return ApiResponse.success(!exists);
}
}
- JWT工具类:
java
package com.ecommerce.user.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
import java.util.Date;
import java.util.Map;
@Slf4j
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
private Key getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes());
}
// 生成token
public static String generateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
.compact();
}
// 从token中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从token中获取过期时间
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 从token中获取指定claim
public <T> T getClaimFromToken(String token, java.util.function.Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 从token中获取所有claims
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 验证token是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 验证token
public Boolean validateToken(String token, String username) {
final String usernameFromToken = getUsernameFromToken(token);
return (username.equals(usernameFromToken) && !isTokenExpired(token));
}
}
第四章:商品服务实现
4.1 商品服务核心功能
商品服务pom.xml:
xml
com.ecommerce common-core ${project.version}
<!-- Elasticsearch -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!-- 缓存 -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<!-- 消息队列 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<!-- Seata分布式事务 -->
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</dependency>
商品服务核心代码:
- 商品实体与Mapper:
java
// 商品实体
package com.ecommerce.product.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Data
@TableName("tb_product")
public class Product {
@TableId(type = IdType.AUTO)
private Long id;
private String spuCode;
private String name;
private String subTitle;
private Long categoryId;
private Long brandId;
private String mainImage;
@TableField(typeHandler = JsonTypeHandler.class)
private List<String> images;
private String detail;
@TableField(typeHandler = JsonTypeHandler.class)
private List<ProductSpec> specifications;
private BigDecimal price;
private BigDecimal marketPrice;
private BigDecimal costPrice;
private Integer stock;
private Integer sales;
private Integer status;
private BigDecimal weight;
private BigDecimal volume;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableLogic
private Integer deleted;
}
// 商品规格
@Data
public class ProductSpec {
private String name;
private String value;
private String unit;
}
// JSON类型处理器
public class JsonTypeHandler extends BaseTypeHandler {
private final Class<T> type;
private final ObjectMapper objectMapper;
public JsonTypeHandler(Class<T> type) {
this.type = type;
this.objectMapper = new ObjectMapper();
this.objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
try {
String json = objectMapper.writeValueAsString(parameter);
ps.setString(i, json);
} catch (JsonProcessingException e) {
throw new RuntimeException("JSON序列化失败", e);
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
String json = rs.getString(columnName);
return parseJson(json);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String json = rs.getString(columnIndex);
return parseJson(json);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String json = cs.getString(columnIndex);
return parseJson(json);
}
private T parseJson(String json) {
if (StringUtils.isBlank(json)) {
return null;
}
try {
return objectMapper.readValue(json, type);
} catch (IOException e) {
throw new RuntimeException("JSON反序列化失败", e);
}
}
}
// 商品Mapper
package com.ecommerce.product.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ecommerce.product.entity.Product;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface ProductMapper extends BaseMapper {
@Select("SELECT * FROM tb_product WHERE id = #{id} AND status = 1 AND deleted = 0")
Product selectAvailableById(Long id);
@Update("UPDATE tb_product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{productId} AND stock >= #{quantity}")
int deductStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
@Update("UPDATE tb_product SET stock = stock + #{quantity} WHERE id = #{productId}")
int addStock(@Param("productId") Long productId, @Param("quantity") Integer quantity);
}
- 商品搜索(Elasticsearch集成):
java
// Elasticsearch商品文档
package com.ecommerce.product.document;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
@Data
@Document(indexName = "products")
public class ProductDocument {
@Id
private Long id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String name;
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String subTitle;
@Field(type = FieldType.Long)
private Long categoryId;
@Field(type = FieldType.Long)
private Long brandId;
@Field(type = FieldType.Keyword)
private String spuCode;
@Field(type = FieldType.Text)
private String mainImage;
@Field(type = FieldType.Double)
private BigDecimal price;
@Field(type = FieldType.Integer)
private Integer stock;
@Field(type = FieldType.Integer)
private Integer sales;
@Field(type = FieldType.Integer)
private Integer status;
@Field(type = FieldType.Nested)
private List<ProductSpecDocument> specifications;
@Field(type = FieldType.Object)
private Map<String, Object> attributes;
@Field(type = FieldType.Date)
private Long createTime;
}
// 商品规格文档
@Data
public class ProductSpecDocument {
@Field(type = FieldType.Keyword)
private String name;
@Field(type = FieldType.Text, analyzer = "ik_smart")
private String value;
@Field(type = FieldType.Keyword)
private String unit;
}
// 商品搜索Repository
package com.ecommerce.product.repository;
import com.ecommerce.product.document.ProductDocument;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.elasticsearch.annotations.Query;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
public interface ProductSearchRepository extends ElasticsearchRepository<ProductDocument, Long> {
// 根据名称搜索
Page<ProductDocument> findByName(String name, Pageable pageable);
// 根据分类ID搜索
Page<ProductDocument> findByCategoryId(Long categoryId, Pageable pageable);
// 价格范围搜索
Page<ProductDocument> findByPriceBetween(BigDecimal minPrice, BigDecimal maxPrice, Pageable pageable);
// 复合条件搜索
@Query("{\"bool\": {\"must\": [" +
"{\"match\": {\"name\": \"?0\"}}," +
"{\"range\": {\"price\": {\"gte\": ?1, \"lte\": ?2}}}" +
"]}}")
Page<ProductDocument> searchByNameAndPriceRange(String name, BigDecimal minPrice, BigDecimal maxPrice, Pageable pageable);
}
// 商品搜索服务
package com.ecommerce.product.service;
import com.ecommerce.common.dto.PageRequest;
import com.ecommerce.common.dto.PageResult;
import com.ecommerce.product.dto.ProductSearchDTO;
import com.ecommerce.product.document.ProductDocument;
public interface ProductSearchService {
// 创建索引
boolean createIndex();
// 添加商品到索引
void addProductToIndex(ProductDocument product);
// 从索引中删除商品
void removeProductFromIndex(Long productId);
// 搜索商品
PageResult<ProductDocument> searchProducts(ProductSearchDTO searchDTO, PageRequest pageRequest);
// 根据ID批量查询
List<ProductDocument> findByIds(List<Long> ids);
// 更新商品索引
void updateProductIndex(ProductDocument product);
}
- 商品缓存策略(Redisson分布式锁):
java
// 商品缓存服务
package com.ecommerce.product.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class ProductCacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final RedissonClient redissonClient;
// 商品缓存key
private static final String PRODUCT_KEY_PREFIX = "product:";
private static final String PRODUCT_STOCK_KEY_PREFIX = "product:stock:";
private static final String PRODUCT_LOCK_KEY_PREFIX = "product:lock:";
// 获取商品信息(带缓存)
public ProductDTO getProductWithCache(Long productId) {
String cacheKey = PRODUCT_KEY_PREFIX + productId;
// 1. 从缓存获取
ProductDTO product = (ProductDTO) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 2. 获取分布式锁
String lockKey = PRODUCT_LOCK_KEY_PREFIX + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待5秒,锁超时时间10秒
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取商品信息超时");
}
// 3. 双重检查缓存
product = (ProductDTO) redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 4. 从数据库获取
product = productService.getProductById(productId);
if (product == null) {
// 缓存空值,防止缓存穿透
redisTemplate.opsForValue().set(cacheKey, "", 5, TimeUnit.MINUTES);
return null;
}
// 5. 放入缓存
redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
// 6. 缓存库存到Redis(用于秒杀等场景)
cacheStock(productId, product.getStock());
return product;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("获取商品信息中断", e);
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 缓存库存
private void cacheStock(Long productId, Integer stock) {
String stockKey = PRODUCT_STOCK_KEY_PREFIX + productId;
redisTemplate.opsForValue().set(stockKey, stock, 1, TimeUnit.HOURS);
}
// 扣减库存(Redis预减库存)
public boolean deductStock(Long productId, Integer quantity) {
String stockKey = PRODUCT_STOCK_KEY_PREFIX + productId;
String lockKey = PRODUCT_LOCK_KEY_PREFIX + productId + ":stock";
RLock lock = redissonClient.getLock(lockKey);
try {
// 加锁
boolean locked = lock.tryLock(3, 5, TimeUnit.SECONDS);
if (!locked) {
return false;
}
// 获取当前库存
Object stockObj = redisTemplate.opsForValue().get(stockKey);
if (stockObj == null) {
// 库存未缓存,从数据库获取
Integer dbStock = productService.getProductStock(productId);
redisTemplate.opsForValue().set(stockKey, dbStock, 1, TimeUnit.HOURS);
stockObj = dbStock;
}
Integer currentStock = (Integer) stockObj;
if (currentStock < quantity) {
return false; // 库存不足
}
// 预减库存
Long newStock = redisTemplate.opsForValue().decrement(stockKey, quantity);
// 异步更新数据库
asyncUpdateDbStock(productId, quantity);
return newStock != null && newStock >= 0;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
// 异步更新数据库库存
private void asyncUpdateDbStock(Long productId, Integer quantity) {
CompletableFuture.runAsync(() -> {
try {
productMapper.deductStock(productId, quantity);
} catch (Exception e) {
log.error("异步更新库存失败: productId={}, quantity={}", productId, quantity, e);
// 失败时回滚Redis库存
rollbackStock(productId, quantity);
}
});
}
// 回滚库存
private void rollbackStock(Long productId, Integer quantity) {
String stockKey = PRODUCT_STOCK_KEY_PREFIX + productId;
redisTemplate.opsForValue().increment(stockKey, quantity);
}
}
第五章:订单服务与分布式事务
5.1 订单服务核心功能
订单状态机设计:
java
// 订单状态枚举
package com.ecommerce.order.enums;
import lombok.Getter;
@Getter
public enum OrderStatus {
PENDING_PAYMENT(1, "待付款") {
@Override
public boolean canChangeTo(OrderStatus targetStatus) {
return targetStatus == PAID || targetStatus == CANCELLED;
}
},
PAID(2, "已付款") {
@Override
public boolean canChangeTo(OrderStatus targetStatus) {
return targetStatus == SHIPPED || targetStatus == REFUNDING;
}
},
SHIPPED(3, "已发货") {
@Override
public boolean canChangeTo(OrderStatus targetStatus) {
return targetStatus == RECEIVED || targetStatus == REFUNDING;
}
},
RECEIVED(4, "已完成") {
@Override
public boolean canChangeTo(OrderStatus targetStatus) {
return targetStatus == REFUNDED;
}
},
CANCELLED(5, "已取消") {
@Override
public boolean canChangeTo(OrderStatus targetStatus) {
return false;
}
},
REFUNDING(6, "退款中") {
@Override
public boolean canChangeTo(OrderStatus targetStatus) {
return targetStatus == REFUNDED || targetStatus == PAID;
}
},
REFUNDED(7, "已退款") {
@Override
public boolean canChangeTo(OrderStatus targetStatus) {
return false;
}
};
private final Integer code;
private final String description;
OrderStatus(Integer code, String description) {
this.code = code;
this.description = description;
}
// 检查是否可以转换到目标状态
public abstract boolean canChangeTo(OrderStatus targetStatus);
// 根据code获取枚举
public static OrderStatus getByCode(Integer code) {
for (OrderStatus status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("无效的订单状态码: " + code);
}
}
// 订单状态机
package com.ecommerce.order.service.state;
import com.ecommerce.order.enums.OrderStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class OrderStateMachine {
/**
* 检查订单状态是否可以转换
*/
public boolean canChangeStatus(OrderStatus currentStatus, OrderStatus targetStatus) {
if (currentStatus == null || targetStatus == null) {
return false;
}
return currentStatus.canChangeTo(targetStatus);
}
/**
* 执行状态转换
*/
public void changeStatus(Order order, OrderStatus targetStatus, String operator, String remark) {
OrderStatus currentStatus = OrderStatus.getByCode(order.getStatus());
if (!canChangeStatus(currentStatus, targetStatus)) {
throw new BusinessException(
String.format("订单状态不能从[%s]转换到[%s]",
currentStatus.getDescription(),
targetStatus.getDescription()));
}
// 更新订单状态
order.setStatus(targetStatus.getCode());
// 记录状态变更日志
OrderStatusLog statusLog = new OrderStatusLog();
statusLog.setOrderId(order.getId());
statusLog.setFromStatus(currentStatus.getCode());
statusLog.setToStatus(targetStatus.getCode());
statusLog.setOperator(operator);
statusLog.setRemark(remark);
statusLog.setCreateTime(LocalDateTime.now());
orderStatusLogMapper.insert(statusLog);
log.info("订单状态变更: orderId={}, from={}, to={}, operator={}",
order.getId(),
currentStatus.getDescription(),
targetStatus.getDescription(),
operator);
// 发布状态变更事件
publishStatusChangeEvent(order, currentStatus, targetStatus);
}
/**
* 发布状态变更事件
*/
private void publishStatusChangeEvent(Order order, OrderStatus fromStatus, OrderStatus toStatus) {
OrderStatusChangeEvent event = new OrderStatusChangeEvent();
event.setOrderId(order.getId());
event.setOrderNo(order.getOrderNo());
event.setUserId(order.getUserId());
event.setFromStatus(fromStatus);
event.setToStatus(toStatus);
event.setChangeTime(LocalDateTime.now());
applicationEventPublisher.publishEvent(event);
}
}
5.2 Seata分布式事务集成
Seata配置:
yaml
seata配置
seata:
enabled: true
application-id: ${spring.application.name}
tx-service-group: default_tx_group
enable-auto-data-source-proxy: true
config:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: SEATA_GROUP
username: nacos
password: nacos
service:
vgroup-mapping:
default_tx_group: default
disable-global-transaction: false
创建订单(分布式事务):
java
// 订单服务
package com.ecommerce.order.service;
import com.ecommerce.order.dto.CreateOrderDTO;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderMapper orderMapper;
private final OrderItemMapper orderItemMapper;
private final InventoryService inventoryService;
private final CouponService couponService;
private final ProductService productService;
/**
* 创建订单(分布式事务)
*/
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
public OrderDTO createOrder(CreateOrderDTO createOrderDTO) {
log.info("开始创建订单: userId={}, items={}",
createOrderDTO.getUserId(),
createOrderDTO.getItems().size());
// 1. 验证商品信息
validateProducts(createOrderDTO.getItems());
// 2. 生成订单号
String orderNo = generateOrderNo();
// 3. 计算订单金额
OrderAmount orderAmount = calculateOrderAmount(createOrderDTO);
// 4. 创建订单
Order order = createOrderEntity(createOrderDTO, orderNo, orderAmount);
orderMapper.insert(order);
// 5. 创建订单商品
List<OrderItem> orderItems = createOrderItems(order.getId(), createOrderDTO.getItems());
orderItemMapper.insertBatch(orderItems);
// 6. 扣减库存(调用库存服务)
inventoryService.deductStock(createOrderDTO.getItems());
// 7. 使用优惠券(调用优惠券服务)
if (createOrderDTO.getCouponId() != null) {
couponService.useCoupon(createOrderDTO.getUserId(), createOrderDTO.getCouponId());
}
// 8. 发送创建订单消息
sendOrderCreatedMessage(order);
log.info("订单创建成功: orderId={}, orderNo={}", order.getId(), order.getOrderNo());
return convertToDTO(order);
}
/**
* 验证商品信息
*/
private void validateProducts(List<CreateOrderItemDTO> items) {
for (CreateOrderItemDTO item : items) {
ProductDTO product = productService.getProductById(item.getProductId());
if (product == null) {
throw new BusinessException("商品不存在: " + item.getProductId());
}
if (product.getStatus() != 1) {
throw new BusinessException("商品已下架: " + product.getName());
}
if (product.getStock() < item.getQuantity()) {
throw new BusinessException("商品库存不足: " + product.getName());
}
}
}
/**
* 生成订单号
*/
private String generateOrderNo() {
// 时间戳 + 随机数
return "O" + System.currentTimeMillis() +
RandomUtil.randomNumbers(6);
}
/**
* 计算订单金额
*/
private OrderAmount calculateOrderAmount(CreateOrderDTO createOrderDTO) {
BigDecimal totalAmount = BigDecimal.ZERO;
for (CreateOrderItemDTO item : createOrderDTO.getItems()) {
ProductDTO product = productService.getProductById(item.getProductId());
BigDecimal itemAmount = product.getPrice().multiply(
BigDecimal.valueOf(item.getQuantity()));
totalAmount = totalAmount.add(itemAmount);
}
// 计算运费
BigDecimal shippingAmount = calculateShipping(createOrderDTO);
// 计算优惠金额
BigDecimal discountAmount = calculateDiscount(createOrderDTO);
// 实付金额 = 商品总额 + 运费 - 优惠
BigDecimal payAmount = totalAmount.add(shippingAmount).subtract(discountAmount);
OrderAmount orderAmount = new OrderAmount();
orderAmount.setTotalAmount(totalAmount);
orderAmount.setShippingAmount(shippingAmount);
orderAmount.setDiscountAmount(discountAmount);
orderAmount.setPayAmount(payAmount);
return orderAmount;
}
}
// 库存服务(扣减库存)
package com.ecommerce.inventory.service;
import io.seata.rm.tcc.api.BusinessActionContext;
import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.LocalTCC;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@LocalTCC
public class InventoryService {
/**
* TCC模式扣减库存
*/
@TwoPhaseBusinessAction(name = "deductStock", commitMethod = "commitDeductStock",
rollbackMethod = "rollbackDeductStock")
public boolean deductStockPrepare(BusinessActionContext actionContext,
@BusinessActionContextParameter(paramName = "items")
List<CreateOrderItemDTO> items) {
log.info("TCC第一阶段:预扣库存, xid={}", actionContext.getXid());
Map<Long, Integer> lockMap = new HashMap<>();
try {
for (CreateOrderItemDTO item : items) {
// 获取分布式锁
String lockKey = "inventory:lock:" + item.getProductId();
RLock lock = redissonClient.getLock(lockKey);
boolean locked = lock.tryLock(5, 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("锁定库存失败");
}
// 预扣库存
int affected = inventoryMapper.lockStock(
item.getProductId(),
item.getQuantity(),
actionContext.getXid());
if (affected == 0) {
throw new BusinessException("库存不足");
}
lockMap.put(item.getProductId(), item.getQuantity());
// 记录库存锁定日志
InventoryLockLog lockLog = new InventoryLockLog();
lockLog.setProductId(item.getProductId());
lockLog.setOrderXid(actionContext.getXid());
lockLog.setQuantity(item.getQuantity());
lockLog.setStatus(0); // 锁定状态
lockLog.setCreateTime(LocalDateTime.now());
inventoryLockLogMapper.insert(lockLog);
}
// 将锁定信息保存到上下文
actionContext.setActionContext("lockMap", lockMap);
return true;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new BusinessException("锁定库存中断", e);
}
}
/**
* TCC第二阶段提交
*/
public boolean commitDeductStock(BusinessActionContext actionContext) {
log.info("TCC第二阶段提交:确认扣减库存, xid={}", actionContext.getXid());
// 更新库存锁定状态为已确认
inventoryLockLogMapper.updateStatusByXid(actionContext.getXid(), 1);
// 实际扣减库存
Map<Long, Integer> lockMap = (Map<Long, Integer>)
actionContext.getActionContext("lockMap");
for (Map.Entry<Long, Integer> entry : lockMap.entrySet()) {
inventoryMapper.confirmDeductStock(entry.getKey(), entry.getValue());
}
return true;
}
/**
* TCC第二阶段回滚
*/
public boolean rollbackDeductStock(BusinessActionContext actionContext) {
log.info("TCC第二阶段回滚:释放库存, xid={}", actionContext.getXid());
// 更新库存锁定状态为已回滚
inventoryLockLogMapper.updateStatusByXid(actionContext.getXid(), 2);
// 释放锁定的库存
Map<Long, Integer> lockMap = (Map<Long, Integer>)
actionContext.getActionContext("lockMap");
for (Map.Entry<Long, Integer> entry : lockMap.entrySet()) {
inventoryMapper.releaseLockedStock(entry.getKey(), entry.getValue());
}
return true;
}
}
// 库存Mapper
@Mapper
public interface InventoryMapper {
// 锁定库存
@Update("UPDATE tb_inventory SET locked_stock = locked_stock + #{quantity}, " +
"available_stock = available_stock - #{quantity} " +
"WHERE product_id = #{productId} AND available_stock >= #{quantity}")
int lockStock(@Param("productId") Long productId,
@Param("quantity") Integer quantity,
@Param("xid") String xid);
// 确认扣减库存
@Update("UPDATE tb_inventory SET locked_stock = locked_stock - #{quantity}, " +
"total_stock = total_stock - #{quantity} " +
"WHERE product_id = #{productId} AND locked_stock >= #{quantity}")
int confirmDeductStock(@Param("productId") Long productId,
@Param("quantity") Integer quantity);
// 释放锁定的库存
@Update("UPDATE tb_inventory SET locked_stock = locked_stock - #{quantity}, " +
"available_stock = available_stock + #{quantity} " +
"WHERE product_id = #{productId} AND locked_stock >= #{quantity}")
int releaseLockedStock(@Param("productId") Long productId,
@Param("quantity") Integer quantity);
}
5.3 订单超时取消(RocketMQ延迟消息)
订单超时取消实现:
java
// 订单超时服务
package com.ecommerce.order.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
@RequiredArgsConstructor
public class OrderTimeoutService {
private final RocketMQTemplate rocketMQTemplate;
private final OrderMapper orderMapper;
private final DelayQueue<OrderTimeoutTask> timeoutQueue = new DelayQueue<>();
/**
* 启动订单超时检查
*/
@PostConstruct
public void init() {
// 启动后台线程处理超时订单
new Thread(this::processTimeoutOrders).start();
// 从数据库加载待支付的超时订单
loadPendingOrders();
}
/**
* 添加订单超时任务
*/
public void addOrderTimeoutTask(Long orderId, LocalDateTime createTime) {
// 计算延迟时间(30分钟)
long delay = TimeUnit.MINUTES.toMillis(30) -
System.currentTimeMillis() -
createTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
if (delay > 0) {
OrderTimeoutTask task = new OrderTimeoutTask(orderId, delay);
timeoutQueue.put(task);
log.info("添加订单超时任务: orderId={}, delay={}ms", orderId, delay);
}
}
/**
* 处理超时订单
*/
private void processTimeoutOrders() {
while (!Thread.currentThread().isInterrupted()) {
try {
OrderTimeoutTask task = timeoutQueue.take();
cancelTimeoutOrder(task.getOrderId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("订单超时处理线程中断", e);
}
}
}
/**
* 取消超时订单
*/
private void cancelTimeoutOrder(Long orderId) {
try {
Order order = orderMapper.selectById(orderId);
if (order == null || order.getStatus() != OrderStatus.PENDING_PAYMENT.getCode()) {
return;
}
// 检查是否真的超时(防止重复处理)
LocalDateTime now = LocalDateTime.now();
LocalDateTime createTime = order.getCreateTime();
long minutes = Duration.between(createTime, now).toMinutes();
if (minutes >= 30) {
log.info("取消超时订单: orderId={}, createTime={}", orderId, createTime);
// 更新订单状态为已取消
order.setStatus(OrderStatus.CANCELLED.getCode());
order.setCancelTime(now);
order.setCancelReason("超时未支付");
orderMapper.updateById(order);
// 释放库存
releaseOrderStock(orderId);
// 发送订单取消消息
sendOrderCanceledMessage(order);
}
} catch (Exception e) {
log.error("取消超时订单失败: orderId={}", orderId, e);
}
}
/**
* 发送延迟消息(RocketMQ方式)
*/
public void sendOrderTimeoutMessage(Long orderId) {
// 发送30分钟后的延迟消息
rocketMQTemplate.syncSend("ORDER_TIMEOUT_TOPIC",
MessageBuilder.withPayload(orderId).build(),
TimeUnit.MINUTES.toMillis(30),
TimeUnit.MILLISECONDS);
log.info("发送订单超时延迟消息: orderId={}", orderId);
}
/**
* 监听订单超时消息
*/
@RocketMQMessageListener(
topic = "ORDER_TIMEOUT_TOPIC",
consumerGroup = "order-timeout-consumer"
)
@Slf4j
@Component
public class OrderTimeoutConsumer implements RocketMQListener<Long> {
@Override
public void onMessage(Long orderId) {
log.info("收到订单超时消息: orderId={}", orderId);
cancelTimeoutOrder(orderId);
}
}
}
// 订单超时任务
@Data
class OrderTimeoutTask implements Delayed {
private final Long orderId;
private final long expireTime;
public OrderTimeoutTask(Long orderId, long delay) {
this.orderId = orderId;
this.expireTime = System.currentTimeMillis() + delay;
}
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(expireTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
@Override
public int compareTo(Delayed o) {
return Long.compare(this.expireTime, ((OrderTimeoutTask) o).expireTime);
}
}
第六章:支付服务与消息队列
6.1 支付服务集成
支付服务配置:
java
// 支付服务接口
package com.ecommerce.payment.service;
import com.ecommerce.payment.dto.*;
public interface PaymentService {
// 创建支付
PaymentDTO createPayment(CreatePaymentDTO createPaymentDTO);
// 查询支付状态
PaymentDTO queryPayment(String paymentNo);
// 支付回调处理
PaymentCallbackResult handlePaymentCallback(PaymentCallbackDTO callbackDTO);
// 退款
RefundDTO refund(RefundRequest refundRequest);
// 查询退款状态
RefundDTO queryRefund(String refundNo);
}
// 支付策略接口
package com.ecommerce.payment.strategy;
public interface PaymentStrategy {
// 支付类型
PaymentType getPaymentType();
// 创建支付
PaymentDTO createPayment(CreatePaymentDTO createPaymentDTO);
// 查询支付
PaymentDTO queryPayment(String paymentNo);
// 处理回调
PaymentCallbackResult handleCallback(PaymentCallbackDTO callbackDTO);
// 退款
RefundDTO refund(RefundRequest refundRequest);
}
// 微信支付实现
@Service
@Slf4j
public class WechatPaymentStrategy implements PaymentStrategy {
@Override
public PaymentType getPaymentType() {
return PaymentType.WECHAT;
}
@Override
public PaymentDTO createPayment(CreatePaymentDTO createPaymentDTO) {
log.info("微信支付创建: orderNo={}, amount={}",
createPaymentDTO.getOrderNo(),
createPaymentDTO.getAmount());
// 调用微信支付API
WechatPaymentRequest request = buildWechatRequest(createPaymentDTO);
WechatPaymentResponse response = wechatClient.createPayment(request);
// 保存支付记录
Payment payment = savePaymentRecord(createPaymentDTO, response);
PaymentDTO paymentDTO = convertToDTO(payment);
paymentDTO.setPayData(response.getPayData()); // 支付参数(用于前端调起支付)
return paymentDTO;
}
@Override
public PaymentCallbackResult handleCallback(PaymentCallbackDTO callbackDTO) {
log.info("处理微信支付回调: {}", callbackDTO);
// 验证签名
if (!verifySignature(callbackDTO)) {
throw new BusinessException("签名验证失败");
}
// 解析回调数据
WechatCallbackData callbackData = parseCallbackData(callbackDTO);
// 更新支付状态
Payment payment = updatePaymentStatus(callbackData);
// 发送支付成功消息
if (payment.getStatus() == PaymentStatus.SUCCESS.getCode()) {
sendPaymentSuccessMessage(payment);
}
return buildCallbackResult(callbackData);
}
private WechatPaymentRequest buildWechatRequest(CreatePaymentDTO createPaymentDTO) {
WechatPaymentRequest request = new WechatPaymentRequest();
request.setAppId(wechatConfig.getAppId());
request.setMchId(wechatConfig.getMchId());
request.setNonceStr(RandomUtil.randomString(32));
request.setBody("商品购买-" + createPaymentDTO.getOrderNo());
request.setOutTradeNo(createPaymentDTO.getPaymentNo());
request.setTotalFee(createPaymentDTO.getAmount().multiply(new BigDecimal(100)).intValue());
request.setSpbillCreateIp(createPaymentDTO.getClientIp());
request.setNotifyUrl(wechatConfig.getNotifyUrl());
request.setTradeType("JSAPI");
request.setOpenid(createPaymentDTO.getOpenId());
// 生成签名
String sign = generateSignature(request);
request.setSign(sign);
return request;
}
}
支付状态同步(消息队列):
java
// 支付成功消息生产者
@Service
@Slf4j
@RequiredArgsConstructor
public class PaymentMessageProducer {
private final RocketMQTemplate rocketMQTemplate;
/**
* 发送支付成功消息
*/
public void sendPaymentSuccessMessage(Payment payment) {
PaymentSuccessMessage message = new PaymentSuccessMessage();
message.setPaymentId(payment.getId());
message.setOrderNo(payment.getOrderNo());
message.setPaymentNo(payment.getPaymentNo());
message.setAmount(payment.getAmount());
message.setPaymentTime(payment.getPaymentTime());
message.setPaymentType(payment.getPaymentType());
// 发送顺序消息,确保同一个订单的消息有序处理
rocketMQTemplate.syncSendOrderly(
"PAYMENT_SUCCESS_TOPIC",
message,
payment.getOrderNo()
);
log.info("发送支付成功消息: paymentId={}, orderNo={}",
payment.getId(), payment.getOrderNo());
}
/**
* 发送支付失败消息
*/
public void sendPaymentFailedMessage(Payment payment, String failReason) {
PaymentFailedMessage message = new PaymentFailedMessage();
message.setPaymentId(payment.getId());
message.setOrderNo(payment.getOrderNo());
message.setPaymentNo(payment.getPaymentNo());
message.setAmount(payment.getAmount());
message.setFailReason(failReason);
message.setFailTime(LocalDateTime.now());
rocketMQTemplate.syncSend("PAYMENT_FAILED_TOPIC", message);
log.info("发送支付失败消息: paymentId={}, orderNo={}, reason={}",
payment.getId(), payment.getOrderNo(), failReason);
}
}
// 订单服务消费者
@Slf4j
@Component
@RocketMQMessageListener(
topic = "PAYMENT_SUCCESS_TOPIC",
consumerGroup = "order-payment-consumer",
consumeMode = ConsumeMode.ORDERLY, // 顺序消费
messageModel = MessageModel.CLUSTERING
)
public class PaymentSuccessConsumer implements RocketMQListener {
private final OrderService orderService;
@Override
public void onMessage(PaymentSuccessMessage message) {
log.info("收到支付成功消息: orderNo={}, paymentNo={}",
message.getOrderNo(), message.getPaymentNo());
try {
// 更新订单状态为已支付
orderService.updateOrderStatus(
message.getOrderNo(),
OrderStatus.PAID,
"系统",
"支付成功"
);
// 发送订单支付成功通知
sendOrderPaidNotification(message);
} catch (Exception e) {
log.error("处理支付成功消息失败: orderNo={}", message.getOrderNo(), e);
// 记录失败,人工干预或重试
}
}
}
// 库存服务消费者
@Slf4j
@Component
@RocketMQMessageListener(
topic = "PAYMENT_SUCCESS_TOPIC",
consumerGroup = "inventory-payment-consumer",
consumeMode = ConsumeMode.ORDERLY,
messageModel = MessageModel.CLUSTERING
)
public class InventoryPaymentConsumer implements RocketMQListener {
private final InventoryService inventoryService;
@Override
public void onMessage(PaymentSuccessMessage message) {
log.info("库存服务收到支付成功消息: orderNo={}", message.getOrderNo());
try {
// 确认扣减库存(从锁定状态转为已售出)
inventoryService.confirmStock(message.getOrderNo());
} catch (Exception e) {
log.error("库存服务处理支付消息失败: orderNo={}", message.getOrderNo(), e);
// 发送库存处理失败消息,触发补偿机制
sendInventoryProcessFailedMessage(message, e.getMessage());
}
}
}
第七章:网关与安全认证
7.1 Spring Cloud Gateway网关配置
网关路由配置:
yaml
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
# 用户服务
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/user/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
key-resolver: "#{@userKeyResolver}"
# 商品服务
- id: product-service
uri: lb://product-service
predicates:
- Path=/api/product/**
filters:
- StripPrefix=1
- name: CircuitBreaker
args:
name: productService
fallbackUri: forward:/fallback/product
# 订单服务
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
- Method=POST,PUT,GET
filters:
- StripPrefix=1
- name: AuthFilter
# 支付服务
- id: payment-service
uri: lb://payment-service
predicates:
- Path=/api/payment/**
filters:
- StripPrefix=1
- name: AuthFilter
# 认证服务
- id: auth-service
uri: lb://auth-service
predicates:
- Path=/api/auth/**
filters:
- StripPrefix=1
# 静态资源
- id: static-resource
uri: lb://user-service
predicates:
- Path=/static/**
# 管理端点
- id: actuator
uri: lb://${spring.application.name}
predicates:
- Path=/actuator/**
filters:
- name: AuthFilter
args:
require-role: ADMIN
网关限流配置
redis:
rate-limiter:
enabled: true
replenish-rate: 10 # 每秒令牌数
burst-capacity: 20 # 令牌桶容量
网关断路器配置
resilience4j:
circuitbreaker:
instances:
productService:
sliding-window-size: 10
minimum-number-of-calls: 5
permitted-number-of-calls-in-half-open-state: 3
wait-duration-in-open-state: 5s
failure-rate-threshold: 50
网关过滤器:
java
// 认证过滤器
@Component
@Slf4j
public class AuthFilter implements GlobalFilter, Ordered {
private final JwtUtil jwtUtil;
private final RedisTemplate<String, Object> redisTemplate;
// 白名单路径
private static final List<String> WHITE_LIST = Arrays.asList(
"/api/auth/login",
"/api/auth/register",
"/api/product/list",
"/api/product/detail",
"/static/",
"/actuator/health"
);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getPath().value();
String method = request.getMethod().name();
// 检查是否在白名单
if (isWhiteList(path, method)) {
return chain.filter(exchange);
}
// 获取token
String token = getToken(request);
if (StringUtils.isBlank(token)) {
return unauthorized(exchange, "缺少认证令牌");
}
// 验证token
if (!jwtUtil.validateToken(token)) {
return unauthorized(exchange, "令牌无效或已过期");
}
// 检查token是否在黑名单中
if (isTokenBlacklisted(token)) {
return unauthorized(exchange, "令牌已失效");
}
// 获取用户信息
UserInfo userInfo = jwtUtil.getUserInfo(token);
// 检查权限
if (!hasPermission(userInfo, path, method)) {
return forbidden(exchange, "没有访问权限");
}
// 将用户信息添加到请求头
ServerHttpRequest mutatedRequest = request.mutate()
.header("X-User-Id", String.valueOf(userInfo.getUserId()))
.header("X-User-Name", userInfo.getUsername())
.header("X-User-Roles", String.join(",", userInfo.getRoles()))
.build();
// 记录访问日志
logAccess(request, userInfo);
return chain.filter(exchange.mutate().request(mutatedRequest).build());
}
private boolean isWhiteList(String path, String method) {
return WHITE_LIST.stream().anyMatch(path::startsWith) ||
(path.startsWith("/api/product/") && "GET".equals(method));
}
private String getToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return request.getQueryParams().getFirst("token");
}
private boolean isTokenBlacklisted(String token) {
String key = "blacklist:token:" + DigestUtils.md5Hex(token);
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
private boolean hasPermission(UserInfo userInfo, String path, String method) {
// 简单的权限检查,实际项目中可以使用RBAC模型
if (path.startsWith("/api/admin/") && !userInfo.getRoles().contains("ADMIN")) {
return false;
}
if (path.startsWith("/api/order/") && method.equals("POST") &&
!userInfo.getRoles().contains("USER")) {
return false;
}
return true;
}
private void logAccess(ServerHttpRequest request, UserInfo userInfo) {
String logMsg = String.format("网关访问: user=%s, method=%s, path=%s, ip=%s",
userInfo.getUsername(),
request.getMethod(),
request.getPath(),
request.getRemoteAddress());
if (log.isDebugEnabled()) {
log.debug(logMsg);
}
}
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ApiResponse<?> apiResponse = ApiResponse.error(401, message);
byte[] bytes = JsonUtil.toJson(apiResponse).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
private Mono<Void> forbidden(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
ApiResponse<?> apiResponse = ApiResponse.error(403, message);
byte[] bytes = JsonUtil.toJson(apiResponse).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
}
@Override
public int getOrder() {
return -100;
}
}
// 限流Key解析器
@Component
public class UserKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
// 根据用户ID限流
String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
if (StringUtils.isNotBlank(userId)) {
return Mono.just(userId);
}
// 根据IP限流
String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress())
.getAddress().getHostAddress();
return Mono.just(ip);
}
}
第八章:监控与部署
8.1 完整的监控体系
Prometheus配置:
yaml
prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
alerting:
alertmanagers:
-
static_configs:
-
targets: ['alertmanager:9093']
rule_files:
- "alert_rules.yml"
scrape_configs:
-
job_name: 'spring-boot-apps'
metrics_path: '/actuator/prometheus'
scrape_interval: 10s
static_configs:
- targets:
- 'gateway-service:8080'
- 'user-service:8082'
- 'product-service:8083'
- 'order-service:8085'
- 'payment-service:8086'
labels:
group: 'e-commerce-services'
- targets:
-
job_name: 'nacos'
static_configs:
- targets: ['nacos:8848']
-
job_name: 'redis'
static_configs:
- targets: ['redis-exporter:9121']
-
job_name: 'mysql'
static_configs:
- targets: ['mysqld-exporter:9104']
-
job_name: 'rocketmq'
static_configs:
- targets: ['rocketmq-exporter:5557']
告警规则
alert_rules.yml
groups:
- name: e-commerce-alerts
rules:-
alert: HighErrorRate
expr: rate(http_server_requests_seconds_count{status!~"2..."}[5m]) > 0.1
for: 1m
labels:
severity: warning
annotations:
summary: "高错误率报警"
description: "{{ $labels.instance }} 错误率超过10%"
-
alert: HighLatency
expr: histogram_quantile(0.95, rate(http_server_requests_seconds_bucket[5m])) > 1
for: 2m
labels:
severity: warning
annotations:
summary: "高延迟报警"
description: "{{ $labels.instance }} 95分位延迟超过1秒"
-
alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "服务下线报警"
description: "{{ $labels.instance }} 服务已下线"
-
alert: HighMemoryUsage
expr: (sum(jvm_memory_used_bytes) by (instance) / sum(jvm_memory_max_bytes) by (instance)) * 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "高内存使用率"
description: "{{ $labels.instance }} 内存使用率超过80%"
-
alert: HighCPUUsage
expr: process_cpu_usage * 100 > 80
for: 5m
labels:
severity: warning
annotations:
summary: "高CPU使用率"
description: "{{ $labels.instance }} CPU使用率超过80%"
Spring Boot Actuator指标:
-
java
// 自定义业务指标
@Component
public class BusinessMetrics {
private final MeterRegistry meterRegistry;
// 订单相关指标
private final Counter orderCreatedCounter;
private final Counter orderPaidCounter;
private final Counter orderCanceledCounter;
private final Timer orderProcessTimer;
// 支付相关指标
private final Counter paymentSuccessCounter;
private final Counter paymentFailedCounter;
private final DistributionSummary paymentAmountSummary;
// 用户相关指标
private final Gauge activeUsersGauge;
private final AtomicInteger activeUsers = new AtomicInteger(0);
public BusinessMetrics(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// 初始化计数器
this.orderCreatedCounter = Counter.builder("ecommerce.orders.created")
.description("创建订单总数")
.tag("application", "order-service")
.register(meterRegistry);
this.orderPaidCounter = Counter.builder("ecommerce.orders.paid")
.description("支付成功订单数")
.tag("application", "order-service")
.register(meterRegistry);
this.orderCanceledCounter = Counter.builder("ecommerce.orders.canceled")
.description("取消订单数")
.tag("application", "order-service")
.register(meterRegistry);
// 初始化计时器
this.orderProcessTimer = Timer.builder("ecommerce.orders.process.time")
.description("订单处理时间")
.publishPercentiles(0.5, 0.95, 0.99)
.register(meterRegistry);
// 初始化支付指标
this.paymentSuccessCounter = Counter.builder("ecommerce.payments.success")
.description("支付成功数")
.tag("application", "payment-service")
.register(meterRegistry);
this.paymentFailedCounter = Counter.builder("ecommerce.payments.failed")
.description("支付失败数")
.tag("application", "payment-service")
.register(meterRegistry);
this.paymentAmountSummary = DistributionSummary.builder("ecommerce.payments.amount")
.description("支付金额分布")
.baseUnit("CNY")
.register(meterRegistry);
// 初始化计量器
this.activeUsersGauge = Gauge.builder("ecommerce.users.active", activeUsers, AtomicInteger::get)
.description("活跃用户数")
.register(meterRegistry);
}
public void recordOrderCreated(OrderDTO order) {
orderCreatedCounter.increment();
// 记录订单金额
meterRegistry.summary("ecommerce.orders.amount")
.tag("status", "created")
.record(order.getPayAmount().doubleValue());
}
public void recordOrderPaid(OrderDTO order, long processTime) {
orderPaidCounter.increment();
orderProcessTimer.record(processTime, TimeUnit.MILLISECONDS);
meterRegistry.summary("ecommerce.orders.amount")
.tag("status", "paid")
.record(order.getPayAmount().doubleValue());
}
public void recordPaymentSuccess(PaymentDTO payment) {
paymentSuccessCounter.increment();
paymentAmountSummary.record(payment.getAmount().doubleValue());
}
public void recordPaymentFailed(PaymentDTO payment, String reason) {
paymentFailedCounter.increment();
meterRegistry.counter("ecommerce.payments.failed.reason",
"reason", reason).increment();
}
public void incrementActiveUsers() {
activeUsers.incrementAndGet();
}
public void decrementActiveUsers() {
activeUsers.decrementAndGet();
}
}
// 在业务代码中使用
@Service
@RequiredArgsConstructor
public class OrderService {
private final BusinessMetrics businessMetrics;
public OrderDTO createOrder(CreateOrderDTO createOrderDTO) {
long startTime = System.currentTimeMillis();
try {
// 创建订单逻辑...
OrderDTO order = createOrderInternal(createOrderDTO);
// 记录指标
businessMetrics.recordOrderCreated(order);
return order;
} finally {
long processTime = System.currentTimeMillis() - startTime;
businessMetrics.recordOrderProcessTime(processTime);
}
}
}
8.2 Docker与Kubernetes部署
Docker Compose完整部署:
yaml
version: '3.8'
services:
基础服务
mysql:
image: mysql:8.0
container_name: ecommerce-mysql
environment:
MYSQL_ROOT_PASSWORD: ecommerce123
MYSQL_DATABASE: ecommerce
ports:
- "3306:3306"
volumes:
-
mysql_data:/var/lib/mysql
-
./sql/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- ecommerce-network
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
timeout: 20s
retries: 10
redis:
image: redis:7-alpine
container_name: ecommerce-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
networks:
- ecommerce-network
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
nacos:
image: nacos/nacos-server:v2.2.0
container_name: ecommerce-nacos
environment:
-
MODE=standalone
-
SPRING_DATASOURCE_PLATFORM=mysql
-
MYSQL_SERVICE_HOST=mysql
-
MYSQL_SERVICE_DB_NAME=nacos
-
MYSQL_SERVICE_PORT=3306
-
MYSQL_SERVICE_USER=root
-
MYSQL_SERVICE_PASSWORD=ecommerce123
-
NACOS_AUTH_ENABLE=true
ports:
-
"8848:8848"
-
"9848:9848"
-
"9849:9849"
volumes:
- nacos_logs:/home/nacos/logs
depends_on:
mysql:
condition: service_healthy
networks:
- ecommerce-network
seata:
image: seataio/seata-server:1.7.1
container_name: ecommerce-seata
environment:
-
SEATA_PORT=8091
-
STORE_MODE=db
-
SEATA_IP=seata
ports:
- "8091:8091"
volumes:
- ./config/seata/registry.conf:/seata-server/resources/registry.conf
depends_on:
-
mysql
-
nacos
networks:
- ecommerce-network
rocketmq:
image: apache/rocketmq:5.1.0
container_name: ecommerce-rocketmq
ports:
-
"9876:9876"
-
"10909:10909"
-
"10911:10911"
volumes:
-
rocketmq_logs:/home/rocketmq/logs
-
rocketmq_store:/home/rocketmq/store
networks:
- ecommerce-network
监控服务
prometheus:
image: prom/prometheus:v2.44.0
container_name: ecommerce-prometheus
ports:
- "9090:9090"
volumes:
-
./config/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
-
prometheus_data:/prometheus
command:
-
'--config.file=/etc/prometheus/prometheus.yml'
-
'--storage.tsdb.path=/prometheus'
-
'--web.console.libraries=/etc/prometheus/console_libraries'
-
'--web.console.templates=/etc/prometheus/consoles'
-
'--storage.tsdb.retention.time=200h'
-
'--web.enable-lifecycle'
networks:
- ecommerce-network
grafana:
image: grafana/grafana:9.5.2
container_name: ecommerce-grafana
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin123
volumes:
-
grafana_data:/var/lib/grafana
-
./config/grafana/dashboards:/etc/grafana/provisioning/dashboards
-
./config/grafana/datasources:/etc/grafana/provisioning/datasources
depends_on:
- prometheus
networks:
- ecommerce-network
应用服务
gateway:
build:
context: ./e-commerce-parent/e-commerce-gateway
dockerfile: Dockerfile
container_name: ecommerce-gateway
ports:
- "8080:8080"
environment:
-
SPRING_PROFILES_ACTIVE=docker
-
SPRING_CLOUD_NACOS_SERVER_ADDR=nacos:8848
-
SPRING_CLOUD_NACOS_USERNAME=nacos
-
SPRING_CLOUD_NACOS_PASSWORD=nacos
-
SPRING_REDIS_HOST=redis
-
SPRING_REDIS_PORT=6379
depends_on:
nacos:
condition: service_healthy
redis:
condition: service_healthy
networks:
- ecommerce-network
deploy:
replicas: 2
user-service:
build:
context: ./e-commerce-parent/e-commerce-user
dockerfile: Dockerfile
container_name: ecommerce-user
ports:
- "8082:8082"
environment:
-
SPRING_PROFILES_ACTIVE=docker
-
SPRING_CLOUD_NACOS_SERVER_ADDR=nacos:8848
-
SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/user_db
-
SPRING_DATASOURCE_USERNAME=root
-
SPRING_DATASOURCE_PASSWORD=ecommerce123
-
SPRING_REDIS_HOST=redis
-
SPRING_REDIS_PORT=6379
depends_on:
-
nacos
-
mysql
-
redis
networks:
- ecommerce-network
deploy:
replicas: 3
product-service:
build:
context: ./e-commerce-parent/e-commerce-product
dockerfile: Dockerfile
container_name: ecommerce-product
ports:
- "8083:8083"
environment:
-
SPRING_PROFILES_ACTIVE=docker
-
SPRING_CLOUD_NACOS_SERVER_ADDR=nacos:8848
-
SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/product_db
-
SPRING_DATASOURCE_USERNAME=root
-
SPRING_DATASOURCE_PASSWORD=ecommerce123
-
SPRING_REDIS_HOST=redis
-
SPRING_REDIS_PORT=6379
-
SPRING_ELASTICSEARCH_HOST=elasticsearch
-
SPRING_ELASTICSEARCH_PORT=9200
depends_on:
-
nacos
-
mysql
-
redis
networks:
- ecommerce-network
deploy:
replicas: 3
order-service:
build:
context: ./e-commerce-parent/e-commerce-order
dockerfile: Dockerfile
container_name: ecommerce-order
ports:
- "8085:8085"
environment:
-
SPRING_PROFILES_ACTIVE=docker
-
SPRING_CLOUD_NACOS_SERVER_ADDR=nacos:8848
-
SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/order_db
-
SPRING_DATASOURCE_USERNAME=root
-
SPRING_DATASOURCE_PASSWORD=ecommerce123
-
SPRING_REDIS_HOST=redis
-
SPRING_REDIS_PORT=6379
-
SEATA_SERVER_HOST=seata
-
ROCKETMQ_NAME_SERVER=rocketmq:9876
depends_on:
-
nacos
-
mysql
-
redis
-
seata
-
rocketmq
networks:
- ecommerce-network
deploy:
replicas: 3
networks:
ecommerce-network:
driver: bridge
volumes:
mysql_data:
redis_data:
nacos_logs:
rocketmq_logs:
rocketmq_store:
prometheus_data:
grafana_data:
Kubernetes部署文件:
yaml
namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: ecommerce
configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: ecommerce-config
namespace: ecommerce
data:
application.yml: |
spring:
cloud:
nacos:
discovery:
server-addr: nacos:8848
config:
server-addr: nacos:8848
redis:
host: redis
port: 6379
secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: ecommerce-secrets
namespace: ecommerce
type: Opaque
data:
mysql-password: ZWNvbW1lcmNlMTIz # ecommerce123 base64编码
nacos-password: bmFjb3M= # nacos base64编码
deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: user-service
namespace: ecommerce
labels:
app: user-service
spec:
replicas: 3
selector:
matchLabels:
app: user-service
template:
metadata:
labels:
app: user-service
spec:
containers:
- name: user-service
image: ecommerce/user-service:1.0.0
ports: - containerPort: 8082
env: - name: SPRING_PROFILES_ACTIVE
value: "k8s" - name: SPRING_DATASOURCE_URL
value: "jdbc:mysql://mysql:3306/user_db" - name: SPRING_DATASOURCE_USERNAME
value: "root" - name: SPRING_DATASOURCE_PASSWORD
valueFrom:
secretKeyRef:
name: ecommerce-secrets
key: mysql-password
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8082
initialDelaySeconds: 60
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8082
initialDelaySeconds: 30
periodSeconds: 5
service.yaml
apiVersion: v1
kind: Service
metadata:
name: user-service
namespace: ecommerce
spec:
selector:
app: user-service
ports:
- port: 8082
targetPort: 8082
type: ClusterIP
ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ecommerce-ingress
namespace: ecommerce
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/ssl-redirect: "false"
spec:
rules:
- host: api.ecommerce.com
http:
paths:- path: /user
pathType: Prefix
backend:
service:
name: user-service
port:
number: 8082 - path: /product
pathType: Prefix
backend:
service:
name: product-service
port:
number: 8083 - path: /order
pathType: Prefix
backend:
service:
name: order-service
port:
number: 8085
- path: /user
hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: user-service-hpa
namespace: ecommerce
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: user-service
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70 - type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
第九章:项目总结与扩展
9.1 项目架构总结
已实现的核心功能:
text
✅ 用户服务:注册、登录、用户管理
✅ 商品服务:商品管理、搜索、缓存
✅ 订单服务:创建订单、状态管理、分布式事务
✅ 支付服务:支付集成、回调处理
✅ 库存服务:库存管理、分布式锁
✅ 优惠券服务:优惠券管理、使用
✅ 网关服务:路由、认证、限流、熔断
✅ 配置中心:统一配置管理
✅ 服务注册发现:Nacos服务治理
✅ 监控告警:Prometheus + Grafana
✅ 链路追踪:Sleuth + Zipkin
✅ 消息队列:RocketMQ异步处理
✅ 分布式事务:Seata TCC模式
✅ 容器化部署:Docker + Kubernetes
性能优化点:
text
- 缓存策略:多级缓存(Redis + 本地缓存)
- 数据库优化:读写分离、分库分表
- 异步处理:消息队列解耦
- 限流熔断:保护核心服务
- 分布式锁:保证数据一致性
- 连接池优化:数据库、Redis连接池
- JVM优化:GC调优、内存配置
- 静态资源:CDN加速
9.2 扩展功能建议 - 秒杀系统:
java
// 秒杀服务设计
@Service
public class SeckillService {
// 1. 库存预热到Redis
public void preheatStock(Long productId, Integer stock) {
redisTemplate.opsForValue().set("seckill:stock:" + productId, stock);
}
// 2. 用户请求排队
public SeckillResult seckill(Long userId, Long productId) {
// 验证用户资格
if (!checkUserQualification(userId)) {
return SeckillResult.fail("用户资格不符");
}
// 验证秒杀时间
if (!checkSeckillTime(productId)) {
return SeckillResult.fail("秒杀未开始或已结束");
}
// 获取Redis分布式锁
RLock lock = redissonClient.getLock("seckill:lock:" + productId);
try {
boolean acquired = lock.tryLock(100, 10, TimeUnit.MILLISECONDS);
if (!acquired) {
return SeckillResult.fail("系统繁忙");
}
// 预减库存
Long stock = redisTemplate.opsForValue().decrement("seckill:stock:" + productId);
if (stock == null || stock < 0) {
// 库存不足,恢复库存
redisTemplate.opsForValue().increment("seckill:stock:" + productId);
return SeckillResult.fail("库存不足");
}
// 生成秒杀订单号
String seckillOrderNo = generateSeckillOrderNo();
// 发送消息到MQ,异步创建订单
sendSeckillOrderMessage(userId, productId, seckillOrderNo);
return SeckillResult.success(seckillOrderNo);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return SeckillResult.fail("系统异常");
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
- 推荐系统:
java
// 基于用户行为的推荐
@Service
public class RecommendationService {
// 协同过滤推荐
public List<ProductDTO> recommendByCF(Long userId) {
// 1. 获取用户历史行为
List<UserBehavior> behaviors = getUserBehaviors(userId);
// 2. 计算用户相似度
Map<Long, Double> userSimilarities = calculateUserSimilarities(userId);
// 3. 获取相似用户喜欢的商品
Set<Long> recommendedProductIds = getProductsFromSimilarUsers(userSimilarities);
// 4. 过滤已购买商品
recommendedProductIds.removeAll(getPurchasedProducts(userId));
// 5. 获取商品详情
return productService.getProductsByIds(new ArrayList<>(recommendedProductIds));
}
// 实时推荐(基于Redis)
public List<ProductDTO> getRealTimeRecommendations(Long userId) {
String key = "recommend:realtime:" + userId;
// 从Redis获取实时推荐
List<Long> productIds = (List<Long>) redisTemplate.opsForValue().get(key);
if (productIds == null || productIds.isEmpty()) {
// 重新计算推荐
productIds = calculateRealTimeRecommendations(userId);
redisTemplate.opsForValue().set(key, productIds, 10, TimeUnit.MINUTES);
}
return productService.getProductsByIds(productIds);
}
}
- 数据统计分析:
java
// 实时数据统计
@Service
public class DataAnalysisService {
// 使用Flink进行实时数据分析
public void realTimeAnalysis() {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
// 从Kafka读取订单数据
DataStream<OrderEvent> orderStream = env
.addSource(new FlinkKafkaConsumer<>(
"order-topic",
new OrderEventSchema(),
properties))
.name("订单数据源");
// 实时统计
orderStream
.keyBy(OrderEvent::getProductId)
.timeWindow(Time.minutes(5))
.aggregate(new ProductSalesAggregator())
.addSink(new RedisSink<>());
env.execute("实时数据分析");
}
// 商品销售聚合器
public static class ProductSalesAggregator
implements AggregateFunction<OrderEvent, ProductSales, ProductSales> {
@Override
public ProductSales createAccumulator() {
return new ProductSales();
}
@Override
public ProductSales add(OrderEvent event, ProductSales accumulator) {
accumulator.setProductId(event.getProductId());
accumulator.setSalesCount(accumulator.getSalesCount() + event.getQuantity());
accumulator.setSalesAmount(accumulator.getSalesAmount() +
event.getPrice() * event.getQuantity());
return accumulator;
}
@Override
public ProductSales getResult(ProductSales accumulator) {
return accumulator;
}
@Override
public ProductSales merge(ProductSales a, ProductSales b) {
a.setSalesCount(a.getSalesCount() + b.getSalesCount());
a.setSalesAmount(a.getSalesAmount() + b.getSalesAmount());
return a;
}
}
}
9.3 学习收获与职业发展
通过本项目你学会了:
✅ 微服务架构设计与拆分
✅ Spring Cloud全家桶实战
✅ 分布式系统核心问题解决方案
✅ 高并发场景下的性能优化
✅ 容器化与云原生部署
✅ 全链路监控与故障排查
✅ 团队协作与项目管理
面试加分项:
text
- 项目经验:完整电商微服务项目
- 技术深度:Spring Cloud源码理解
- 架构能力:系统设计、性能优化
- 解决问题:分布式事务、缓存一致性问题
- 运维能力:Docker、Kubernetes、监控
职业发展建议:
text
初级 → 中级 → 高级 → 架构师 → 技术专家
学习路线:
- 夯实基础:Java、Spring、数据库、网络
- 掌握框架:Spring Cloud、MyBatis、Redis
- 学习架构:微服务、DDD、云原生
- 深入原理:JVM、操作系统、网络原理
- 拓展视野:大数据、AI、区块链
第十章:资源与后续学习
10.1 项目资源下载
GitHub仓库地址:
text
https://github.com/yourusername/e-commerce-microservices
包含内容:
├── 完整源代码
├── 数据库脚本
├── Docker配置文件
├── Kubernetes部署文件
├── 接口文档
├── 架构设计文档
└── 学习路线图
快速启动命令:
bash
1. 克隆项目
git clone https://github.com/yourusername/e-commerce-microservices.git
2. 启动基础设施
cd e-commerce-microservices
docker-compose up -d mysql redis nacos seata rocketmq
3. 导入数据库
mysql -uroot -p < sql/init.sql
4. 启动服务
方式1:IDEA中逐个启动
方式2:使用Maven
mvn spring-boot:run -pl e-commerce-parent/e-commerce-user
mvn spring-boot:run -pl e-commerce-parent/e-commerce-product
...
5. 访问系统
网关:http://localhost:8080
Nacos:http://localhost:8848/nacos (nacos/nacos)
Grafana:http://localhost:3000 (admin/admin123)
10.2 学习路线图
30天学习计划:
text
第1周:Spring Cloud基础
Day1-2:Nacos服务注册与发现
Day3-4:OpenFeign服务调用
Day5-6:Spring Cloud Gateway
Day7:项目搭建与调试
第2周:核心业务开发
Day8-9:用户服务实现
Day10-11:商品服务实现
Day12-13:订单服务实现
Day14:支付服务实现
第3周:高级特性
Day15:分布式事务Seata
Day16:消息队列RocketMQ
Day17:缓存与分布式锁
Day18:监控与链路追踪
Day19:安全与认证
Day20:性能优化
第4周:部署与运维
Day21-22:Docker容器化
Day23-24:Kubernetes部署
Day25-26:CI/CD流水线
Day27-28:压力测试与调优
Day29-30:项目总结与面试准备
10.3 常见问题解答
Q1:项目启动报错怎么办?
text
A:常见问题排查:
- 检查Java版本(需要Java 17+)
- 检查MySQL、Redis、Nacos是否启动
- 检查数据库连接配置
- 检查依赖是否下载完整
- 查看日志文件定位问题
Q2:如何修改配置?
text
A:配置修改位置:
- 公共配置:config/目录下
- 服务配置:bootstrap.yml + application.yml
- 环境配置:通过profile切换
- 运行时配置:通过Nacos动态配置
Q3:如何扩展新功能?
text
A:扩展步骤:
- 在父工程中创建新模块
- 继承公共模块依赖
- 实现业务逻辑
- 注册到Nacos
- 在网关配置路由
- 更新部署配置
结语:从学习者到架构师的蜕变
恭喜你完成了这个完整的电商微服务系统项目!这不仅仅是一个技术项目,更是你技术成长的重要里程碑。
关键收获:
✅ 掌握了企业级微服务架构设计
✅ 理解了分布式系统核心概念
✅ 积累了完整的项目开发经验
✅ 具备了解决复杂问题的能力
✅ 建立了技术自信和学习方法
记住:
技术的学习永无止境,但每个完整的项目都是你职业生涯的坚实基石。
从今天起,你不再只是技术的使用者,而是架构的设计者。
下一步行动:
运行项目:确保所有服务正常运行
深入研究:选择感兴趣的部分深入源码
分享经验:写博客、做分享、帮助他人
参与开源:贡献代码,加入技术社区
持续学习:关注新技术,不断更新知识体系
特别福利:
关注我并私信"电商微服务",获取:
本文完整Markdown版本
项目PPT演示文稿
面试常见问题及答案
扩展功能实现代码
学习社群邀请
今日打卡任务:
👍 点赞本文,支持原创
💾 收藏本文,方便查阅
🚀 运行项目,在评论区分享截图
👨💻 关注我,获取更多实战教程
在评论区告诉我:
你在实现过程中最大的收获是什么?
还希望学习什么类型的项目?
对电商系统有什么改进建议?
让我们一起在技术的道路上不断前行! 🚀🔥
下期预告:
《亿级流量电商系统架构设计与实践》
高并发场景下的架构优化
数据库分库分表实战
缓存穿透、雪崩、击穿解决方案
全链路压测与性能优化
容灾与多活架构设计
关注我,不错过每一篇精品教程! 💪