# 谷粒商城 **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 > > ![SpringCLoud](https://img.shields.io/badge/SpringCloud-Hoxton.SR7-brightgreen?logo=spring) > ![SpringCLoud](https://img.shields.io/badge/SpringBoot-2.3.2.RELEASE-brightgreen?logo=springboot) --- [紧急通知] 我的数据库被黑客删除和勒索了,但我不知道怎么取回 ![输入图片说明](docs/img/image.png) > 你需要支付一定的费用,如需恢复请联系我们,发送邮件按格式发送,否则自动过滤。邮件标题:你的数据库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 账号。 --- ![micro-service-architech.png](docs/img/micro-service-architech.png) 接口文档: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 持久层框架等 | ![img.png](docs/img/index.png) | | 2 | 会员 | SpringWeb 拦截器 + JWT鉴权、Oauth2.0 第三方授权 | ![img.png](docs/img/login.png) | | 3 | 搜索 | ElasticSearch 检索 | ![img.png](docs/img/es.png) | | 4 | 订单、购物车 | Redis 分布式缓存、锁、幂等性校验,RabbitMQ 消息队列 | ![img.png](docs/img/cartOrder.png) | | 5 | 路由、发现中心 | Alibaba-Nacos 注册中心和配置中心、OpenFeign 微服务通信、SpringBoot-Gateway 路由 | ![nacos.png](docs/img/nacos.png) | | 6 | 第三方 | oss 图床、短信验证 | | 基础服务设施(技术栈)还有: | 序号 | 基础设施 | 技术栈 | 截图 | |-----|-----------|---------------------------------------------------------------|---------------------------| | 1 | 服务器 | Docker 部署中间件 | ![docker-server..png](docs/img/docker-server.png) | | 2 | 测试 | Selenium(Python)单元测试 | ![img.png](docs/img/selenium.png) | | 3 | 压测调试 | JMeter 压力测试、VisualVM 健康检测、Arthas 线上诊断 | ![img.png](docs/img/jmeter.png) | | 4 | 后台 | Renren-fast、Renren-fast-vue 后台(商家)管理系统 | ![img.png](docs/img/renrenfast.png) | | 5 | 通用 | Thymeleaf 页面渲染、Nginx 负载均衡、SpringCloud-Admin 微服务监控中心 | ![img.png](docs/img/admin-server.png) | ## 线上部署 | 快速开始 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 ```