谷粒商城项目总结

谷粒商城项目总结

模块:


数据库设计

商品库

每个商品都是一个spuspu下面又分为不同的规格sku,即一个spu对应多个sku

每一个spu关联一个分类category和一个品牌brand

商品库存在多种属性attr,每一个属性可以关联不同的属性分组attr_group

一个商品spu关联不同的属性spu_attr_value,并且这些属性在不同的商品上有不同的值,比如:CPU型号,在IPhone上可能是Axx,在华为上可能是麒麟xxx

这些spu_attr_value可以被一个商品下的不同规格sku共享,但是每个sku之间存在不同的属性值sku_sale_attr_value,比如:华为Mate40,可以有黑、白等不同的颜色。

  • pms_spu_info:商品表

    CREATE TABLE `mall_pms`.`pms_spu_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品id', `spu_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品名称', `spu_description` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品描述', `category_id` bigint(20) NULL DEFAULT NULL COMMENT '所属分类id', `brand_id` bigint(20) NULL DEFAULT NULL COMMENT '品牌id', `weight` decimal(18, 4) NULL DEFAULT NULL COMMENT '商品重量', `publish_status` tinyint(4) NULL DEFAULT NULL COMMENT '上架状态: 0新建,1上架,2下架', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'spu信息' ROW_FORMAT = Dynamic;
  • pms_sku_info:规格表

    CREATE TABLE `mall_pms`.`pms_sku_info` ( `sku_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'skuId', `spu_id` bigint(20) NULL DEFAULT NULL COMMENT 'spuId', `sku_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'sku名称', `sku_desc` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'sku介绍描述', `category_id` bigint(20) NULL DEFAULT NULL COMMENT '所属分类id', `brand_id` bigint(20) NULL DEFAULT NULL COMMENT '品牌id', `sku_default_img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '默认图片', `sku_title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标题', `sku_subtitle` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '副标题', `price` decimal(18, 4) NULL DEFAULT NULL COMMENT '价格', `sale_count` bigint(20) NULL DEFAULT NULL COMMENT '销量', PRIMARY KEY (`sku_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'sku信息' ROW_FORMAT = Dynamic;
  • pms_category:分类表

    CREATE TABLE `mall_pms`.`pms_category` ( `cat_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分类id', `name` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分类名称', `parent_cid` bigint(20) NULL DEFAULT NULL COMMENT '父分类id', `cat_level` int(11) NULL DEFAULT NULL COMMENT '层级', `show_status` tinyint(4) NULL DEFAULT NULL COMMENT '是否显示[0-不显示,1显示]', `sort` int(11) NULL DEFAULT NULL COMMENT '排序', `icon` char(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '图标地址', `product_unit` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '计量单位', `product_count` int(11) NULL DEFAULT NULL COMMENT '商品数量', PRIMARY KEY (`cat_id`) USING BTREE, INDEX `cat_level_index`(`cat_level`) USING BTREE, INDEX `parent_cid_index`(`parent_cid`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1441 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品三级分类' ROW_FORMAT = Dynamic;
  • pms_brand:品牌表

    CREATE TABLE `mall_pms`.`pms_brand` ( `brand_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '品牌id', `name` char(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌名', `logo` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌logo地址', `introduction` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL, `show_status` tinyint(4) NULL DEFAULT NULL COMMENT '显示状态[0-不显示;1-显示]', `first_letter` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '检索首字母', `sort` int(11) NULL DEFAULT NULL COMMENT '排序', PRIMARY KEY (`brand_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '品牌' ROW_FORMAT = Dynamic;
  • pms_category_brand_relation:品牌分类关联表

    CREATE TABLE `mall_pms`.`pms_category_brand_relation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `brand_id` bigint(20) NULL DEFAULT NULL COMMENT '品牌id', `category_id` bigint(20) NULL DEFAULT NULL, `brand_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, `category_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '品牌分类关联' ROW_FORMAT = Dynamic;
  • pms_attr:属性表

    CREATE TABLE `mall_pms`.`pms_attr` ( `attr_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '属性id', `attr_name` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性名', `search_type` tinyint(4) NULL DEFAULT NULL COMMENT '是否需要检索[0-不需要,1-需要]', `value_type` tinyint(4) NULL DEFAULT NULL COMMENT '值类型[0-为单个值,1-可以选择多个值]', `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性图标', `value_select` char(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '可选值列表[用逗号分隔]', `attr_type` tinyint(4) NULL DEFAULT NULL COMMENT '属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]', `enable` tinyint(4) NULL DEFAULT NULL COMMENT '启用状态[0 - 禁用,1 - 启用]', `category_id` bigint(20) NULL DEFAULT NULL COMMENT '所属分类', `show_desc` tinyint(4) NULL DEFAULT NULL COMMENT '快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整', PRIMARY KEY (`attr_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品属性' ROW_FORMAT = Dynamic;
  • pms_attr_group:属性分组表

    CREATE TABLE `mall_pms`.`pms_attr_group` ( `attr_group_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '分组id', `attr_group_name` char(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组名', `sort` int(11) NULL DEFAULT NULL COMMENT '排序', `instruction` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '描述', `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组图标', `category_id` bigint(20) NULL DEFAULT NULL COMMENT '所属分类id', PRIMARY KEY (`attr_group_id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 12 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '属性分组' ROW_FORMAT = Dynamic;
  • pms_attr_attrgroup_relation:属性分组关联表

    CREATE TABLE `mall_pms`.`pms_attr_attrgroup_relation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `attr_id` bigint(20) NULL DEFAULT NULL COMMENT '属性id', `attr_group_id` bigint(20) NULL DEFAULT NULL COMMENT '属性分组id', `attr_sort` int(11) NULL DEFAULT NULL COMMENT '属性组内排序', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 11 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '属性&属性分组关联' ROW_FORMAT = Dynamic;
  • pms_spu_attr_value:商品属性表

    CREATE TABLE `mall_pms`.`pms_spu_attr_value` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `spu_id` bigint(20) NULL DEFAULT NULL COMMENT '商品id', `attr_id` bigint(20) NULL DEFAULT NULL COMMENT '属性id', `attr_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性名', `attr_value` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '属性值', `attr_sort` int(11) NULL DEFAULT NULL COMMENT '顺序', `quick_show` tinyint(4) NULL DEFAULT NULL COMMENT '快速展示【是否展示在介绍上;0-否 1-是】', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'spu属性值' ROW_FORMAT = Dynamic;
  • pms_sku_sale_attr_value:销售属性表

    CREATE TABLE `mall_pms`.`pms_sku_sale_attr_value` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `sku_id` bigint(20) NULL DEFAULT NULL COMMENT 'sku_id', `attr_id` bigint(20) NULL DEFAULT NULL COMMENT 'attr_id', `attr_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '销售属性名', `attr_value` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '销售属性值', `attr_sort` int(11) NULL DEFAULT NULL COMMENT '顺序', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'sku销售属性&值' ROW_FORMAT = Dynamic;

用户库

用户表ums_member中每一个用户可以关联多个地址信息ums_member_receive_address,用户可以收藏多个商品spu,即表ums_member_collection_spu,每个用户都有对应唯一的等级ums_member_level,每个用户购买商品后会获得积分,这个记录会保存到表ums_growth_change_history中。

  • ums_member:用户信息表

    CREATE TABLE `mall_ums`.`ums_member` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `level_id` bigint(20) NULL DEFAULT NULL COMMENT '会员等级id', `username` char(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名', `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', `nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '昵称', `mobile` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号码', `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱', `header` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', `gender` tinyint(4) NULL DEFAULT NULL COMMENT '性别', `birth` date NULL DEFAULT NULL COMMENT '生日', `city` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所在城市', `job` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '职业', `sign` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '个性签名', `source_type` tinyint(4) NULL DEFAULT NULL COMMENT '用户来源', `integration` int(11) NULL DEFAULT NULL COMMENT '积分', `growth` int(11) NULL DEFAULT NULL COMMENT '成长值', `status` tinyint(4) NULL DEFAULT NULL COMMENT '启用状态', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '注册时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员' ROW_FORMAT = Dynamic;
  • ums_member_level:用户等级表

    CREATE TABLE `mall_ums`.`ums_member_level` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '等级名称', `growth_point` int(11) NULL DEFAULT NULL COMMENT '等级需要的成长值', `default_status` tinyint(4) NULL DEFAULT NULL COMMENT '是否为默认等级[0->不是;1->是]', `free_freight_point` decimal(18, 4) NULL DEFAULT NULL COMMENT '免运费标准', `comment_growth_point` int(11) NULL DEFAULT NULL COMMENT '每次评价获取的成长值', `privilege_free_freight` tinyint(4) NULL DEFAULT NULL COMMENT '是否有免邮特权', `privilege_member_price` tinyint(4) NULL DEFAULT NULL COMMENT '是否有会员价格特权', `privilege_birthday` tinyint(4) NULL DEFAULT NULL COMMENT '是否有生日特权', `note` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员等级' ROW_FORMAT = Dynamic;
  • ums_member_collect_spu:用户收藏的商品信息表

    CREATE TABLE `mall_ums`.`ums_member_collect_spu` ( `id` bigint(20) NOT NULL COMMENT 'id', `member_id` bigint(20) NULL DEFAULT NULL COMMENT '会员id', `spu_id` bigint(20) NULL DEFAULT NULL COMMENT 'spu_id', `spu_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'spu_name', `spu_img` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'spu_img', `create_time` datetime(0) NULL DEFAULT NULL COMMENT 'create_time', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员收藏的商品' ROW_FORMAT = Dynamic;
  • ums_member_receive_address:用户地址表

    CREATE TABLE `mall_ums`.`ums_member_receive_address` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `member_id` bigint(20) NULL DEFAULT NULL COMMENT 'member_id', `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人姓名', `phone` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '电话', `post_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮政编码', `province` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份/直辖市', `city` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', `region` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区', `detail_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '详细地址(街道)', `areacode` varchar(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省市区代码', `default_status` tinyint(1) NULL DEFAULT NULL COMMENT '是否默认', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '会员收货地址' ROW_FORMAT = Dynamic;
  • ums_growth_change_history:用户积分变化表

    CREATE TABLE `mall_ums`.`ums_growth_change_history` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `member_id` bigint(20) NULL DEFAULT NULL COMMENT 'member_id', `create_time` datetime(0) NULL DEFAULT NULL COMMENT 'create_time', `change_count` int(11) NULL DEFAULT NULL COMMENT '改变的值(正负计数)', `note` varchar(0) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `source_type` tinyint(4) NULL DEFAULT NULL COMMENT '积分来源[0-购物,1-管理员修改]', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '成长值变化历史记录' ROW_FORMAT = Dynamic;

订单库

每一个订单oms_order都关联多个订单项oms_order_item。订单确认后,还会生成对应的订单支付信息oms_payment_info

  • oms_order:订单表

    CREATE TABLE `mall_oms`.`oms_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `member_id` bigint(20) NULL DEFAULT NULL COMMENT 'member_id', `order_sn` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单号', `coupon_id` bigint(20) NULL DEFAULT NULL COMMENT '使用的优惠券', `create_time` datetime(0) NULL DEFAULT NULL COMMENT 'create_time', `member_username` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用户名', `total_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '订单总额', `pay_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '应付总额', `freight_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '运费金额', `promotion_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '促销优化金额(促销价、满减、阶梯价)', `integration_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '积分抵扣金额', `coupon_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '优惠券抵扣金额', `discount_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '后台调整订单使用的折扣金额', `pay_type` tinyint(4) NULL DEFAULT NULL COMMENT '支付方式【1->支付宝;2->微信;3->银联; 4->货到付款;】', `source_type` tinyint(4) NULL DEFAULT NULL COMMENT '订单来源[0->PC订单;1->app订单]', `status` tinyint(4) NULL DEFAULT NULL COMMENT '订单状态【0->待付款;1->待发货;2->已发货;3->已完成;4->已关闭;5->无效订单】', `delivery_company` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '物流公司(配送方式)', `delivery_sn` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '物流单号', `auto_confirm_day` int(11) NULL DEFAULT NULL COMMENT '自动确认时间(天)', `integration` int(11) NULL DEFAULT NULL COMMENT '可以获得的积分', `growth` int(11) NULL DEFAULT NULL COMMENT '可以获得的成长值', `bill_type` tinyint(4) NULL DEFAULT NULL COMMENT '发票类型[0->不开发票;1->电子发票;2->纸质发票]', `bill_header` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发票抬头', `bill_content` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '发票内容', `bill_receiver_phone` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收票人电话', `bill_receiver_email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收票人邮箱', `receiver_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人姓名', `receiver_phone` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人电话', `receiver_post_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人邮编', `receiver_province` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '省份/直辖市', `receiver_city` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '城市', `receiver_region` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区', `receiver_detail_address` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '详细地址', `note` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单备注', `confirm_status` tinyint(4) NULL DEFAULT NULL COMMENT '确认收货状态[0->未确认;1->已确认]', `delete_status` tinyint(4) NULL DEFAULT NULL COMMENT '删除状态【0->未删除;1->已删除】', `use_integration` int(11) NULL DEFAULT NULL COMMENT '下单时使用的积分', `payment_time` datetime(0) NULL DEFAULT NULL COMMENT '支付时间', `delivery_time` datetime(0) NULL DEFAULT NULL COMMENT '发货时间', `receive_time` datetime(0) NULL DEFAULT NULL COMMENT '确认收货时间', `comment_time` datetime(0) NULL DEFAULT NULL COMMENT '评价时间', `modify_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '订单' ROW_FORMAT = Dynamic;
  • oms_order_item:订单项表

    CREATE TABLE `mall_oms`.`oms_order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `order_id` bigint(20) NULL DEFAULT NULL COMMENT 'order_id', `order_sn` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'order_sn', `spu_id` bigint(20) NULL DEFAULT NULL COMMENT 'spu_id', `spu_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'spu_name', `spu_pic` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'spu_pic', `spu_brand` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '品牌', `category_id` bigint(20) NULL DEFAULT NULL COMMENT '商品分类id', `sku_id` bigint(20) NULL DEFAULT NULL COMMENT '商品sku编号', `sku_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品sku名字', `sku_pic` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品sku图片', `sku_price` decimal(18, 4) NULL DEFAULT NULL COMMENT '商品sku价格', `sku_quantity` int(11) NULL DEFAULT NULL COMMENT '商品购买的数量', `sku_attrs_vals` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '商品销售属性组合(JSON)', `promotion_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '商品促销分解金额', `coupon_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '优惠券优惠分解金额', `integration_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '积分优惠分解金额', `real_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '该商品经过优惠后的分解金额', `gift_integration` int(11) NULL DEFAULT NULL COMMENT '赠送积分', `gift_growth` int(11) NULL DEFAULT NULL COMMENT '赠送成长值', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '订单项信息' ROW_FORMAT = Dynamic;
  • oms_payment_info:订单支付信息表

    CREATE TABLE `mall_oms`.`oms_payment_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `order_sn` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单号(对外业务号)', `order_id` bigint(20) NULL DEFAULT NULL COMMENT '订单id', `alipay_trade_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付宝交易流水号', `total_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '支付总金额', `subject` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '交易内容', `payment_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '支付状态', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `confirm_time` datetime(0) NULL DEFAULT NULL COMMENT '确认时间', `callback_content` varchar(4000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '回调内容', `callback_time` datetime(0) NULL DEFAULT NULL COMMENT '回调时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '支付信息表' ROW_FORMAT = Dynamic;

库存库

每个商品规格sku都存在对应的库存信息wms_ware_sku,每个商品的库存都存放在对应的仓库中wms_ware_info,商品库存的入库都有对应的采购单wms_purchase

  • wms_ware_sku:商品库存信息表

    CREATE TABLE `mall_wms`.`wms_ware_sku` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `sku_id` bigint(20) NULL DEFAULT NULL COMMENT 'sku_id', `ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id', `stock` int(11) NULL DEFAULT NULL COMMENT '库存数', `sku_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'sku_name', `stock_locked` int(11) NULL DEFAULT NULL COMMENT '锁定库存', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '商品库存' ROW_FORMAT = Dynamic;
  • wms_ware_info:仓库信息表

    CREATE TABLE `mall_wms`.`wms_ware_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '仓库名', `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '仓库地址', `areacode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '区域编码', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '仓库信息' ROW_FORMAT = Dynamic;
  • wms_purchase:采购单信息表

    CREATE TABLE `mall_wms`.`wms_purchase` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '采购单id', `assignee_id` bigint(20) NULL DEFAULT NULL COMMENT '采购人id', `assignee_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '采购人名', `phone` char(13) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '联系方式', `priority` int(4) NULL DEFAULT NULL COMMENT '优先级', `status` int(4) NULL DEFAULT NULL COMMENT '状态', `ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id', `amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '总金额', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建日期', `update_time` datetime(0) NULL DEFAULT NULL COMMENT '更新日期', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '采购信息' ROW_FORMAT = Dynamic;
  • wms_purchase_detail:采购单详情表

    CREATE TABLE `mall_wms`.`wms_purchase_detail` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `purchase_id` bigint(20) NULL DEFAULT NULL COMMENT '采购单id', `sku_id` bigint(20) NULL DEFAULT NULL COMMENT '采购商品id', `sku_num` int(11) NULL DEFAULT NULL COMMENT '采购数量', `sku_price` decimal(18, 4) NULL DEFAULT NULL COMMENT '采购金额', `ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id', `status` int(11) NULL DEFAULT NULL COMMENT '状态[0新建,1已分配,2正在采购,3已完成,4采购失败]', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
  • wms_ware_order_task:工作单表

    CREATE TABLE `mall_wms`.`wms_ware_order_task` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `order_id` bigint(20) NULL DEFAULT NULL COMMENT 'order_id', `order_sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'order_sn', `consignee` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人', `consignee_tel` char(15) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '收货人电话', `delivery_address` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '配送地址', `order_comment` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单备注', `payment_way` tinyint(1) NULL DEFAULT NULL COMMENT '付款方式【 1:在线付款 2:货到付款】', `task_status` tinyint(2) NULL DEFAULT NULL COMMENT '任务状态', `order_body` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '订单描述', `tracking_no` char(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '物流单号', `create_time` datetime(0) NULL DEFAULT NULL COMMENT 'create_time', `ware_id` bigint(20) NULL DEFAULT NULL COMMENT '仓库id', `task_comment` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '工作单备注', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '库存工作单' ROW_FORMAT = Dynamic;
  • wms_ware_order_task_detail:工作单项表

    CREATE TABLE `mall_wms`.`wms_ware_order_task_detail` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `sku_id` bigint(20) NULL DEFAULT NULL COMMENT 'sku_id', `sku_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'sku_name', `sku_num` int(11) NULL DEFAULT NULL COMMENT '购买个数', `task_id` bigint(20) NULL DEFAULT NULL COMMENT '工作单id', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '库存工作单' ROW_FORMAT = Dynamic;

优惠库

一个秒杀活动sms_seckill_promotion每天可以有多个场次sms_seckill_session,每个场次中可以存在多件秒杀商品sms_seckill_sku_relation

  • sms_seckill_promotion秒杀活动表

    CREATE TABLE `mall_sms`.`sms_seckill_promotion` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '活动标题', `start_time` datetime(0) NULL DEFAULT NULL COMMENT '开始日期', `end_time` datetime(0) NULL DEFAULT NULL COMMENT '结束日期', `status` tinyint(4) NULL DEFAULT NULL COMMENT '上下线状态', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', `user_id` bigint(20) NULL DEFAULT NULL COMMENT '创建人', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀活动' ROW_FORMAT = Dynamic;
  • sms_seckill_session:秒杀活动场次表

    CREATE TABLE `mall_sms`.`sms_seckill_session` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '场次名称', `start_time` datetime(0) NULL DEFAULT NULL COMMENT '每日开始时间', `end_time` datetime(0) NULL DEFAULT NULL COMMENT '每日结束时间', `status` tinyint(1) NULL DEFAULT NULL COMMENT '启用状态', `create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀活动场次' ROW_FORMAT = Dynamic;
  • sms_seckill_sku_relation:秒杀活动商品关联表

    CREATE TABLE `mall_sms`.`sms_seckill_sku_relation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `promotion_id` bigint(20) NULL DEFAULT NULL COMMENT '活动id', `promotion_session_id` bigint(20) NULL DEFAULT NULL COMMENT '活动场次id', `sku_id` bigint(20) NULL DEFAULT NULL COMMENT '商品id', `seckill_price` decimal(10, 0) NULL DEFAULT NULL COMMENT '秒杀价格', `seckill_count` decimal(10, 0) NULL DEFAULT NULL COMMENT '秒杀总量', `seckill_limit` decimal(10, 0) NULL DEFAULT NULL COMMENT '每人限购数量', `seckill_sort` int(11) NULL DEFAULT NULL COMMENT '排序', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀活动商品关联' ROW_FORMAT = Dynamic;
  • sms_seckill_sku_notice

    CREATE TABLE `mall_sms`.`sms_seckill_sku_notice` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `member_id` bigint(20) NULL DEFAULT NULL COMMENT 'member_id', `sku_id` bigint(20) NULL DEFAULT NULL COMMENT 'sku_id', `session_id` bigint(20) NULL DEFAULT NULL COMMENT '活动场次id', `subcribe_time` datetime(0) NULL DEFAULT NULL COMMENT '订阅时间', `send_time` datetime(0) NULL DEFAULT NULL COMMENT '发送时间', `notice_type` tinyint(1) NULL DEFAULT NULL COMMENT '通知方式[0-短信,1-邮件]', PRIMARY KEY (`id`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '秒杀商品通知订阅' ROW_FORMAT = Dynamic;

业务详情

商品上架

接口:/product/spuinfo/{spuId}/up

前端发起某个spu(商品)的上架请求,请求/product/spuinfo/{spuId}/up接口,该接口调用Service层的实现类SpuInfoServiceImplup方法。

up方法的逻辑:

  1. 根据spuId判断该spu是否存在,不存在则抛出异常,交由异常处理器处理,返回统一的异常信息。

  2. 根据spuId获取商品的所有规格信息(sku)列表。

  3. 发起远程调用请求库存服务,查看sku的库存信息。

  4. 查询spu下所有可被检索到的规格参数属性值。

  5. 将以上查询的信息封装到对应Model中。

    查询对应sku的品牌和分类信息。

  6. 发起远程调用请求检索服务,将封装的sku信息保存到ES中。

  7. 修改商品的上架状态。

商品上架Controller层

Service层相关方法:SpuInfoServiceImpl类的void up(Long spuId);方法

image-20220212123229500

/*--------------------- SpuInfoServiceImpl Start ---------------------*/ /** * 商品上架 * @param spuId */ @Override public void up(Long spuId) { // 1.判断spu是否存在 SpuInfoEntity spuInfoEntity = getById(spuId); if (spuInfoEntity == null) { throw new RRException("spu【" + spuId + "】不存在"); } /* 组装Es中的商品模型 */ // 2.根据spuId获取所有商品规格信息列表 List<SkuInfoEntity> skuInfoEntityList = skuInfoService.getSkuListBySpuId(spuId); if (CollectionUtils.isEmpty(skuInfoEntityList)) { throw new RRException("spu【" + spuId + "】下不存在sku信息"); } // 3.查询sku对应的库存信息 List<Long> skuIdList = skuInfoEntityList.stream() .map(SkuInfoEntity::getSkuId) .collect(Collectors.toList()); R result = wareFeignService.getSkuHasStock(skuIdList); if (result == null || result.getCode() != 0) { throw new RRException("获取sku的库存信息失败"); } List<SkuStockVo> skuStockVoList = ConvertUtils.convertByJson(result.get("data"), List.class); Map<Long, Boolean> stockMap = skuStockVoList.stream() .collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock)); // 3.查询对应spu对应可被检索的规格参数 List<SpuAttrEsModel> spuAttrEsModelList = getSpuAttrEsModelListBySpuId(spuId); // 4.封装sku信息到对应Model List<SkuEsModel> skuEsModelList = skuInfoEntityList.stream() .map(skuInfoEntity -> { SkuEsModel skuEsModel = new SkuEsModel(); // 4-1.查询sku对应的品牌信息 CompletableFuture<Void> queryBrandTask = CompletableFuture.runAsync(() -> { BrandEntity brand = brandService.getById(skuInfoEntity.getBrandId()); skuEsModel.setBrandName(brand.getName()); skuEsModel.setBrandImg(brand.getLogo()); }, executor); // 4-2.查询sku对应的分类信息 CompletableFuture<Void> queryCategoryTask = CompletableFuture.runAsync(() -> { CategoryEntity category = categoryService.getById(skuInfoEntity.getCategoryId()); skuEsModel.setCategoryName(category.getName()); }, executor); // 4-3.设置sku的基本信息 skuEsModel.setSkuId(skuInfoEntity.getSkuId()); skuEsModel.setSkuTitle(skuInfoEntity.getSkuTitle()); skuEsModel.setSkuImg(skuInfoEntity.getSkuDefaultImg()); skuEsModel.setSkuPrice(skuInfoEntity.getPrice()); skuEsModel.setSaleCount(skuInfoEntity.getSaleCount()); skuEsModel.setSpuId(spuId); skuEsModel.setBrandId(skuInfoEntity.getBrandId()); skuEsModel.setCategoryId(skuInfoEntity.getCategoryId()); // 4-4.查询sku对应的规格参数 skuEsModel.setAttrs(spuAttrEsModelList); skuEsModel.setHotScore(0L); // 4-5.查询sku是否有库存 skuEsModel.setHasStock(Optional.ofNullable(stockMap.get(skuInfoEntity.getSkuId())) .orElse(Boolean.FALSE)); CompletableFuture<Void> summaryTask = CompletableFuture.allOf(queryBrandTask, queryCategoryTask); summaryTask.join(); return skuEsModel; }) .collect(Collectors.toList()); // 5.调用检索服务 result = searchFeignService.saveProduct(skuEsModelList); if (result == null || result.getCode() != 0) { // TODO 失败重试(分布式事务) } // 6.修改商品上架状态 lambdaUpdate() .set(SpuInfoEntity::getPublishStatus, ProductConstant.SpuPublishStatus.UP.getStatus()) .set(SpuInfoEntity::getUpdateTime, new Date()) .eq(SpuInfoEntity::getId, spuId) .update(); } /** * 通过spuId获取该spu下可被检索的spu信息 * @param spuId * @return */ private List<SpuAttrEsModel> getSpuAttrEsModelListBySpuId(Long spuId) { // 1.根据spuId获取对应的规格参数属性值 List<SpuAttrValueEntity> spuAttrValueEntityList = spuAttrValueService .getBaseAttrListOfSpu(spuId); // 2. 调用属性服务, 从规格参数属性值中查询可以被检索的属性 List<Long> attrIdList = spuAttrValueEntityList.stream() .map(SpuAttrValueEntity::getAttrId) .collect(Collectors.toList()); Set<Long> selectedIdSet = attrService.selectAttrIdListOfCanBeRetrieved(attrIdList); List<SpuAttrEsModel> spuAttrEsModelList = spuAttrValueEntityList.stream() .filter(spuAttrValue -> selectedIdSet.contains(spuAttrValue.getAttrId())) .map(spuAttrValue -> { SpuAttrEsModel spuAttrEsModel = new SpuAttrEsModel(); spuAttrEsModel.setAttrId(spuAttrValue.getAttrId()); spuAttrEsModel.setAttrName(spuAttrValue.getAttrName()); spuAttrEsModel.setAttrValue(spuAttrValue.getAttrValue()); return spuAttrEsModel; }) .collect(Collectors.toList()); return spuAttrEsModelList; } /*--------------------- SpuInfoServiceImpl End ---------------------*/ /*--------------------- SkuInfoServiceImpl Start ---------------------*/ /** * 通过spuId获取sku列表 * @param spuId * @return */ @Override public List<SkuInfoEntity> getSkuListBySpuId(Long spuId) { return lambdaQuery() .eq(SkuInfoEntity::getSpuId, spuId) .list(); } /*--------------------- SkuInfoServiceImpl End ---------------------*/ /*--------------------- SpuAttrValueServiceImpl Start ---------------------*/ /** * 获取spu对应的规格参数 * @param spuId * @return */ @Override public List<SpuAttrValueEntity> getBaseAttrListOfSpu(Long spuId) { List<SpuAttrValueEntity> list = this.lambdaQuery() .eq(SpuAttrValueEntity::getSpuId, spuId) .list(); return list; } /*--------------------- SpuAttrValueServiceImpl End ---------------------*/

远程调用库存服务:WareFeignService

image-20220212123736364

image-20220212123829814

/** * 获取sku的库存信息 * @param skuIdList * @return */ @Override public List<SkuStockVo> getSkuHasStock(List<Long> skuIdList) { // 1. 根据skuId列表查询对应的库存 List<WareSkuEntity> wareSkuEntityList = lambdaQuery() .in(WareSkuEntity::getSkuId, skuIdList) .list(); if (CollectionUtils.isEmpty(wareSkuEntityList)) { return Collections.emptyList(); } List<SkuStockVo> skuStockVoList = wareSkuEntityList.stream() .map(wareSkuEntity -> { SkuStockVo skuStockVo = new SkuStockVo(); skuStockVo.setHasStock(wareSkuEntity.getStock() > 0); skuStockVo.setSkuId(wareSkuEntity.getSkuId()); return skuStockVo; }) .collect(Collectors.toList()); return skuStockVoList; }

调用远程的检索服务:SearchFeignService

image-20220212124951159

image-20220212125036970

/** * 保存商品信息到ES * @param skuEsModelList * @return true 保存成功, false 保存失败 */ public boolean saveProduct(List<SkuEsModel> skuEsModelList) { BulkRequest bulkRequest = new BulkRequest(); try { skuEsModelList.forEach(skuEsModel -> { IndexRequest indexRequest = new IndexRequest(SearchConstant.PRODUCT_INDEX); indexRequest.id(skuEsModel.getSkuId().toString()); String json = JsonUtils.toJsonString(skuEsModel); indexRequest.source(json, XContentType.JSON); bulkRequest.add(indexRequest); }); BulkResponse bulkResponse = client.bulk(bulkRequest, SimpleElasticSearchConfig.COMMON); if (bulkResponse.hasFailures()) { List<String> idList = Arrays.stream(bulkResponse.getItems()) .map(BulkItemResponse::getId) .collect(Collectors.toList()); log.info("商品信息保存ES中出现错误, 错误原因: {}", idList); throw new RRException("商品信息保存到ES中出现错误, 保存失败的id列表为: " + idList); } return !bulkResponse.hasFailures(); } catch (IOException e) { log.error("商品信息保存到ES中出现错误, 错误原因: " + e.getCause(), e); throw new RRException("商品信息保存到ES中出现错误, 错误原因: " + e.getCause()); } }

用户注册和登录

接口:/sms/sendCode(发送验证码)、/register(注册)、/login(登录)

发送验证码

通过Redis的字符串结构来保存验证码来防止恶意请求接口,key是手机号,value是验证码_再次发送验证码的截止时间戳,且设置key的过期时间为10min,即在10min内该验证码有效,同时在value里面保存了时间戳来防止重复请求接口。

/** * 发送验证码 * @param phone 手机号码 */ @PostMapping("/sms/sendcode") @ResponseBody public R sendCode(@RequestParam("phone") @Pattern(regexp = "1[0-9]{10}", message = "手机号码格式错误") String phone) { // 1.通过redis判断该手机号是否可以发送验证码 if (!checkCanSendVerifyCode(phone)) { throw new RRException("发送频率太高了, 请稍后重试"); } // 2.生成验证码 String verifyCode = generateVerifyCode(); // 3.调用第三方服务发送验证码 R result = thirdPartyFeignService.sendVerifyCodeSms(phone, verifyCode); if (result == null || result.getCode() != 0) { if (result != null) { log.error("调用第三方服务成功, 响应结果: {}", result); } else { log.error("调用第三方服务失败, 返回结果为null"); } throw new RRException("发送验证码失败, 请稍后重试"); } // 4.在redis中保存手机号和验证码 saveVerifyCode(phone, verifyCode); return R.ok(); } /** * 检查是否可以发送验证码 * @param phone * @return */ private boolean checkCanSendVerifyCode(String phone) { String value = redisUtils.getVerifyCode(phone); if (StringUtils.isEmpty(value)) { return true; } String[] strings = value.split("_"); long timestamp = Long.parseLong(strings[1]); if (System.currentTimeMillis() <= timestamp) { return false; } return true; } /** * 保存验证码 * @param phone * @param code */ private void saveVerifyCode(String phone, String code) { long canSendTimestamp = System.currentTimeMillis() + 60 * 1000; String value = code + "_" + canSendTimestamp; redisUtils.setVerifyCode(phone, value); }

注册用户

校验用户填写的信息 -> 判断验证码是否正确 -> 远程调用会员服务:添加会员信息 -> 重定向到登录页

/** * 注册用户 */ @PostMapping("/register") public String register(@Validated UserRegisterVo userRegisterVo, BindingResult bindingResult, RedirectAttributes redirectAttributes) { // 1.校验待注册的用户信息是否正确 if (bindingResult.hasErrors()) { Map<String, String> errorMsgMap = bindingResult.getFieldErrors() .stream() .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)); redirectAttributes.addFlashAttribute("errors", errorMsgMap); return "redirect:" + REGISTER_PAGE_URL; } // 2.从Redis中获取验证码, 判断验证码是否正确 String verifyCode = getVerifyCodeByPhone(userRegisterVo.getPhone()); if (verifyCode == null || !verifyCode.equalsIgnoreCase(userRegisterVo.getCode())) { Map<String, String> errorMsgMap = new HashMap<>(1); errorMsgMap.put("tipMsg", "验证码错误"); redirectAttributes.addFlashAttribute("errors", errorMsgMap); return "redirect:" + REGISTER_PAGE_URL; } // 3.从Redis中删除验证码 redisUtils.deleteVerifyCode(userRegisterVo.getPhone()); MemberRegisterVo memberRegisterVo = new MemberRegisterVo(); memberRegisterVo.setUsername(userRegisterVo.getUsername()); memberRegisterVo.setPassword(userRegisterVo.getPassword()); memberRegisterVo.setPhone(userRegisterVo.getPhone()); try { // 4.发起远程调用,请求会员服务添加会员信息 R result = memberFeignService.register(memberRegisterVo); if (result == null) { redirectAttributes.addFlashAttribute("errors", Collections.singletonMap("tipMsg", "注册失败请稍后重试")); return "redirect:" + REGISTER_PAGE_URL; } else if (result.getCode() != 0) { redirectAttributes.addFlashAttribute("errors", Collections.singletonMap("tipMsg", result.get("msg"))); return "redirect:" + REGISTER_PAGE_URL; } return "redirect:" + LOGIN_PAGE_URL; } catch (Exception e) { log.error("调用远程会员服务失败, 异常原因: " + e.getMessage(), e); redirectAttributes.addFlashAttribute("errors", Collections.singletonMap("tipMsg", "注册失败请稍后重试")); return "redirect:" + REGISTER_PAGE_URL; } }

远程调用会员服务:

image-20220212131657879

用户登录

流程:

判断是否已登录,已登录则跳转到首页 -> 校验用户参数 -> 远程调用会员服务:校验用户名和密码并返回用户信息 -> 保存用户信息到Session -> 重定向到首页

采用分布式Session来解决Session的跨域问题。

  1. 放大域名的作用域:*.mall.com -> mall.com
  2. 将Session保存到Redis中(SpringSession),使用注解@EnableRedisHttpSession

SpringSession的原理:

注入Filter拦截每一个请求,对request进行包装(装饰器模式)并重写getSession方法,获取Session时从Redis获取。

Redis的结构:

  • spring:session:sessions:{sessionId}:hash结构,过期时间是lastAccessedTime + maxInactiveInterval + 5Minute

    createTime(创建时间)、maxInactiveInterval(最大未激活时间间隔)、lastAccessedTime(最近访问时间)

  • spring:session:expirations:{过期时间}:set结构,过期时间是lastAccessedTime + maxInactiveInterval + 5Minute

    存储expires:{sessionId}

  • spring:session:sessions:expires:{sessionId}:string结构,过期时间是lastAccessedTime + maxInactiveInterval

/** * 用户登录 */ @PostMapping("/login") public String login(@Validated UserLoginVo userLoginVo, BindingResult bindingResult, RedirectAttributes redirectAttributes) { // 1.判断是否已登录,如果已登录则跳转到首页 HttpServletRequest request = WebUtils.getCurrentRequest(); HttpSession session = request.getSession(true); if (session != null && session.getAttribute(USER_OF_SESSION) != null) { return "redirect:http://www.mall.com"; } // 2.校验用户传入的参数是否有错 if (bindingResult.hasErrors()) { // 参数校验出现错误 Map<String, String> errorTipMsg = bindingResult.getFieldErrors() .stream() .collect(Collectors.toMap(FieldError::getField, FieldError::getDefaultMessage)); redirectAttributes.addFlashAttribute("errors", errorTipMsg); return "redirect:" + LOGIN_PAGE_URL; } try { // 3.远程调用会员服务,校验用户名和密码并返回用户信息 MemberLoginVo memberLoginVo = new MemberLoginVo(); memberLoginVo.setAccount(userLoginVo.getAccount()); memberLoginVo.setPassword(userLoginVo.getPassword()); R result = memberFeignService.login(memberLoginVo); if (result == null) { redirectAttributes.addFlashAttribute("errors", Collections.singletonMap("tipMsg", "登录失败, 请稍后重试")); return "redirect:" + LOGIN_PAGE_URL; } else if (result.getCode() != 0) { redirectAttributes.addFlashAttribute("errors", Collections.singletonMap("tipMsg", result.get("msg"))); return "redirect:" + LOGIN_PAGE_URL; } MemberVo memberVo = result.getWithSpecifiedType("data", MEMBER_VO_TYPE_REFERENCE); // 4.设置session session.setAttribute(USER_OF_SESSION, memberVo); log.info("会员信息为: {}", memberVo); // 5.重定向到首页 return "redirect:http://www.mall.com"; } catch (Exception e) { log.error("出现异常, 异常原因: " + e.getMessage(), e); redirectAttributes.addFlashAttribute("errors", Collections.singletonMap("tipMsg", "登录失败, 请稍后重试")); return "redirect:" + LOGIN_PAGE_URL; } }

商品购买

查看商品详情

相关接口:{skuId}.html

查看商品详情接口

Service层相关方法:SkuInfoServiceImpl类下的getItemById方法

使用CompletableFuture进行异步编排,提高查询速度。

根据skuId获取sku的基本信息 -> 根据sku的spuId获取销售属性、规格参数、商品描述 \ \ 根据skuId获取sku的图片信息 -------------------------------------------------> 封装成Vo返回

注意:第三步通过spuId来查询该商品下所有规格的销售属性是通过连表查询的。

/** * 通过skuId获取sku的信息(商品详情) * @param skuId * @return */ @Override public SkuItemVo getItemById(Long skuId) { SkuItemVo skuItemVo = new SkuItemVo(); // 1. 获取sku的基本信息 CompletableFuture<Long> getSkuInfoTask = CompletableFuture.supplyAsync(() -> { SkuInfoEntity skuInfoEntity = getById(skuId); if (skuInfoEntity == null) { throw new RRException("sku【" + skuId + "】不存在"); } skuItemVo.setInfo(skuInfoEntity); return skuInfoEntity.getSpuId(); }); // 2. 获取sku的图片信息 CompletableFuture<Void> getImageOfSkuTask = CompletableFuture.runAsync(() -> { List<SkuImagesEntity> imageList = skuImagesService.listById(skuId); skuItemVo.setImageList(imageList); }); CompletableFuture<Void> getSaleAttrCombinationTask = getSkuInfoTask.thenAcceptAsync(spuId -> { // 3. 获取该spu下所有sku的销售属性 List<SkuItemSaleAttrVo> spuItemBaseAttrVoList = skuSaleAttrValueService.getSaleAttrListBySpuId(spuId); skuItemVo.setSaleAttrList(spuItemBaseAttrVoList); }, executor); CompletableFuture<Void> getSpuDescriptionTask = getSkuInfoTask.thenAcceptAsync(spuId -> { // 4. 获取spu的介绍 SpuInfoDescEntity description = spuInfoDescService.getById(spuId); skuItemVo.setDescription(description); }, executor); CompletableFuture<Void> getBaseAttrListOfSpuTask = getSkuInfoTask.thenAcceptAsync(spuId -> { // 5. 获取spu的规格参数 List<SpuItemBaseAttrVo> groupAttrList = spuAttrValueService.getGroupAttrListBySpuId(spuId); skuItemVo.setGroupAttrList(groupAttrList); }, executor); CompletableFuture.allOf(getImageOfSkuTask, getSaleAttrCombinationTask, getSpuDescriptionTask, getBaseAttrListOfSpuTask).join(); return skuItemVo; }

添加商品到购物车

相关接口:/addToCart

所需参数:skuId商品规格ID、number购买数量。

添加商品到购物车

Service层相关方法:CartServiceImpl类的addSkuToCart方法。

由于购物车是用户会进行频繁操作的一个场景,因此将购物车的数据全部放入Redis中缓存。

购物车根据登录情况分为:

在进行购物车操作前,所有请求都会被购物车拦截器拦截,如果当前请求中存在Session,则从Session中获取用户信息并保存到用户上下文(使用TheadLocal来保存),否则检查Cookie中是否存在用户标识,如果不存在则添加一个用户标识的Cookie,该用户标识用来获取临时购物车。

  • 已登录:用户购物车
  • 未登录:临时购物车
购物车拦截器

购物车在Redis中存储结构

采用hash结构,hash中的key为skuId,value为json格式的商品信息

购物车的key是用户唯一标识,保证了每个用户都有独立的购物车。

/** * 添加商品到购物车 * @param skuId 商品id * @param number 购买数量 */ @Override public void addSkuToCart(Long skuId, Integer number) { // 1.获取当前用户信息 UserInfoVo userInfoVo = UserContext.getUserInfoVo(); // 2.从购物车中获取此商品信息 CartItem cartItem = getCartItemBySkuIdAndUserInfoVo(skuId, userInfoVo); if (cartItem == null) { // 3.不存在此商品信息,则查询商品信息 cartItem = new CartItem(); final CartItem finalCartItem = cartItem; // 4.从远程查询商品信息 CompletableFuture<Void> getSkuInfoTask = CompletableFuture.runAsync(() -> { R result = productFeignService.info(skuId); if (result == null || result.getCode() != 0) { throw new RRException("获取商品[" + skuId + "]的基本信息失败"); } SkuInfoVo skuInfoVo = result.getWithSpecifiedType("skuInfo", SKU_INFO_VO_TYPE_REFERENCE); finalCartItem.setTitle(skuInfoVo.getSkuTitle()); finalCartItem.setSkuId(skuInfoVo.getSkuId()); finalCartItem.setPrice(skuInfoVo.getPrice()); finalCartItem.setImageUrl(skuInfoVo.getSkuDefaultImg()); // 默认选中商品且数量为0 finalCartItem.setCheck(true); finalCartItem.setCount(number); }); // 5.从远程查询商品销售属性值 CompletableFuture<Void> getSkuAttrListTask = CompletableFuture.runAsync(() -> { R result = productFeignService.getSaleAttrValueListBySkuId(skuId); if (result == null || result.getCode() != 0) { throw new RRException("获取商品[" + skuId + "]的销售属性值列表失败"); } List<String> skuAttrList = result.getWithSpecifiedType("data", SKU_SALE_ATTR_VALUE_VO_LIST_TYPE_REFERENCE) .stream() .map(attrValue -> String.join(":", attrValue.getAttrName(), attrValue.getAttrValue())) .collect(Collectors.toList()); finalCartItem.setSkuAttrList(skuAttrList); }); CompletableFuture<Void> allOfTask = CompletableFuture.allOf(getSkuInfoTask, getSkuAttrListTask); allOfTask.join(); if (allOfTask.isCompletedExceptionally()) { throw new RRException("添加商品[" + skuId + "]到购物车失败"); } } else { // 6.存在商品信息,则修改商品数量 cartItem.setCount(cartItem.getCount() + number); } String skuJsonString = JsonUtils.toJsonString(cartItem); if (userInfoVo.isLogged()) { // 7.用户已登录,则保存到用户购物车 redisUtils.setSkuOfUserCart(userInfoVo.getUserId(), skuId, skuJsonString); } else { // 8.用户未登录,则保存到临时购物车 redisUtils.setSkuOfTemporaryCart(userInfoVo.getUserKey(), skuId, skuJsonString); } } /** * 根据用户信息和商品id来获取购物车中的商品信息 * @param skuId 商品id * @param userInfoVo 用户信息 * @return */ private CartItem getCartItemBySkuIdAndUserInfoVo(Long skuId, UserInfoVo userInfoVo) { CartItem cartItem = null; // 如果用户登录,则从用户购物车中获取对应的商品信息(可能不存在);否则,从临时购物车中获取 if (userInfoVo.isLogged()) { String skuJsonString = redisUtils.getSkuOfUserCart(userInfoVo.getUserId(), skuId); if (skuJsonString != null) { cartItem = JsonUtils.readValue(skuJsonString, CART_ITEM_TYPE_REFERENCE); } } else { String skuJsonString = redisUtils.getSkuOfTemporaryCart(userInfoVo.getUserKey(), skuId); if (skuJsonString != null) { cartItem = JsonUtils.readValue(skuJsonString, CART_ITEM_TYPE_REFERENCE); } } return cartItem; }

查看购物车

相关接口:/cart.html

查看购物车接口

Service层相关方法:CartServiceImpl类的getCart方法。

流程:

  1. 获取用户信息
  2. 获取临时购物车的商品信息
  3. 如果用户已登录,则合并临时购物车和用户购物车
  4. 删除临时购物车,并重新获取用户购物车的商品信息
  5. 封装成Vo返回给前端
/** * 获取购物车中的所有信息 * @return */ @Override public Cart getCart() { // 1.获取用户信息 UserInfoVo userInfoVo = UserContext.getUserInfoVo(); // 2.获取临时购物车中的商品信息 List<CartItem> cartItemList = getAllCartItemByUserInfoVo(userInfoVo, true); if (userInfoVo.isLogged()) { // 3.如果用户已登录,则添加临时购物车的商品到用户购物车 cartItemList.stream().forEach(cartItem -> addSkuToCart(cartItem.getSkuId(), cartItem.getCount())); // 4.删除临时购物车 redisUtils.removeTemporaryCart(userInfoVo.getUserKey()); // 5.重新获取用户购物车的商品 cartItemList = getAllCartItemByUserInfoVo(userInfoVo, false); } Cart cart = new Cart(); cart.setItemList(cartItemList); return cart; } /** * 根据用户信息获取购物车中所有的商品信息 * @param userInfoVo 用户信息 * @param isTemporary 是否获取临时购物车中的数据 * @return */ private List<CartItem> getAllCartItemByUserInfoVo(UserInfoVo userInfoVo, boolean isTemporary) { List<Object> skuList = isTemporary ? redisUtils.getSkuListOfTemporaryCart(userInfoVo.getUserKey()) : redisUtils.getSkuListOfUserCart(userInfoVo.getUserId()); if (CollectionUtils.isEmpty(skuList)) { return Collections.emptyList(); } else { return skuList.stream() .map(sku -> JsonUtils.readValue((String) sku, CART_ITEM_TYPE_REFERENCE)) .collect(Collectors.toList()); } }

订单状态

订单状态

订单确认

在购物车列表点击结算按钮即重定向到http://order.mall.com/toTrace订单确认页。

订单确认页接口

Service层相关方法:OrderServiceImpl类的confirmOrder方法。

通过拦截器获取当前用户信息(未登录则重定向到登录页),远程查询用户收货地址列表,远程查询购物车中已选中的商品信息,远程查询商品库存信息,计算商品价格,生成一个令牌保存到Redis来防止用户重复提交(过期时间30分钟),key为order:token:{userId},value为token

  • 问题:在CompletableFuture异步任务内进行远程调用会丢失请求头,导致请求被拦截(无法获取用户信息),如何解决?

    RequestContextHolder请求上下文中获取请求头,在开启异步任务时,设置当前线程的请求头信息(请求头信息是保存在ThreadLocal,因此跨线程无效),在CompletableFuture任务启动时,就替换掉请求头信息,保持跟主请求线程一致。

  • 问题:在进行Feign远程调用时,也会丢失请求头,如何解决?

    从当前RequestContextHolder请求上下文中获取请求头,设置其中的Session即可(实际上是设置Cookie)。

/** * 订单确认页返回需要用的数据 * @return */ @Override public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException { OrderConfirmVo confirmVo = new OrderConfirmVo(); // 1.获取当前用户 MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get(); // 获取当前线程请求头信息(解决Feign异步调用丢失请求头问题) RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // 2. 远程查询用户收货地址列表 CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> { RequestContextHolder.setRequestAttributes(requestAttributes); List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId()); confirmVo.setMemberAddressVos(address); }, threadPoolExecutor); // 3. 远程查询购物车所有选中的购物项 CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> { RequestContextHolder.setRequestAttributes(requestAttributes); List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems(); confirmVo.setItems(currentCartItems); }, threadPoolExecutor).thenRunAsync(() -> { // 4. 远程查询商品库存信息 List<OrderItemVo> items = confirmVo.getItems(); List<Long> skuIds = items.stream() .map(OrderItemVo::getSkuId) .collect(Collectors.toList()); R skuHasStock = wmsFeignService.getSkuHasStock(skuIds); List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {}); if (skuStockVos != null && skuStockVos.size() > 0) { Map<Long, Boolean> skuHasStockMap = skuStockVos.stream() .collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock)); confirmVo.setStocks(skuHasStockMap); } },threadPoolExecutor); // 5. 设置用户积分 Integer integration = memberResponseVo.getIntegration(); confirmVo.setIntegration(integration); /* 在vo的getter里面封装好了计算价格 */ // 6. 防重令牌(防止表单重复提交),为用户设置一个token,三十分钟过期时间(存在redis) String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue() .set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES); confirmVo.setOrderToken(token); CompletableFuture.allOf(addressFuture,cartInfoFuture).get(); return confirmVo; }

提交订单

相关接口:http://order.mall.com/submitOrder

下单成功,则跳转到收银台页,下单失败,则返回订单确认页重新确认订单信息。

下单失败的可能原因:

  • 令牌信息过期(用户重复提交,令牌过期时间已到)
  • 订单价格发生变化
  • 库存锁定失败(库存不足)

提交订单所需参数:

  • 收获地址ID
  • 支付方式
  • 防重令牌(从订单确认页获取)
  • 应付价格
  • 订单备注
image-20220213145240894

Service层相关方法:OrderServiceImpl类的submitOrder方法。

步骤:

  1. 将请求参数保存到confirmVoThreadLocal(ThreadLocal)线程上下文中。

  2. 获取当前用户信息。

  3. 使用lua脚本校验防重令牌,校验成功则删除令牌。

    if redis.call('get', KEYS[1]) == ARGV[1] then return redic.call('del', KEYS[1]) else return 0 end
  4. 创建订单和订单项、并计算总价。

    步骤:

    1. 生成订单流水号,采用MybatisPlus的IdWorker类的getTimeId()方法。

      原理:时间戳+雪花算法生成的64位ID

    2. 创建订单,远程调用库存服务查询运费和收货地址。

    3. 创建订单项,远程调用购物车服务查询选中的订单项,并根据订单项的skuId远程调用商品服务查询商品详情。

  5. 校验价格是否发生变化(误差范围<0.01)。

  6. 保存订单和订单项到数据库。

  7. 远程调用库存服务锁定库存。

  8. 锁定成功,则发送消息给MQ,交换机为:order-event-exchange,路由键为:order.create.order

    发送消息给MQ主要作用是:

    订单到期后用户没有支付(过期时间为30分钟,设置队列order.delay.queue的参数x-message-ttl,RabbitMQ会将消息重新投入指定的队列(通过设置重投的交换机x-dead-letter-exchange: order-event-exchange和路由键x-dead-letter-routing-key: order.release.order),订单服务会修改订单状态为已取消并再次发送消息给交换机为order-event-exchange,路由键为order.release.other的队列来保存库存服务能在订单状态改变为已取消后解锁库存,库存服务也会去消费这个消息,来将库存工作单和库存工作单详情中锁定的库存给释放掉。

  9. 删除购物车中的订单数据。

  10. 锁定失败,则抛出异常。

    锁定失败如何保证被锁的库存正确释放?

    库存服务会在指定时间间隔后收到延迟队列的消息去解锁库存。

/** * 提交订单 * @param vo * @return */ @Transactional(rollbackFor = Exception.class) @Override public SubmitOrderResponseVo submitOrder(OrderSubmitVo vo) { // 1. 将请求参数保存到ThreadLocal confirmVoThreadLocal.set(vo); SubmitOrderResponseVo responseVo = new SubmitOrderResponseVo(); // 2. 获取当前用户登录的信息 MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get(); responseVo.setCode(0); // 使用lua脚本校验防重令牌是否有效,校验成功则删除令牌 String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; String orderToken = vo.getOrderToken(); Long result = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList(USER_ORDER_TOKEN_PREFIX + memberResponseVo.getId()), orderToken); if (result == 0L) { // 令牌验证失败 responseVo.setCode(1); return responseVo; } else { // 令牌验证成功 // 3. 创建订单、订单项,并计算应付总价 OrderCreateTo order = createOrder(); // 4. 校验价格是否发生变化 BigDecimal payAmount = order.getOrder().getPayAmount(); BigDecimal payPrice = vo.getPayPrice(); if (Math.abs(payAmount.subtract(payPrice).doubleValue()) < 0.01) { // 5. 实际误差小于0.01时,保存订单和订单项信息到数据库 saveOrder(order); // 6. 锁定库存, 只要有异常,回滚订单数据 // 订单号、所有订单项信息(skuId,skuNum,skuName) WareSkuLockVo lockVo = new WareSkuLockVo(); lockVo.setOrderSn(order.getOrder().getOrderSn()); // 6-1. 获取出要锁定的商品数据信息 List<OrderItemVo> orderItemVos = order.getOrderItems().stream().map((item) -> { OrderItemVo orderItemVo = new OrderItemVo(); orderItemVo.setSkuId(item.getSkuId()); orderItemVo.setCount(item.getSkuQuantity()); orderItemVo.setTitle(item.getSkuName()); return orderItemVo; }).collect(Collectors.toList()); lockVo.setLocks(orderItemVos); // 6-2. 调用远程锁定库存的方法 // 可能出现的问题: // 1. 扣减库存成功了,但是由于网络原因超时,出现异常,导致订单事务回滚, // 库存事务不回滚(解决方案:seata) // 2. 为了保证高并发,不推荐使用seata,因为是加锁,并行化,提升不了效率, // 可以发消息给库存服务 R r = wmsFeignService.orderLockStock(lockVo); if (r.getCode() == 0) { //锁定成功 responseVo.setOrder(order.getOrder()); // 7. 订单创建成功,发送消息给MQ rabbitTemplate.convertAndSend("order-event-exchange", "order.create.order", order.getOrder()); // 8. 删除购物车里的数据 redisTemplate.delete(CART_PREFIX + memberResponseVo.getId()); return responseVo; } else { // 9. 锁定失败,抛出异常 String msg = (String) r.get("msg"); throw new NoStockException(msg); } } else { responseVo.setCode(2); return responseVo; } } }

自动关单

当创建订单、锁库存步骤完成后,订单服务就会发送订单创建成功消息给MQ中的延时队列,30min后延时队列会将消息重新投递给订单服务的关单队列和库存服务的释放库存队列。

队列监听器(消费者):OrderCloseListener

自动关单队列监听器

Service层相关方法:OrderServiceImpl类的closeOrder方法

步骤:

  1. 查询数据库中的订单状态是否为已支付,如果订单已支付则丢弃该消息,否则进入下一步
  2. 修改订单状态为已取消,并重新发送消息给MQ,保证库存服务能解锁库存
// 关闭订单 @Override public void closeOrder(OrderEntity orderEntity) { // 关闭订单之前先查询一下数据库,判断此订单状态是否已支付 OrderEntity orderInfo = this.getOne(new QueryWrapper<OrderEntity>(). eq("order_sn", orderEntity.getOrderSn())); if (orderInfo.getStatus().equals(OrderStatusEnum.CREATE_NEW.getCode())) { // 代付款状态进行关单 OrderEntity orderUpdate = new OrderEntity(); orderUpdate.setId(orderInfo.getId()); orderUpdate.setStatus(OrderStatusEnum.CANCLED.getCode()); this.updateById(orderUpdate); // 发送消息给MQ OrderTo orderTo = new OrderTo(); BeanUtils.copyProperties(orderInfo, orderTo); try { // 确保每个消息发送成功,给每个消息做好日志记录,(给数据库保存每一个详细信息)保存每个消息的详细信息 rabbitTemplate.convertAndSend("order-event-exchange", "order.release.other", orderTo); } catch (Exception e) { // 定期扫描数据库,重新发送失败的消息 } } }

🔒锁库存

相关接口:/ware/waresku/lock/order

锁库存接口

Service层相关方法:WareSkuServiceImpl类中的addStock方法

步骤:

  1. 保存订单关联的工作单信息

  2. 查询出有需要锁定的订单项库存的仓库

  3. 遍历每一个订单项,依次遍历仓库

    1. 该订单项没有存在有其库存的仓库,抛出异常,回滚所有数据

    2. 遍历完所有仓库后,锁定成功,保存该订单项对应的工作单详情到数据库,并发送消息给MQ

      发送到交换机为stock-event-exchange,路由键为stock.locked的延迟队列中。

      该延迟队列会等待两分钟(主要是为给订单服务一定的缓冲时间),将消息投递到交换机为stock-event-exchange,路由键为stock.release的队列。

      库存服务就会监听该队列中的消息,当收到该消息时

    3. 遍历完所有仓库后,锁定失败,抛出异常,回滚所有数据

锁定库存对应的SQL语句

UPDATE wms_ware_sku SET stock_locked = stock_locked + #{num} WHERE sku_id = #{skuId} AND ware_id = #{wareId} AND stock - stock_locked > 0
/** * 为某个订单锁定库存 */ @Transactional(rollbackFor = Exception.class) @Override public boolean orderLockStock(WareSkuLockVo vo) { // 1. 保存订单关联的库存工作单 WareOrderTaskEntity wareOrderTaskEntity = new WareOrderTaskEntity(); wareOrderTaskEntity.setOrderSn(vo.getOrderSn()); wareOrderTaskEntity.setCreateTime(new Date()); wareOrderTaskService.save(wareOrderTaskEntity); // 按照下单的收货地址,找到一个就近仓库,锁定库存 List<OrderItemVo> locks = vo.getLocks(); List<SkuWareHasStock> collect = locks.stream().map((item) -> { SkuWareHasStock stock = new SkuWareHasStock(); Long skuId = item.getSkuId(); stock.setSkuId(skuId); stock.setNum(item.getCount()); // 2. 查询有指定sku库存的仓库 List<Long> wareIdList = wareSkuDao.listWareIdHasSkuStock(skuId); stock.setWareId(wareIdList); return stock; }).collect(Collectors.toList()); // 3. 锁定库存 for (SkuWareHasStock hasStock : collect) { boolean skuStocked = false; Long skuId = hasStock.getSkuId(); List<Long> wareIds = hasStock.getWareId(); if (org.springframework.util.StringUtils.isEmpty(wareIds)) { // 没有任何仓库有这个商品的库存,则抛出异常 throw new NoStockException(skuId); } // 3-1. 锁定成功, 将当前商品锁定了几件的工作单记录发给MQ // 3-2. 锁定失败, 前面保存的工作单信息都回滚了。发送出去的消息,即使要解锁库存,由于在数据库查不到指定的id,所以就不用解锁 for (Long wareId : wareIds) { // 锁定成功就返回1,失败就返回0 Long count = wareSkuDao.lockSkuStock(skuId, wareId, hasStock.getNum()); if (count == 1) { skuStocked = true; WareOrderTaskDetailEntity taskDetailEntity = WareOrderTaskDetailEntity.builder() .skuId(skuId) .skuName("") .skuNum(hasStock.getNum()) .taskId(wareOrderTaskEntity.getId()) .wareId(wareId) .lockStatus(1) .build(); wareOrderTaskDetailService.save(taskDetailEntity); // 发送库存锁定成功消息到MQ StockLockedTo lockedTo = new StockLockedTo(); lockedTo.setId(wareOrderTaskEntity.getId()); StockDetailTo detailTo = new StockDetailTo(); BeanUtils.copyProperties(taskDetailEntity, detailTo); lockedTo.setDetailTo(detailTo); rabbitTemplate.convertAndSend("stock-event-exchange", "stock.locked", lockedTo); break; } else { // 当前仓库锁失败,重试下一个仓库 } } if (skuStocked == false) { // 当前商品所有仓库都没有锁住,抛出异常,回滚所有工作单 throw new NoStockException(skuId); } } // 肯定全部都是锁定成功的 return true; }

🔓解锁库存

解锁库存主要有两个触发点:

  1. 下单成功,锁库存成功,但接下来的业务调用失败,因此要释放掉已锁定的库存。
  2. 订单超时未支付,导致释放库存。

消息投递的流程:

订单服务 -> 订单创建成功消息(自动关单队列) -> (30min后) 解锁库存消息 库存服务 -> 所库存消息(解锁库存延迟队列) -> (50min后) 解锁库存消息

注意:并不是发送了解锁库存消息就一定会解锁库存,在业务逻辑里面会先查询订单或者工作单的状态来判断是否需要解锁库存,来保证幂等性

队列监听器(消费者):StockReleaseListener

@Slf4j @RabbitListener(queues = "stock.release.stock.queue") @Service public class StockReleaseListener { @Autowired private WareSkuService wareSkuService; /** * 1、库存自动解锁 * 下订单成功,库存锁定成功,接下来的业务调用失败,导致订单回滚。之前锁定的库存就要自动解锁 * * 2、订单失败 * 库存锁定失败 * * 注意:只要解锁库存的消息失败,一定要告诉服务解锁失败 */ @RabbitHandler public void handleStockLockedRelease(StockLockedTo to, Message message, Channel channel) throws IOException { log.info("******收到解锁库存的信息******"); try { // 解锁库存 wareSkuService.unlockStock(to); // 手动删除消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { // 解锁失败 将消息重新放回队列,让别人消费 channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); } } // 订单超时未支付 @RabbitHandler public void handleOrderCloseRelease(OrderTo orderTo, Message message, Channel channel) throws IOException { log.info("******收到订单关闭,准备解锁库存的信息******"); try { wareSkuService.unlockStock(orderTo); // 手动删除消息 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { // 解锁失败 将消息重新放回队列,让别人消费 channel.basicReject(message.getMessageProperties().getDeliveryTag(), true); } }

Service层相关方法:WareSkuServiceImpl类的unlockStock方法

流程:

  • 根据工作单项详情来解释库存

    1. 获取工作单ID
    2. 查询工作单项详情,如果不存在则返回(说明数据已回滚),如果存在则进入下一步
    3. 远程查询订单状态,如果订单不存在或订单状态为已取消则释放库存,否则丢弃该消息
    4. 若远程调用失败,则抛出异常,让其他消费者消费该消息(抛出异常主要是为了让其他消费者能查询到订单的最新状态)
  • 根据订单来解锁库存

    1. 获取订单流水号
    2. 查询工作单的最新状态,如果工作单不存在或工作单状态不为锁定状态,则丢弃该消息,否则进入下一步
    3. 查询出工作单下已锁定的工作单项,根据工作单项解锁库存。

解锁库存SQL语句

UPDATE wms_ware_sku SET stock_locked = stock_locked - #{num} WHERE sku_id = ${skuId} AND ware_id = #{wareId}
// 根据工作单项详情来解锁库存 @Override public void unlockStock(StockLockedTo to) { // 获取库存工作单的id StockDetailTo detail = to.getDetailTo(); Long detailId = detail.getId(); /** * 查询数据库关于这个订单锁定库存信息 * a. 有, 证明库存锁定成功了, 不一定解锁库存 * 1. 订单状态为已取消, 则解锁库存 * 2. 订单状态为已支付, 不能解锁库存 * b. 没有这个订单,必须解锁库存 */ WareOrderTaskDetailEntity taskDetailInfo = wareOrderTaskDetailService.getById(detailId); if (taskDetailInfo != null) { // 查出 wms_ware_order_task 工作单的信息 Long id = to.getId(); WareOrderTaskEntity orderTaskInfo = wareOrderTaskService.getById(id); // 获取订单号查询订单状态 String orderSn = orderTaskInfo.getOrderSn(); // 远程查询订单信息 R orderData = orderFeignService.getOrderStatus(orderSn); if (orderData.getCode() == 0) { // 订单数据返回成功 OrderVo orderInfo = orderData.getData("data", new TypeReference<OrderVo>() { }); // 判断订单状态是否已取消或者支付或者订单不存在 if (orderInfo == null || orderInfo.getStatus() == 4) { // 订单已被取消,才能解锁库存 if (taskDetailInfo.getLockStatus() == 1) { //当前库存工作单详情状态1,已锁定,但是未解锁才可以解锁 unLockStock(detail.getSkuId(), detail.getWareId(), detail.getSkuNum(), detailId); } } } else { // 消息拒绝以后重新放在队列里面,让别人继续消费解锁 // 远程调用服务失败 throw new RuntimeException("远程调用服务失败"); } } } /** * 防止订单服务卡顿,导致订单状态消息一直改不了,库存优先到期,查订单状态新建,什么都不处理 * 导致卡顿的订单,永远都不能解锁库存 * @param orderTo */ @Transactional(rollbackFor = Exception.class) @Override public void unlockStock(OrderTo orderTo) { String orderSn = orderTo.getOrderSn(); WareOrderTaskEntity orderTaskEntity = wareOrderTaskService.getOrderTaskByOrderSn(orderSn); // 查一下最新的库存解锁状态,防止重复解锁库存 if (orderTaskEntity != null && orderTaskEntity.getTaskStatus().equals(1)) { // 按照工作单的id找到所有 没有解锁的库存,进行解锁 Long id = orderTaskEntity.getId(); List<WareOrderTaskDetailEntity> list = wareOrderTaskDetailService.list(new QueryWrapper<WareOrderTaskDetailEntity>() .eq("task_id", id).eq("lock_status", 1)); for (WareOrderTaskDetailEntity taskDetailEntity : list) { unLockStock(taskDetailEntity.getSkuId(), taskDetailEntity.getWareId(), taskDetailEntity.getSkuNum(), taskDetailEntity.getId()); } } } /** * 解锁库存的方法 * @param skuId * @param wareId * @param num * @param taskDetailId */ public void unLockStock(Long skuId, Long wareId, Integer num, Long taskDetailId) { // 库存解锁 wareSkuDao.unLockStock(skuId, wareId, num); // 更新工作单的状态 WareOrderTaskDetailEntity taskDetailEntity = new WareOrderTaskDetailEntity(); taskDetailEntity.setId(taskDetailId); taskDetailEntity.setLockStatus(2); wareOrderTaskDetailService.updateById(taskDetailEntity); }

支付

当用户提交订单,跳转到收银台页,用户选择相应的支付宝支付后,会发起对应请求跳转到支付包的付款界面。

相关接口:http://order.mall.com/aliPayOrder

主要流程:

  1. 根据订单号获取支付信息(总金额、商品描述、订单项属性、订单项名称)

  2. 向支付宝网关(https://openapi.alipaydev.com/gateway.do)发起请求,返回一个html页面,后台服务再返回该内容给用户

    发起请求时,需要设置支付成功同步回调接口(支付成功后显示的界面)和支付成功的异步调用接口。

    • 订单支付成功后支付宝会回调商户接口,这个时候需要修改订单状态。
    • 由于同步跳转可能由于网络问题失败,所以使用异步通知。
    • 支付宝使用的是最大努力通知方案,保障数据一致性,隔一段时间会通知商户支付成功,直到返回success

支付宝的加密原理

  • 支付宝加密采用RSA非对称加密,分别在商户端和支付宝端有两对公钥和私钥。
  • 在发送订单数据时,直接使用明文,但会使用商户私钥加一个对应的签名,支付宝端会使用商户公钥对签名进行验签,只有数据明文和签名对应的时候才能说明传输正确。
  • 支付成功后,支付宝发送支付成功数据之外,还会使用支付宝私钥加一个对应的签名,商户端收到支付成功数据之后也会使用支付宝公钥延签,成功后才能确认。

支付宝支付

同步调用接口:MemberWebController类的memberOrderPage方法

异步调用接口:

当用户在支付宝支付成功后,支付宝就会调用我们提供的notify接口,直到调用成功(即接口返回success)。

异步接口的处理步骤:

  1. 使用支付宝提供的SDK验证数据和签名
  2. 将交易信息保存数据库中
  3. 检查订单状态,修改订单状态
  4. 返回success
@PostMapping(value = "/payed/notify") public String handleAlipayed(PayAsyncVo asyncVo, HttpServletRequest request) throws AlipayApiException { /* 只要收到支付宝的异步通知,返回 success 支付宝便不再通知, 获取支付宝POST过来反馈信息 */ // 验证数据和签名 Map<String, String> params = new HashMap<>(); Map<String, String[]> requestParams = request.getParameterMap(); for (String name : requestParams.keySet()) { String[] values = requestParams.get(name); String valueStr = ""; for (int i = 0; i < values.length; i++) { valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ","; } // 乱码解决,这段代码在出现乱码时使用 params.put(name, valueStr); } // 用SDK验证签名 boolean signVerified = AlipaySignature.rsaCheckV1(params, alipayTemplate.getAlipay_public_key(), alipayTemplate.getCharset(), alipayTemplate.getSign_type()); if (signVerified) { System.out.println("签名验证成功..."); // 去修改订单状态 return orderService.handlePayResult(asyncVo); } else { System.out.println("签名验证失败..."); return "error"; } } /** * 处理支付宝的支付结果 * @param asyncVo * @return */ @Transactional(rollbackFor = Exception.class) @Override public String handlePayResult(PayAsyncVo asyncVo) { // 保存交易流水信息到数据库中 PaymentInfoEntity paymentInfo = new PaymentInfoEntity(); paymentInfo.setOrderSn(asyncVo.getOut_trade_no()); paymentInfo.setAlipayTradeNo(asyncVo.getTrade_no()); paymentInfo.setTotalAmount(new BigDecimal(asyncVo.getBuyer_pay_amount())); paymentInfo.setSubject(asyncVo.getBody()); paymentInfo.setPaymentStatus(asyncVo.getTrade_status()); paymentInfo.setCreateTime(new Date()); paymentInfo.setCallbackTime(asyncVo.getNotify_time()); this.paymentInfoService.save(paymentInfo); // 修改订单状态 String tradeStatus = asyncVo.getTrade_status(); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { // 支付成功状态 String orderSn = asyncVo.getOut_trade_no(); //获取订单号 this.updateOrderStatus(orderSn, OrderStatusEnum.PAYED.getCode(), PayConstant.ALIPAY); } return "success"; }

RabbitMQ中涉及到的队列

消息队列流程


商品秒杀

秒杀商品上架流程

  1. 秒杀项目单独抽出一个模块,即gulimall-seckill
  2. 使用定时任务每天隔一个小时扫描一次数据库获取最近三天的秒杀商品。
  3. 秒杀链接加密,为秒杀商品添加唯一的商品随机码,在开始秒杀时才暴露接口。
  4. 库存预热,先从数据库中扣减一部分库存,以Redission信号量的形式保存到Redis中。
  5. 使用消息队列削峰,秒杀成功后立即返回,以发送消息的形式创建订单。

秒杀商品上架流程

秒杀流程

秒杀流程

Redis模型设计

  • 场次信息:seckill:sessions:{开始时间 + '_' + 结束时间}list结构

    里面的元素是场次ID + '-' + 商品ID的字符串

  • 秒杀商品信息:seckill:skushash结构,所有的秒杀商品都保存于此

    里面的元素是以场次ID + '-' + 商品ID为key,SeckillSkuRedisTo类的json字符串为value的键值对。

    SeckillSkuRedisTo

    @Data public class SeckillSkuRedisTo { private Long promotionId; // 活动id private Long promotionSessionId; // 活动场次id private Long skuId; // 商品id private BigDecimal seckillPrice; // 秒杀价格 private Integer seckillCount; // 秒杀总量 private Integer seckillLimit; // 每人限购数量 private Integer seckillSort; // 排序 private SkuInfoVo skuInfo; // sku的详细信息 private Long startTime; // 当前商品秒杀的开始时间 private Long endTime; // 当前商品秒杀的结束时间 private String randomCode; // 当前商品秒杀的随机码 }
  • 秒杀库存:seckill:stock:{秒杀随机码}set结构,分布式信号(Redission),存储秒杀商品总量。

定时上架最近的三天秒杀活动

通过SpringBoot自带的@EnableScheduling注解来启用定时任务,并且配合@EnableAsync注解开启异步任务防止定时任务阻塞请求处理线程。

定时任务相关方法:SeckillScheduled类下的uploadSeckillSkuLatest3Days方法。

使用Redission分布式锁seckill:upload:lock来防止多个服务同时上架商品。

定时任务配置

把秒杀活动和商品缓存到Redis中:

流程:

  1. 调用远程的优惠卷服务获取最近三天需要参加秒杀活动的场次和商品信息
  2. 缓存场次信息,判断该场次是否已经添加?如果未添加,则以seckill:sessions:{startTime_endTime}为key添加到Redis的队列中
  3. 缓存商品信息,将所有秒杀商品信息都保存到seckill:skus的hash结构中。hash-key为场次ID + '-' + 商品ID,同时把商品秒杀数量放入到以seckill:stock:{秒杀随即码}为key的分布式信号量中。
private final String SESSION__CACHE_PREFIX = "seckill:sessions:"; private final String SECKILL_CHARE_PREFIX = "seckill:skus"; private final String SKU_STOCK_SEMAPHORE = "seckill:stock:"; //+商品随机码 // 上架最近三天需要参加秒杀活动的商品 @Override public void uploadSeckillSkuLatest3Days() { // 1、远程调用优惠卷服务获取最近三天需要参加秒杀活动的商品 R lates3DaySession = couponFeignService.getLates3DaySession(); if (lates3DaySession.getCode() == 0) { List<SeckillSessionWithSkusVo> sessionData = lates3DaySession.getData("data", new TypeReference<List<SeckillSessionWithSkusVo>>() {}); // 2、缓存场次信息 saveSessionInfos(sessionData); // 3、缓存活动的关联商品信息 saveSessionSkuInfo(sessionData); } } /** * 缓存秒杀活动信息 * @param sessions */ private void saveSessionInfos(List<SeckillSessionWithSkusVo> sessions) { sessions.stream().forEach(session -> { // 1. 获取当前活动的开始和结束时间的时间戳 long startTime = session.getStartTime().getTime(); long endTime = session.getEndTime().getTime(); // 存入到Redis中的key String key = SESSION__CACHE_PREFIX + startTime + "_" + endTime; // 2. 判断Redis中是否有该信息,如果没有才进行添加 Boolean hasKey = redisTemplate.hasKey(key); // 3. 缓存活动信息 if (!hasKey) { // 将 `场次ID + '-' + 商品ID` 添加到 `seckill:sessions:{startTime_endTime}` 的队列中 List<String> skuIds = session.getRelationSkus() .stream() .map(item -> item.getPromotionSessionId() + "-" + item.getSkuId()) .collect(Collectors.toList()); redisTemplate.opsForList().leftPushAll(key, skuIds); } }); } /** * 缓存秒杀活动所关联的商品信息 * @param sessions */ private void saveSessionSkuInfo(List<SeckillSessionWithSkusVo> sessions) { sessions.stream().forEach(session -> { // 准备hash操作,绑定hash,key为seckill:skus BoundHashOperations<String, Object, Object> operations = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); session.getRelationSkus().stream().forEach(seckillSkuVo -> { // 1. 生成随机码 String token = UUID.randomUUID().toString().replace("-", ""); String redisKey = seckillSkuVo.getPromotionSessionId() + "-" + seckillSkuVo.getSkuId(); if (!operations.hasKey(redisKey)) { // 如果不存在对应场次的秒杀商品信息,则进行缓存 SeckillSkuRedisTo redisTo = new SeckillSkuRedisTo(); Long skuId = seckillSkuVo.getSkuId(); // 2. 调用远程商品服务查询sku的基本信息, R info = productFeignService.getSkuInfo(skuId); if (info.getCode() == 0) { SkuInfoVo skuInfo = info.getData("skuInfo", new TypeReference<SkuInfoVo>() {}); redisTo.setSkuInfo(skuInfo); } // 3-1. 设置需要保存的sku秒杀信息 BeanUtils.copyProperties(seckillSkuVo, redisTo); // 3-2. 设置当前商品的秒杀时间信息 redisTo.setStartTime(session.getStartTime().getTime()); redisTo.setEndTime(session.getEndTime().getTime()); // 3-3. 设置商品的随机码(防止恶意攻击) redisTo.setRandomCode(token); // 4. 序列化json格式存入Redis中 String seckillValue = JSON.toJSONString(redisTo); String key = String.format("%s-%s", seckillSkuVo.getPromotionSessionId(), seckillSkuVo.getSkuId()); operations.put(key, seckillValue); // 5. 使用库存作为分布式Redisson信号量(限流) // 使用库存作为分布式信号量 RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token); // 商品的秒杀数量作为信号量 semaphore.trySetPermits(seckillSkuVo.getSeckillCount()); } }); }); }

商城首页(显示秒杀活动信息)

相关接口:http://seckill.mall.com/getCurrentSeckillSkus

商城首页(显示秒杀活动信息)页面

商城首页(显示秒杀活动信息)接口

Service层相关方法:SeckillServiceImpl类的getCurrentSeckillSkus方法

/** * 获取到当前可以参加秒杀商品的信息 * @return */ @SentinelResource(value = "getCurrentSeckillSkusResource", blockHandler = "blockHandler") @Override public List<SeckillSkuRedisTo> getCurrentSeckillSkus() { try (Entry entry = SphU.entry("seckillSkus")) { // 1. 确定当前属于哪个秒杀场次 long currentTime = System.currentTimeMillis(); //从Redis中查询到所有key以seckill:sessions开头的所有数据 Set<String> keys = redisTemplate.keys(SESSION__CACHE_PREFIX + "*"); for (String key : keys) { // seckill:sessions:{startTime_endTime} String replace = key.replace(SESSION__CACHE_PREFIX, ""); String[] s = replace.split("_"); // 获取存入Redis商品的开始时间 long startTime = Long.parseLong(s[0]); // 获取存入Redis商品的结束时间 long endTime = Long.parseLong(s[1]); // 判断是否是当前秒杀场次 if (currentTime >= startTime && currentTime <= endTime) { // 2. 获取这个秒杀场次需要的所有商品信息 List<String> range = redisTemplate.opsForList().range(key, -100, 100); BoundHashOperations<String, String, String> hasOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); assert range != null; List<String> listValue = hasOps.multiGet(range); if (listValue != null && listValue.size() >= 0) { List<SeckillSkuRedisTo> collect = listValue.stream().map(item -> { String items = (String) item; SeckillSkuRedisTo redisTo = JSON.parseObject(items, SeckillSkuRedisTo.class); return redisTo; }).collect(Collectors.toList()); return collect; } break; } } } catch (BlockException e) { log.error("资源被限流{}", e.getMessage()); } return null; }

商品详情(携带秒杀活动信息)

相关接口:/{skuId}.html

商品详情(携带秒杀活动信息)

Service层相关方法:SkuInfoServiceImpl类的item方法。

步骤:

  1. 获取sku的基本信息
  2. 获取spu的销售属性集合
  3. 获取spu的介绍
  4. 获取spu的规格参数
  5. 获取spu的图片信息
  6. 远程查询秒杀服务该商品是否有参与秒杀活动
// 查看商品详情 @Override public SkuItemVo item(Long skuId) throws ExecutionException, InterruptedException { SkuItemVo skuItemVo = new SkuItemVo(); CompletableFuture<SkuInfoEntity> infoFuture = CompletableFuture.supplyAsync(() -> { //1、sku基本信息的获取 pms_sku_info SkuInfoEntity info = this.getById(skuId); skuItemVo.setInfo(info); return info; }, executor); CompletableFuture<Void> saleAttrFuture = infoFuture.thenAcceptAsync((res) -> { //3、获取spu的销售属性组合 List<SkuItemSaleAttrVo> saleAttrVos = skuSaleAttrValueService.getSaleAttrBySpuId(res.getSpuId()); skuItemVo.setSaleAttr(saleAttrVos); }, executor); CompletableFuture<Void> descFuture = infoFuture.thenAcceptAsync((res) -> { //4、获取spu的介绍 pms_spu_info_desc SpuInfoDescEntity spuInfoDescEntity = spuInfoDescService.getById(res.getSpuId()); skuItemVo.setDesc(spuInfoDescEntity); }, executor); CompletableFuture<Void> baseAttrFuture = infoFuture.thenAcceptAsync((res) -> { //5、获取spu的规格参数信息 List<SpuItemAttrGroupVo> attrGroupVos = attrGroupService.getAttrGroupWithAttrsBySpuId(res.getSpuId(), res.getCatalogId()); skuItemVo.setGroupAttrs(attrGroupVos); }, executor); // Long spuId = info.getSpuId(); // Long catalogId = info.getCatalogId(); //2、sku的图片信息 pms_sku_images CompletableFuture<Void> imageFuture = CompletableFuture.runAsync(() -> { List<SkuImagesEntity> imagesEntities = skuImagesService.getImagesBySkuId(skuId); skuItemVo.setImages(imagesEntities); }, executor); CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> { //3、远程调用查询当前sku是否参与秒杀优惠活动 R skuSeckilInfo = seckillFeignService.getSkuSeckilInfo(skuId); if (skuSeckilInfo.getCode() == 0) { //查询成功 SeckillSkuVo seckilInfoData = skuSeckilInfo.getData("data", new TypeReference<SeckillSkuVo>() { }); skuItemVo.setSeckillSkuVo(seckilInfoData); if (seckilInfoData != null) { long currentTime = System.currentTimeMillis(); if (currentTime > seckilInfoData.getEndTime()) { skuItemVo.setSeckillSkuVo(null); } } } }, executor); //等到所有任务都完成 CompletableFuture.allOf(saleAttrFuture,descFuture,baseAttrFuture,imageFuture,seckillFuture).get(); return skuItemVo; }
查询商品是否参与秒杀活动

相关接口:mall-seckill/sku/seckill/{skuId}

参数:skuId

查询商品是否参与秒杀活动

Service层相关方法:SeckillServiceImpl类的getSkuSeckilInfo方法

/** * 根据skuId查询商品是否参加秒杀活动 * @param skuId * @return */ @Override public SeckillSkuRedisTo getSkuSeckilInfo(Long skuId) { // 1. 找到所有需要秒杀的商品的key信息 => `seckill:skus` BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); Set<String> keys = hashOps.keys(); // 2. 遍历所有key, 使用正则表达式匹配 if (keys != null && keys.size() > 0) { // 正则表达式进行匹配 String reg = "\\d-" + skuId; for (String key : keys) { // 如果匹配上了 if (Pattern.matches(reg, key)) { // 从Redis中取出数据来 String redisValue = hashOps.get(key); // 进行序列化 SeckillSkuRedisTo redisTo = JSON.parseObject(redisValue, SeckillSkuRedisTo.class); // 随机码 Long currentTime = System.currentTimeMillis(); Long startTime = redisTo.getStartTime(); Long endTime = redisTo.getEndTime(); // 3. 检查是否到了秒杀时间 // 如果当前时间大于等于秒杀活动开始时间并且要小于活动结束时间 if (currentTime >= startTime && currentTime <= endTime) { return redisTo; } redisTo.setRandomCode(null); return redisTo; } } } return null; }

前端展示:

如果不存在秒杀活动,则按照正常逻辑显示,若存在秒杀活动,则进行下一步判断。

如果未到秒杀活动时间,则在页面展示秒杀活动开启时间;否则,显示秒杀价。

如果秒杀活动进行中,则显示立即抢购按钮。

秒杀活动提示

立即抢购按钮

立即抢购按钮js代码

秒杀

相关接口:/kill

参数:kill_id(格式:场次ID-skuID),code(秒杀随机码),num(购买数量)

秒杀接口

Service层相关方法:SeckillServiceImpl类的kill方法

步骤:

  1. 获取当前用户信息。
  2. 根据killId从Redis中获取商品详情,如果Redis中不存在则返回null。
  3. 合法性校验
    • 判断当前时间是否在秒杀活动时间范围内
    • 判断随机码是否正确
    • 判断killID是否正确
  4. 获取信号量的值,判断当前用户购买的数量是否在限购范围内且库存量是否充足。
  5. 使用用户id-场次ID-商品ID作为key,判断Redis中是否存在该key,存在则说明该用户已购买过,否则设置该key的值为购买数量,过期时间为当前时间减去活动结束时间。(保证一个用户只能在该场次内购买一次)
  6. 尝试扣减信号量,扣减成功则生成订单号和订单相关信息发送给MQ,返回订单号。
  7. 否则返回null。
/** * 当前商品进行秒杀(秒杀开始) * @param killId * @param key * @param num * @return */ @Override public String kill(String killId, String key, Integer num) throws InterruptedException { // 1. 获取当前用户的信息 MemberResponseVo user = LoginUserInterceptor.loginUser.get(); // 2. 获取当前秒杀商品的详细信息从Redis中获取 BoundHashOperations<String, String, String> hashOps = redisTemplate.boundHashOps(SECKILL_CHARE_PREFIX); String skuInfoValue = hashOps.get(killId); if (StringUtils.isEmpty(skuInfoValue)) { return null; } // (合法性效验) SeckillSkuRedisTo redisTo = JSON.parseObject(skuInfoValue, SeckillSkuRedisTo.class); Long startTime = redisTo.getStartTime(); Long endTime = redisTo.getEndTime(); long currentTime = System.currentTimeMillis(); // 3. 判断当前这个秒杀请求是否在活动时间区间内(效验时间的合法性) if (currentTime >= startTime && currentTime <= endTime) { // 4. 效验随机码和商品id String randomCode = redisTo.getRandomCode(); String skuId = redisTo.getPromotionSessionId() + "-" + redisTo.getSkuId(); if (randomCode.equals(key) && killId.equals(skuId)) { // 5. 验证购物数量是否合理和库存量是否充足 Integer seckillLimit = redisTo.getSeckillLimit(); // 获取信号量 String seckillCount = redisTemplate.opsForValue().get(SKU_STOCK_SEMAPHORE + randomCode); int count = Integer.parseInt(seckillCount); // 判断信号量是否大于0,并且买的数量不能超过库存 if (count > 0 && num <= seckillLimit && count > num) { //4、验证这个人是否已经买过了(幂等性处理),如果秒杀成功,就去占位。userId-sessionId-skuId // SETNX 原子性处理 String redisKey = user.getId() + "-" + skuId; // 设置自动过期(活动结束时间-当前时间) long ttl = endTime - currentTime; if (redisTemplate.opsForValue().setIfAbsent(redisKey, num.toString(), ttl, TimeUnit.MILLISECONDS)) { //占位成功说明从来没有买过,分布式锁(获取信号量-1) RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + randomCode); // 秒杀成功,快速下单 boolean semaphoreCount = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS); // 保证Redis中还有商品库存 if (semaphoreCount) { // 创建订单号和订单信息发送给MQ // 秒杀成功 快速下单 发送消息到 MQ 整个操作时间在 10ms 左右 String timeId = IdWorker.getTimeId(); SeckillOrderTo orderTo = new SeckillOrderTo(); orderTo.setOrderSn(timeId); orderTo.setMemberId(user.getId()); orderTo.setNum(num); orderTo.setPromotionSessionId(redisTo.getPromotionSessionId()); orderTo.setSkuId(redisTo.getSkuId()); orderTo.setSeckillPrice(redisTo.getSeckillPrice()); rabbitTemplate.convertAndSend("order-event-exchange", "order.seckill.order", orderTo); return timeId; } } } } } return null; }

消息队列处理逻辑:

秒杀快速下单队列

Service层相关方法:OrderServiceImpl类的createSeckillOrder方法

主要就是将订单信息保存到数据库,并保存订单项信息到数据库。

/** * 创建秒杀单 * @param orderTo */ @Override public void createSeckillOrder(SeckillOrderTo orderTo) { // 保存订单信息 OrderEntity orderEntity = new OrderEntity(); orderEntity.setOrderSn(orderTo.getOrderSn()); orderEntity.setMemberId(orderTo.getMemberId()); orderEntity.setCreateTime(new Date()); BigDecimal totalPrice = orderTo.getSeckillPrice().multiply(BigDecimal.valueOf(orderTo.getNum())); orderEntity.setPayAmount(totalPrice); orderEntity.setStatus(OrderStatusEnum.CREATE_NEW.getCode()); // 保存订单 this.save(orderEntity); // 保存订单项信息 OrderItemEntity orderItem = new OrderItemEntity(); orderItem.setOrderSn(orderTo.getOrderSn()); orderItem.setRealAmount(totalPrice); orderItem.setSkuQuantity(orderTo.getNum()); // 保存商品的spu信息 R spuInfo = productFeignService.getSpuInfoBySkuId(orderTo.getSkuId()); SpuInfoVo spuInfoData = spuInfo.getData("data", new TypeReference<SpuInfoVo>() {}); orderItem.setSpuId(spuInfoData.getId()); orderItem.setSpuName(spuInfoData.getSpuName()); orderItem.setSpuBrand(spuInfoData.getBrandName()); orderItem.setCategoryId(spuInfoData.getCatalogId()); // 保存订单项数据 orderItemService.save(orderItem); }

SpringCloud组件

常见问题

微服务和RPC的区别?

  • 微服务是指一种将一个单一应用程序开发为一组小型服务的方法,每个服务运行在自己的进程中,服务间通信采用轻量级通信机制(通常用HTTP资源API)。可通过全自动部署机制独立部署,共用一个最小型的集中式的管理。服务可用不同的语言开发,使用不同的数据存储技术。
  • RPC全称Remote Procedure Call,即远程过程调用。其本质上其实就是主机A通过某种网络协议向支持相同协议的主机B发送一个任务执行命令,并且在某些情况下,还能支持任务执行结果的返回。

Sentinel服务流控、熔断和降级

流量规则

  • 资源名:唯一名称,默认请求路径

  • 针对来源:sentinel可以针对调用者进行限流,填写微服务名,默认不区分来源

  • 阈值类型/单机值:

    • QPS(每秒钟的请求数量):当调用该api就QPS达到阈值的时候,进行限流

    • 线程数:当调用该api的线程数达到阈值的时候,进行限流

  • 单机/均摊阈值:和下面的选项有关

  • 集群阈值模式:

    • 单机均摊:前面设置的阈值是每台机器的阈值
    • 总体阈值:前面设置的阈值是集群总体的阈值
  • 流控模式:

    • 直接:api达到限流条件时,直接限流,分为QPS和线程数

    • 关联:当关联的资源到达阈值时,就限流自己。别人惹事,自己买单。当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。

      举例来说,read_db 和 write_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的:设置 strategy 为 RuleConstant.STRATEGY_RELATE 同时设置 refResource 为 write_db。这样当写库操作过于频繁时,读数据的请求会被限流。

    • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】

  • 流控效果:

    • 快速失败:直接拒绝。当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException
    • warm up:若干秒后才能达到阈值。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮
    • 排队等待:让请求以均匀的速度通过

自定义限流响应

Sentinel自定义限流响应

网关层限流

<!-- 引入sentinel网关限流 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> <version>2.1.0.RELEASE</version> </dependency>

Feign的流控和限流

默认情况下,Sentinel不会对Feign远程调用进行监控,需要开启配置。在@FeignClient注解中指定属性fallback

feign: sentinel: enabled: true

FeignClient注解设置fallback属性

Feign结合Sentinel限流的实现类

服务降级和服务熔断的区别?

  • 服务降级:系统有限的资源的合理协调。

    服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行。

  • 服务熔断:应对雪崩效应的链路自我保护机制,可看作降级的特殊情况。

    服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。


Ribbon负载均衡

Ribbon是NetfIix发布的开源项目,主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项如连接超时,重试等。简单的说,就是在配置文件中列出LoadBalancer(简称LB)后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。

LB负载均衡(LoadBalancer)

LB就是将用户的请求平摊的分配到多个服务上,从而达到系统的HA(高可用)。

常见的负载均衡有软件Nginx,LVS,硬件F5等。

Ribbon本地负载均衡客户端和LVS、Nginx服务端负载均衡区别

  • Nginx是服务器负载均衡(集中式LB),客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的。
  • Ribbon是本地负载均衡(进程内LB),在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术。

集中式LB:即在服务的消费方和提供方之间使用独立的LB设施(可以是硬件,如F5,也可以是软件,如nginx),由该设施负责把访问请求通过某种策略转发至服务的提供方。

进程内LB:将LB逻辑集成到消费方,消费方从服务注册中心获知有哪些地址可用,然后自己再从这些地址中选择出一个合适的服务器!Ribbon就属于进程内LB,它只是一个类库,集成于消费方进程,消费方通过它来获取到服务提供方的地址。

Ribbon负载均衡规则类型

  • RoundRobinRule:轮询
  • RandomRule:随机
  • RetryRuIe:先按照RoundRobinRule的策略获取服务,如果获取服务失败则在指定时间内会进行重试,获取可用的服务
  • WeightedResponseTimeRule:对RoundRobinRule的扩展,响应速度越快的实例选择权重越大,越容易被选择
  • BestAvailableRule:会先过滤掉由于多次访问故障而处于断路器跳闸状态的服务,然后选择一个并发量最小的服务
  • AvailabilityFilteringRule:先过滤掉故障实例,再选择并发较小的实例
  • ZoneAvoidanceRule:默认规则,复合判断server所在区域的性能和server的可用性选择服务器

Feign服务调用

Feign是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端。Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务


Gateway服务网关

Gateway原理

Gateway三大核心概念

  • Route(路由):路由是构建网关的基本模块,它由ID、目标URI、一系列的断言和过滤器组成,如果断言为true则匹配该路由。

  • Predicate(断言):参考的是JDK8的java.util.function.predicate。开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由。

  • Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

    例子:通过断言虽然进来了,但老师要罚站10min(过滤器操作),然后才能正常坐下听课。


Nacos服务注册和发现、服务配置中心


__EOF__

本文作者Lht1
本文链接https://www.cnblogs.com/yghr/p/15893675.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   yghr  阅读(226)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
历史上的今天:
2021-02-14 InnoDB存储引擎学习笔记
点击右上角即可分享
微信分享提示