V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wayn111
V2EX  ›  程序员

waynboot 商城发布啦,整合了 Redis、RabbitMQ、ElasticSearch 等常用中间件, 根据生产环境开发经验而来不断完善、优化、改进中

  •  
  •   wayn111 ·
    wayn111 · 2021-05-15 15:07:06 +08:00 · 1485 次点击
    这是一个创建于 1076 天前的主题,其中的信息可能已经有所发展或是发生改变。

    waynboot-mall 项目

    觉得有用的铁子们给个 star 就行了,求求你们啦😘😍

    waynboot-mall 是一套全部开源的微商城项目,包含一个运营后台、h5 商城和后台接口。 实现了一个商城所需的首页展示、商品分类、商品详情、sku 详情、商品搜索、加入购物车、结算下单、订单状态流转、商品评论等一系列功能。 技术上基于 Springboot2.0,整合了 Redis 、RabbitMQ 、ElasticSearch 等常用中间件, 贴近生产环境实际经验开发而来不断完善、优化、改进中。

    后台接口项目
    运营后台项目
    h5 商城项目

    waynboot-mall 接口项目

    1. 商城接口代码清晰、注释完善、模块拆分合理
    2. 使用 Spring-Security 进行访问权限控制
    3. 使用 jwt 进行接口授权验证
    4. ORM 层使用 Mybatis Plus 提升开发效率
    5. 添加全局异常处理器,统一异常处理
    6. 添加 https 配置代码,支持 https 访问
    7. 集成七牛云存储配置,上传文件至七牛
    8. 集成常用邮箱配置,方便发送邮件
    9. 集成 druid 连接池,进行 sql 监控
    10. 集成 swagger,管理接口文档
    11. 添加策略模式使用示例,优化首页金刚区跳转逻辑
    12. 拆分出通用的数据访问模块,统一 redis & elastic 配置与访问
    13. 使用 elasticsearch-rest-high-level-client 客户端对 elasticsearch 进行操作
    14. 支持商品数据同步 elasticsearch 操作以及 elasticsearch 商品搜索
    15. RabbitMQ 生产者发送消息采用异步 confirm 模式,消费者消费消息时需手动确认
    16. 下单处理过程引入 rabbitMQ,异步生成订单记录,提高系统下单处理能力
    17. ...

    商城难点整理

    1. 库存扣减操作是在下单操作扣减还是在支付成功时扣减?( ps:扣减库存使用乐观锁机制 where goods_num - num >= 0

    1. 下单时扣减,这个方案属于实时扣减,当有大量下单请求时,由于订单数小于请求数,会发生下单失败,但是无法防止短时间大量恶意请求占用库存, 造成普通用户无法下单
    2. 支付成功扣减,这个方案可以预防恶意请求占用库存,但是会存在多个请求同时下单后,在支付回调中扣减库存失败,导致订单还是下单失败并且还要退还订单金额(这种请求就是订单数超过了库存数,无法发货,影响用户体验)
    3. 还是下单时扣减,但是对于未支付订单设置一个超时过期机制,比如下单时库存减一,生成订单后,对于未在 15 分钟内完成支付的订单, 自动取消超期未支付订单并将库存加一,该方案基本满足了大部分使用场景
    4. 针对大流量下单场景,比如一分钟内五十万次下单请求,可以通过设置虚拟库存的方式减少下单接口对数据库的访问。具体来说就是把商品实际库存保存到 redis 中, 下单时配合 lua 脚本原子的 get 和 decr 商品库存数量(这一步就拦截了大部分请求),执行成功后在扣减实际库存

    2. 首页商品展示接口利用多线程技术进行查询优化,将多个 sql 语句的排队查询变成异步查询,接口时长只跟查询时长最大的 sql 查询挂钩

    # 1. 通过创建子线程继承 Callable 接口
    Callable<List<Banner>> bannerCall = () -> iBannerService.list(new QueryWrapper<Banner>().eq("status", 0).orderByAsc("sort"));
    # 2. 传入 Callable 的任务给 FutureTask
    FutureTask<List<Banner>> bannerTask = new FutureTask<>(bannerCall);
    # 3. 放入线程池执行
    threadPoolTaskExecutor.submit(bannerTask);
    # 4. 最后可以在外部通过 FutureTask 的 get 方法异步获取执行结果 
    List<Banner> list = bannerTask.get()
    

    3. ElasticSearch查询操作,查询包含搜索关键字并且是上架中的商品,在根据指定字段进行排序,最后分页返回

    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    MatchQueryBuilder matchFiler = QueryBuilders.matchQuery("isOnSale", true);
    MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("name", keyword);
    MatchPhraseQueryBuilder matchPhraseQueryBuilder = QueryBuilders.matchPhraseQuery("keyword", keyword);
    boolQueryBuilder.filter(matchFiler).should(matchQuery).should(matchPhraseQueryBuilder).minimumShouldMatch(1);
    searchSourceBuilder.timeout(new TimeValue(10, TimeUnit.SECONDS));
    // 按是否新品排序
    if (isNew) { 
        searchSourceBuilder.sort(new FieldSortBuilder("isNew").order(SortOrder.DESC));
    }
    // 按是否热品排序
    if (isHot) {
        searchSourceBuilder.sort(new FieldSortBuilder("isHot").order(SortOrder.DESC));
    }
    // 按价格高低排序
    if (isPrice) {
        searchSourceBuilder.sort(new FieldSortBuilder("retailPrice").order("asc".equals(orderBy) ? SortOrder.ASC : SortOrder.DESC));
    }
    // 按销量排序
    if (isSales) {
        searchSourceBuilder.sort(new FieldSortBuilder("sales").order(SortOrder.DESC));
    }
    // 筛选新品
    if (filterNew) {
        MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isNew", true);
        boolQueryBuilder.filter(filterQuery);
    }
    // 筛选热品
    if (filterHot) {
        MatchQueryBuilder filterQuery = QueryBuilders.matchQuery("isHot", true);
        boolQueryBuilder.filter(filterQuery);
    }
    
    searchSourceBuilder.query(boolQueryBuilder);
    searchSourceBuilder.from((int) (page.getCurrent() - 1) * (int) page.getSize());
    searchSourceBuilder.size((int) page.getSize());
    List<JSONObject> list = elasticDocument.search("goods", searchSourceBuilder, JSONObject.class);
    

    4. 订单编号生成规则:秒级时间戳 + 加密用户 ID + 今日第几次下单

    1. 秒级时间戳:时间递增保证唯一性
    2. 加密用户 ID:加密处理,返回用户 ID6 位数字,可以防并发访问,同一秒用户不会产生 2 个订单
    3. 今日第几次下单:便于运营查询处理用户当日订单

    5. 下单流程处理过程,通过 rabbitMQ 异步生成订单,提高系统下单处理能力

    1. 用户点击提交订单按钮,后台生成订单编号和订单金额跳转到订单支付页面,并发送 rabbitMQ 消息(包含订单编号等信息)
    2. 订单消费者接受到订单消息后生成订单记录(未支付)
    3. 用户点击支付按钮时,前端根据订单编号轮询订单信息查询接口,如果订单编号记录已经入库则进行后续支付操作,如果订单编号未入库则返回错误信息(订单异常)
    4. 用户支付完成后在回调通知里更新订单状态为已支付

    6. 金刚区跳转使用策略模式

    # 1. 定义金刚位跳转策略接口
    public interface DiamondJumpType {
    
        List<Goods> getGoods(Page<Goods> page, Diamond diamond);
    
        Integer getType();
    }
    
    # 2. 定义策略实现类,并使用 @Component 注解注入 spring
    @Component
    public class CategoryStrategy implements DiamondJumpType {
    
        @Autowired
        private GoodsMapper goodsMapper;
    
        @Override
        public List<Goods> getGoods(Page<Goods> page, Diamond diamond) {
            List<Long> cateList = Arrays.asList(diamond.getValueId());
            return goodsMapper.selectGoodsListPageByl2CateId(page, cateList).getRecords();
        }
    
        @Override
        public Integer getType() {
            return JumpTypeEnum.CATEGORY.getType();
        }
    }
    @Component
    public class ColumnStrategy implements DiamondJumpType {
    
        @Autowired
        private IColumnGoodsRelationService iColumnGoodsRelationService;
    
        @Autowired
        private IGoodsService iGoodsService;
    
        @Override
        public List<Goods> getGoods(Page<Goods> page, Diamond diamond) {
            List<ColumnGoodsRelation> goodsRelationList = iColumnGoodsRelationService.list(new QueryWrapper<ColumnGoodsRelation>()
                    .eq("column_id", diamond.getValueId()));
            List<Long> goodsIdList = goodsRelationList.stream().map(ColumnGoodsRelation::getGoodsId).collect(Collectors.toList());
            Page<Goods> goodsPage = iGoodsService.page(page, new QueryWrapper<Goods>().in("id", goodsIdList).eq("is_on_sale", true));
            return goodsPage.getRecords();
        }
    
        @Override
        public Integer getType() {
            return JumpTypeEnum.COLUMN.getType();
        }
    }
    
    # 3. 定义策略上下文,通过构造器注入 spring,定义 map 属性,通过 key 获取对应策略实现类
    @Component
    public class DiamondJumpContext {
    
        private Map<Integer, DiamondJumpType> map = new HashMap<>();
    
        /**
         * 由 spring 自动注入 DiamondJumpType 子类
         *
         * @param diamondJumpTypes 金刚位跳转类型集合
         */
        public DiamondJumpContext(List<DiamondJumpType> diamondJumpTypes) {
            for (DiamondJumpType diamondJumpType : diamondJumpTypes) {
                map.put(diamondJumpType.getType(), diamondJumpType);
            }
        }
    
        public DiamondJumpType getInstance(Integer jumpType) {
            return map.get(jumpType);
        }
    }
    
    # 4.使用
    @Autowired
    private DiamondJumpContext diamondJumpContext;
    
    @Test
    public void test(){
        DiamondJumpType diamondJumpType = diamondJumpContext.getInstance(1);
    }
    
    
    • todo

    文件目录

    |-- waynboot-admin-api             // 运营后台 api 模块,提供后台项目 api 接口
    |-- waynboot-common                // 通用模块,包含项目核心基础类
    |-- waynboot-data                  // 数据模块,通用中间件数据访问
    |   |-- waynboot-data-redis        // redis 访问配置模块
    |   |-- waynboot-data-elastic      // elastic 访问配置模块
    |-- waynboot-generator             // 代码生成模块
    |-- waynboot-message-consumer      // 消费者模块,处理订单消息和邮件消息
    |-- waynboot-message-core          // 消费者核心模块,队列、交换机配置
    |-- waynboot-mobile-api            // h5 商城 api 模块,提供 h5 商城 api 接口
    |-- pom.xml                        // maven 父项目依赖,定义子项目依赖版本
    |-- ...
    

    开发部署

    # 1. 克隆项目
    git clone [email protected]:wayn111/waynboot-mall.git
    
    # 2. 导入项目依赖
    将 waynboot-mall 目录用 idea 打开,导入 maven 依赖
    
    # 3. 安装 Mysql8.0+、Redis3.0+、RabbitMQ3.0+、ElasticSearch7.0+到本地
    
    # 4. 导入 sql 文件
    在项目根目录下,找到`wayn_shop_*.sql`文件,新建 mysql 数据库 wayn_shop,导入其中
    
    # 5. 修改 Mysql 、Redis 、RabbitMQ 、Elasticsearch 连接配置
    修改`application-dev.yml`以及`application.yml`文件中数据连接配置相关信息
    
    # 6. 启动项目
    后台 api:
        进入 waynboot-admin-api 子项目,找到 AdminApplication 文件,右键`run AdminApplication`,启动后台项目
    h5 商城 api:
        进入 waynboot-mobile-api 子项目,找到 MobileApplication 文件,右键`run MobileApplication`,启动 h5 商城项目
    

    在线体验

    • 注册一个账号
    • 然后登陆

    演示地址: http://www.wayn.ltd

    演示图

    商城登陆 商城注册
    商城首页 商城搜索
    搜索结果展示 金刚位跳转
    商品分类 商品详情
    商品 sku 选择 购物车查看
    确认下单 选择支付方式
    商城我的页面 我的订单列表
    添加商品评论 查看商品评论
    后台登陆 后台首页
    后台会员管理 后台评论管理
    后台地址管理 后台添加商品
    后台商品管理 后台 banner 管理
    后台订单管理 后台分类管理
    后台金刚区管理 后台栏目管理

    waynboot-mall 交流群

    QQ 群:waynboot-mall 交流群 有问题可以先提 issue😁

    todo

    • 支持多店铺
    • 订单详情页面
    • 秒杀专区
    • 优惠卷使用
    • 团购下单
    • ...

    感谢

    10 条回复    2021-05-17 09:59:43 +08:00
    wayn111
        1
    wayn111  
    OP
       2021-05-15 15:10:19 +08:00
    欢迎大家提出问题😁,帮助这个项目改进优化
    xuanbg
        2
    xuanbg  
       2021-05-16 04:53:30 +08:00
    商品库存其实可以分三种,我的策略是:
    1 、卖完不能采购后发货。无库存不能下单,下单扣预警库存,支付扣实际库存;
    2 、卖完可以采购后发货。无预警库存,支付扣实际库存;
    3 、虚拟商品,无库存。随便卖,不扣库存。
    wayn111
        3
    wayn111  
    OP
       2021-05-16 10:10:19 +08:00
    @xuanbg 感谢回复,不同的库存策略肯定是适应不同的业务场景,在我们的场景里,用户下单时到店家发货需要保证短时效性,下单库存不足直接返回失败,同时发送企业微信群报警消息,由运营联系商家是否补货
    xuanbg
        4
    xuanbg  
       2021-05-16 10:44:18 +08:00
    @wayn111 我定义的 3 种库存属性已经涵盖了所有的场景需求。如你们这种需求,你只需要把需要及时发货的商品的库存属性设置为第一种就行了。同时,并不妨碍第二种、第三种库存属性的商品同时存在啊。
    wayn111
        5
    wayn111  
    OP
       2021-05-16 11:38:26 +08:00
    @xuanbg 明白了,不过我理解你说的第一种策略中如果支付时实际库存不足的话,是在第二种策略中采购后发货的意思吗
    xuanbg
        6
    xuanbg  
       2021-05-16 14:35:27 +08:00   ❤️ 1
    不是啊,假设你商城中有 A 、B 、C3 种商品:
    A 商品你需要在用户下单后立即发货,所以不能超卖。在库存下降到保留数后就不能下单了,以免无法发货以及影响售后换货。这种就设定 1,下单扣预警库存。
    B 商品由于有一定的价格优势,采购周期很短,发货稍慢一点用户也不会退单,或者厂家可以代发货,那就可以允许超卖。只在支付后扣实际库存,而且允许库存为负数。卖就是了,能发货的立即发货,不够的话采购后发货。
    C 商品是虚拟商品,压根不存在库存这个概念。

    那么,这三种商品你能同时卖吗?你的设计不能,我的设计就可以。
    xuanbg
        7
    xuanbg  
       2021-05-16 14:39:32 +08:00
    对于商家来说,大部分都是 B 商品,A 可能是一些尾货,C 不需要多谈。
    bsg1992
        8
    bsg1992  
       2021-05-17 09:20:00 +08:00
    下单减库存 和支付减库存做成配置项就可以了。减库存这个也不是啥难点吧。。。。
    wayn111
        9
    wayn111  
    OP
       2021-05-17 09:57:08 +08:00
    @bsg1992 主要是理解扣减库存业务流程😂
    wayn111
        10
    wayn111  
    OP
       2021-05-17 09:59:43 +08:00
    @xuanbg 明白了,感谢指点
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   4307 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 10:12 · PVG 18:12 · LAX 03:12 · JFK 06:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.