Spring Cloud实战:电商微服务系统从0到1(25000字终极实战指南)
第一章:项目架构设计与技术选型
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: NACOSHOST:localhost:{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;
}
2. 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);
}
3. 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);
}
4. 控制器层:
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);
}
}
5. 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);
}
2. 商品搜索(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);
}
3. 商品缓存策略(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();
}
}
}
}
2. 推荐系统:
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);
}
}
3. 数据统计分析:
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演示文稿
面试常见问题及答案
扩展功能实现代码
学习社群邀请
今日打卡任务:
👍 点赞本文,支持原创
💾 收藏本文,方便查阅
🚀 运行项目,在评论区分享截图
👨💻 关注我,获取更多实战教程
在评论区告诉我:
你在实现过程中最大的收获是什么?
还希望学习什么类型的项目?
对电商系统有什么改进建议?
让我们一起在技术的道路上不断前行! 🚀🔥
下期预告:
《亿级流量电商系统架构设计与实践》
高并发场景下的架构优化
数据库分库分表实战
缓存穿透、雪崩、击穿解决方案
全链路压测与性能优化
容灾与多活架构设计
关注我,不错过每一篇精品教程! 💪
更多推荐




所有评论(0)