# 谷粒商城
**Repository Path**: zhangshuai841/gulimall_1
## Basic Information
- **Project Name**: 谷粒商城
- **Description**: 仿京东商城 2019
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 9
- **Created**: 2024-09-26
- **Last Updated**: 2024-09-26
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 谷粒商城
> All my effort is the parody of JD.COM since 2019.
>
> 仿京东商城 2019
>
> 
> 
---
[紧急通知] 我的数据库被黑客删除和勒索了,但我不知道怎么取回

> 你需要支付一定的费用,如需恢复请联系我们,发送邮件按格式发送,否则自动过滤。邮件标题:你的数据库IP地址,邮件内容:恢复 bdhuawei@protonmail.com
这是6月7日至11日容器中的 mysql 访问日志。
```log
2022-06-07T00:43:26.990331Z 740 [Warning] [MY-010055] [Server] IP address '139.196.98.39' could not be resolved: Name or service not known
2022-06-07T01:27:30.136113Z 744 [Warning] [MY-010055] [Server] IP address '221.199.70.144' could not be resolved: Name or service not known
⭐ 2022-06-07T01:27:30.154238Z 743 [Warning] [MY-010057] [Server] IP address '171.34.177.27' has been resolved to the host name '27.177.34.171.adsl-pool.jx.chinaunicom.com', which resembles IPv4-address itself.
2022-06-08T03:56:03.021368Z 853 [Warning] [MY-010055] [Server] IP address '101.132.126.80' could not be resolved: Name or service not known
2022-06-08T14:49:16.797060Z 898 [Warning] [MY-010055] [Server] IP address '85.209.42.233' could not be resolved: Name or service not known
2022-06-08T16:08:49.537765Z 905 [Warning] [MY-010055] [Server] IP address '117.61.11.99' could not be resolved: Name or service not known
mbind: Operation not permitted
2022-06-09T03:12:33.700988Z 965 [Warning] [MY-010055] [Server] IP address '36.57.85.252' could not be resolved: Name or service not known
2022-06-09T03:14:36.436575Z 966 [Warning] [MY-010055] [Server] IP address '47.100.194.160' could not be resolved: Name or service not known
2022-06-09T09:38:21.073759Z 993 [Warning] [MY-010055] [Server] IP address '120.77.26.203' could not be resolved: Name or service not known
2022-06-09T14:09:48.908469Z 1046 [Warning] [MY-010056] [Server] Host name '87-243-171-53.ainet.at' could not be resolved: Name or service not known
2022-06-09T14:12:42.736075Z 1047 [Warning] [MY-010055] [Server] IP address '39.170.17.27' could not be resolved: Temporary failure in name resolution
2022-06-09T18:57:11.958536Z 1070 [Warning] [MY-010055] [Server] IP address '39.99.158.136' could not be resolved: Name or service not known
2022-06-09T20:04:28.374306Z 1076 [Warning] [MY-010055] [Server] IP address '36.6.58.31' could not be resolved: Name or service not known
2022-06-09T20:21:27.999047Z 1079 [Warning] [MY-010055] [Server] IP address '47.101.201.167' could not be resolved: Name or service not known
2022-06-10T08:17:51.431031Z 1129 [Warning] [MY-010055] [Server] IP address '103.167.151.206' could not be resolved: Name or service not known
mbind: Operation not permitted
2022-06-10T09:53:54.666360Z 1137 [Warning] [MY-010056] [Server] Host name 'Host-by.nerocloud.io' could not be resolved: Name or service not known
mbind: Operation not permitted
2022-06-10T20:59:35.958942Z 18127 [Warning] [MY-010055] [Server] IP address '8.142.112.1' could not be resolved: Name or service not known
2022-06-10T22:13:21.794196Z 20024 [Warning] [MY-010055] [Server] IP address '47.101.204.96' could not be resolved: Name or service not known
⭐ 2022-06-11T02:08:21.961647Z 26066 [Warning] [MY-010057] [Server] IP address '118.81.0.141' has been resolved to the host name '141.0.81.118.adsl-pool.sx.cn', which resembles IPv4-address itself.
2022-06-11T02:08:27.907632Z 26065 [Warning] [MY-010055] [Server] IP address '1.85.219.13' could not be resolved: Temporary failure in name resolution
2022-06-11T02:13:51.578978Z 26208 [Warning] [MY-010055] [Server] IP address '117.69.183.164' could not be resolved: Name or service not known
2022-06-11T03:34:35.879666Z 28284 [Warning] [MY-010055] [Server] IP address '60.166.164.184' could not be resolved: Name or service not known
2022-06-11T03:48:39.265416Z 28646 [Warning] [MY-010055] [Server] IP address '113.75.138.24' could not be resolved: Name or service not known
2022-06-11T11:02:47.742152Z 39804 [Warning] [MY-010055] [Server] IP address '111.224.7.171' could not be resolved: Name or service not known
2022-06-11T11:02:47.836884Z 39805 [Warning] [MY-010055] [Server] IP address '110.177.176.229' could not be resolved: Name or service not known
2022-06-11T14:03:09.876478Z 44443 [Warning] [MY-010055] [Server] IP address '117.69.128.30' could not be resolved: Name or service not known
2022-06-11T16:07:39.498941Z 47643 [Warning] [MY-010055] [Server] IP address '39.103.163.187' could not be resolved: Name or service not known
2022-06-11T21:01:32.291036Z 55198 [Warning] [MY-010055] [Server] IP address '106.15.192.48' could not be resolved: Name or service not known
```
查看 binlog 日志,它写入 recover 表后,清空了 binlog ,所以不能从 binlog 恢复了。
```log
root@d21c916a0f46:/var/lib/mysql# mysqlbinlog binlog.000001
# The proper term is pseudo_replica_mode, but we use this compatibility alias
# to make the statement usable on server versions 8.0.24 and older.
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;
/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;
DELIMITER /*!*/;
# at 4
⭐ #220610 9:54:03 server id 1 end_log_pos 125 CRC32 0x73d96118 Start: binlog v 4, server v 8.0.26 created 220610 9:54:03 at startup
ROLLBACK/*!*/;
BINLOG '
uxSjYg8BAAAAeQAAAH0AAAAAAAQAOC4wLjI2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAC7FKNiEwANAAgAAAAABAAEAAAAYQAEGggAAAAICAgCAAAACgoKKioAEjQA
CigBGGHZcw==
'/*!*/;
# at 125
#220610 9:54:03 server id 1 end_log_pos 156 CRC32 0x2c1c2a83 Previous-GTIDs
# [empty]
# at 156
#220612 2:57:59 server id 1 end_log_pos 179 CRC32 0x349c52f8 Stop
SET @@SESSION.GTID_NEXT= 'AUTOMATIC' /* added by mysqlbinlog */ /*!*/;
DELIMITER ;
# End of log file
/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;
/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;
```
推测原因:脚本扫描 3036 端口,暴力破解 root 账号。
---

接口文档:https://easydoc.xyz/s/78237135/ZUqEdvA4/hKJTcbfd
视频链接:https://www.bilibili.com/video/BV1np4y1C7Yf?from=search&seid=8989733132604162058
## 项目结构
```bash
❯ tree -L 1
.
├── docs 文档说明
├── gulimall-admin 微服务健康监控中心
├── gulimall-auth 登录鉴权
├── gulimall-cart 购物车
├── gulimall-common 开发脚手架
├── gulimall-coupon 优惠券
├── gulimall-gateway 路由
├── gulimall-member 会员
├── gulimall-order 订单
├── gulimall-plugins 第三方服务
├── gulimall-product 商品
├── gulimall-search 搜索
├── gulimall-seckill 秒杀
├── gulimall-ware 仓库
├── renren-fast 后台管理
├── renren-fast-vue 后台页面
├── renren-generator 低代码开发生成数据库CRUD模型
└── selenium 自动化测试
```
## 项目简介
仿 2019 年京东商城,基于 SpringCloud 微服务架构和 MVC 模式开发的电商系统。完成了商家后台管理商品上架,菜单分类,用户注册和登录,搜索商品、下单购物,支付宝付款等一条龙服务。 拆分成多种类型微服务(按相似技术栈分类):
| 序号 | 微服务 | 技术栈 | 截图 |
|------|-----------|---------------------------------------------------------------|---------------------------|
| 1 | 商品、库存、优惠券 | MySQL 数据库、MyBatisPlus 持久层框架等 |  |
| 2 | 会员 | SpringWeb 拦截器 + JWT鉴权、Oauth2.0 第三方授权 |  |
| 3 | 搜索 | ElasticSearch 检索 |  |
| 4 | 订单、购物车 | Redis 分布式缓存、锁、幂等性校验,RabbitMQ 消息队列 |  |
| 5 | 路由、发现中心 | Alibaba-Nacos 注册中心和配置中心、OpenFeign 微服务通信、SpringBoot-Gateway 路由 |  |
| 6 | 第三方 | oss 图床、短信验证 | |
基础服务设施(技术栈)还有:
| 序号 | 基础设施 | 技术栈 | 截图 |
|-----|-----------|---------------------------------------------------------------|---------------------------|
| 1 | 服务器 | Docker 部署中间件 |  |
| 2 | 测试 | Selenium(Python)单元测试 |  |
| 3 | 压测调试 | JMeter 压力测试、VisualVM 健康检测、Arthas 线上诊断 |  |
| 4 | 后台 | Renren-fast、Renren-fast-vue 后台(商家)管理系统 |  |
| 5 | 通用 | Thymeleaf 页面渲染、Nginx 负载均衡、SpringCloud-Admin 微服务监控中心 |  |
## 线上部署 | 快速开始
1.配置
具体参考教程视频或其他 Gulimall 开源笔记。因为 Nacos 配置中心,管理每个微服务的配置,类多且繁杂,但好在持久化在一个数据库中。本人使用 2G*2 服务器内存太吃紧,所处这里不展开共享测试了。
- [ ] 后续重构项目会将账号信息隐藏后,加上注解公开配置文件,并开源 Notion 笔记开发细节,手摸手教你实现从零到一完成电商系统。
2.部署
数据库和中间件部署在阿里云服务器(1核2G),本机电脑运行所有的 SpringCloud 微服务实例(内存吃紧 5G, 因此没有硬件支撑在线上查看项目),主要受网络时延和中间件影响,压力测试吞吐量 9 /s
- [ ] 如果部署在大内存的服务器,再多几台服务器集群,才能展现它的支撑高并发、高可用的能力(如果我有条件部署的话,再做压力测试数据)
## 项目重构
课程除了讲主流框架的使用场景的解决方案,以及演示基本的增删改查操作,在实现业务逻辑的细节上,观众弹幕评价褒贬不一。
本着简单实用的原则,本人参考了一些商城开源项目的设计(如 yami-shop、renren-fast),以及 Java-Guide 关于 Spring
框架技术文章,尝试从以下几个方向,对项目做了重构(这些都是超出课程,重构上瘾了)。
### 🍽️ Spring AOP 面向切面编程
1. 全局异常:在底层实现尽管抛出,在上层控制层捕获。
1. 它放在公共服务 common 下,基本异常、前端参数校验异常、商城服务自定义异常,这三类基本涵盖所有了。
2. 商城服务自定义异常 GuliMallBindException ,包含了全局通用异常枚举类 BizCodeEnum
```java
/**
* 默认异常处理程序配置
*/
@Controller
@RestControllerAdvice
public class DefaultExceptionHandlerConfig {
@ExceptionHandler(BindException.class)
public ResponseEntity bindExceptionHandler(BindException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getBindingResult().getFieldErrors().get(0).getDefaultMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
e.printStackTrace();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getBindingResult().getFieldErrors().get(0).getDefaultMessage());
}
@ExceptionHandler(GuliMallBindException.class)
public ResponseEntity unauthorizedExceptionHandler(GuliMallBindException e) {
e.printStackTrace();
return ResponseEntity.status(e.getBizCode()).body(e.getMessage());
}
}
```
2. 缓存管理:对缓存的增删改查、令牌校验、幂等性校验
```java
/**
* Redis 购物车商品缓存切面
*/
@Aspect
@Component
@Order(1)
public class OrderRedisAspect {
@Autowired
StringRedisTemplate redisTemplate;
private final String CART_PREFIX = "gulimall:cart:";
@Around(value = "@annotation(idempotent)")
public Object checkIdempotentRedisCache(ProceedingJoinPoint pjp, Idempotent idempotent) throws Throwable {
OrderSubmitVo args = (OrderSubmitVo) pjp.getArgs()[0];
Long execute = deleteKeyIfExistTokenRedis(args);
if (execute == 0L) {
throw new GuliMallBindException("令牌校验失败,请勿重复提交订单");
}
return pjp.proceed();
}
// ...
}
```
3. 消息队列:分离额外推送,业务逻辑代码解耦
```java
/**
* 订单消息队列切面
*/
@Aspect
@Component
@Order(2)
public class OrderRabbitMqAspect {
@Lazy
@Autowired
RabbitTemplate rabbitTemplate;
@AfterReturning(value = "@annotation(postRabbitMq)", returning = "retVal")
public Object sendRabbitMq(JoinPoint point, Object retVal, PostRabbitMq postRabbitMq) {
if (Objects.nonNull(retVal)) {
// 创建订单:发送消息创建完成
OrderEntity order = (OrderEntity) retVal;
pushDelayQueueAfterSubmitOrder(order);
} else {
// 关闭订单:二次确认解锁库存
OrderEntity methodArg = (OrderEntity) point.getArgs()[0];
pushReleaseQueueAfterCancelOrderForSure(methodArg);
}
return retVal;
}
// ...
}
```
4. 系统日志:审计重点函数执行时间、参数、返回值,并持久化。(仅单个微服务有效,跨服或需消息队列)
```java
/**
* 系统日志切面
*/
@Component
@Aspect
@Slf4j
public class SysLogAspect {
// @Autowired
// private SysLogService sysLogService;
@Around("@annotation(sysLog)")
public Object around(ProceedingJoinPoint joinPoint, SysLog sysLog) throws Throwable {
long beginTime = SystemClock.now();
//执行方法
Object result = joinPoint.proceed();
//执行时长(毫秒)
long time = SystemClock.now() - beginTime;
SysLogTo sysLogTo = new SysLogTo();
if (sysLog != null) {
//注解上的描述
sysLogTo.setOperation(sysLog.value());
}
//请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = joinPoint.getSignature().getName();
sysLogTo.setMethod(className + "." + methodName + "()");
//请求的参数
Object[] args = joinPoint.getArgs();
String params = JSONUtil.toJsonStr(args[0]);
sysLogTo.setParams(params);
//设置IP地址
sysLogTo.setIp(IpHelper.getIpAddr());
//用户名:需要 Shiro 授权框架/Spring-Security 支持
// String username = SecurityUtils.getSysUser().getUsername();
// sysLogEntity.setUsername(username);
sysLogTo.setTime(time);
sysLogTo.setCreateDate(new Date());
//保存系统日志
log.info("sysLogEntity {} ", sysLogTo);
// sysLogService.save(sysLogEntity);
return result;
}
}
```
### 🤖 Selenium 测试驱动开发
从零到一造了个轮子,简单的测试框架。
- 设计模式
- PO:面向页面,实现三层代码解耦合:元素选择器 + 浏览器引擎模板对象 + 单元测试编写用例
- 单例:日志对象、浏览器对象必须只有1个,用的加锁双重校验保证多线程安全。(可惜复用浏览器上次对话还要搜集会话信息)
- 装饰器: 封装了 URL 路由方法,一般点击操作需要先打开网页
- 模版:上述模型都封装模块,每次写新页面,直接调用组件;写测试用例,就调用页面、方法、数据。
- 模拟真人操作流程
- 通过测试,发现和修复了文档标题、登录后应隐藏登录入口、Hystrix 熔断的隔离级别导致 Feign 通讯丢失请求头等问题
- 没有接口测试:接口文档参照上面的。
-[ ] 后期重构的话,准备使用 Apifox 工具
其中装饰器模式,不就 AOP 的抽象嘛。(语言不重要,解决问题方案才重要。)
```python
class mapping(object):
def __init__(self):
""" 不允许实例化 """
return False
@staticmethod
def get(url):
"""
打开网页,然后执行你的方法
"""
if isinstance(url, Enum):
url = url.value
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
if driver.current_url != url:
log.info(f'[GET] {url} [{func.__name__}]')
driver.get(url)
return func(*args, **kwargs)
return wrapper
return decorator
```