去年下半年的主要工作是在购物车与结算两个模块,完成了从magento向微服务迁移,重新实现了票据,加价购,促销规则,限时特价,优惠券(红包),积分,余额,预售,倒计时等一系列功能。新加入了优惠规则分块与提示等功能。API完全兼容老版本APP与站点。目前购物车所有操作的处理时间平均在70ms,并且非常稳定。

购物车截图

购物车截图

结算页截图

结算页截图 结算页截图

基于以上的工作,想把自己开发与设计思路分享出来。

最初的想法(两份数据)

刚刚拿到任务,过需求时就已经有最初的思路。将购物车数据分成两部分:用户操作数据(计算流中为原始数据),用户可见数据(计算流中为结果数据)。

举个例子:

用户选了一个苹果,苹果正在买一赠一的促销。

用户操作数据为 苹果X1

用户可见数据为 苹果X1 苹果[赠品]X1

从“用户操作数据”到“用户可见数据”经过“买一赠一”这个促销规则的计算

如果用户不想要苹果了,删除操作只作用于原始数据,而结果数据是经过所有规则重新计算的结果。

又如果“买一赠一”的促销下线了,用户下次看到的数据将没有赠品,而“原始数据”依然存在

  • 用户的操作只作用于“原始数据”(用户->原始数据)。
  • “结果数据”是通过“原始数据”经过所有规则重新计算的结果(原始数据&规则->结果数据)。

数据存储

根据主流程的设想,原始数据需要进行存储。而结果数据只是计算产物暂时不需要存储(其实也要存储,用于数据对比发现是否变化,当规则的变化或时间的推移导致结果发生变化时需要给用户准确的提示,比如商品之前有货,现在缺货,需要提示,后面具体描述)。

用什么进行存储。我选择用redis的string结构直接存json化字符串。有以下几个好处:

  • 快,key为customerID(登录前)或deviceID(登录后),value为一个大字符串。读和写只要进行一次io即可。如果用rdbms意味着要建至少两张表,一张是购物车本身信息,一张是明细数据,读写过程非常复杂。
  • 灵活,业务会不断变化,购物车原始数据结构也会变化,这种变化在encoding时自动代入到了json化字符串中。

(为了方便数据部门的分析,我们把json数据的所有变化过程都放进了文件系统中,用脚本定期进行清理,数据不断进入数据仓库)

API设计

  • 更新类,如添加商品,勾选商品,删除商品,选择加票据兑换,选择加价购,选择优惠券,更改收货地址,更改积分量,更改余额量,更改支付方式等等。但无论如何,所有的更新类方法都由一个核心方法处理,步骤【读出原始数据->改变原始数据->进行计算->存储新的原始数据与结果数据->返回计算结果】。
  • 读取类,如加载购物车列表,加载购物车数量,加载优惠券列表(可用,不可用原因),结算页商品与促销明细等等。但无论如何,所有的读取类方法都由一个核心方法处理,步骤【读出原始数据->进行计算->存储新的原始数据与结果数据->返回计算结果】。
  • 数据变更检查,这里需要使用上一次计算出的结果数据,与本次计算的结果数据进行对比,结果可以为:商品缺货、下架、优惠券过期、余额不足、赠品失效、优惠券使用后导致订单金额不满足促销条件、赠品已抢光等等。步骤【读出原始数据->进行计算->读取上一次结果数据->本次与上次结果数据对比->返回计算结果】
  • 非登录状态购物车向登录态合并,步骤【读出非登录状态与登录状态原始数据->合并到登录状态原始数据->进行计算->存储新的原始数据与结果数据->返回计算结果】。
  • 生成订单。步骤【读出原始数据->进行计算->生成订单与核销用户资产->改变原始数据->存储新的原始数据与结果数据->返回订单号】(这里核销用户资产指的是消耗用户的优惠券、票据、积分、余额等,如果有订单取消,会进行退还,取消订单不会对购物车数据产生影响所以这里不继续分享)。

API设计的核心目标就是一方面将流程与数据结构设计的更清晰,一方面又要让API交互性丰富,方便使用。

核心API数据结构示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
{
    "SubtotalAmount": 59.9,
    "PackageAmount": 0,
    "ShippingAmount": 25,
    "PromotionDeduct": 0,
    "BalanceDeduct": 0,
    "PointDeduct": 0,
    "PointSelected": 0,
    "PayDeduct": 84.9,
    "TotalQty": 1,
    "InCartTotalQty": 1,
    "ShippingFreeTotal": 125,
    "ExchangeDiscount": 0,
    "ExchangeInfo": "满225加6.9元得2斤冰糖橙,还差225",
    "FreeGiftInfo": {
        "freeGiftNotice": null
    },
    "Promotions": null,
    "PromotionBlocks": [
        {
            "ID": 2768,
            "Type": "block_discount",
            "IconText": "满减",
            "Text": "满118减40,再买118元即享40元优惠",
            "ActionText": "去凑单",
            "ActionURL": "https://www.freshfresh.com/ffweb/npmj0222",
            "ProductIds": "572,4302",
            "GiftEnable": false,
            "GiftProducts": null
        }
    ],
    "Lines": [
        {
            "ProductID": 4302,
            "PresaleTime": "",
            "PresaleUrl": "",
            "NotifyStockQty": 0,
            "StockQty": 534,
            "IsDisable": false,
            "IsOutStock": false,
            "Image": "http://qiniu.freshfresh.com/a_fetch_4302_655c48a2376efcaff5ddf6b1b4bb9cef.jpg",
            "Name": "[限购一件]泰国香水椰青 整箱/9个装(赠开椰器)",
            "Sku": "FF800003",
            "Standard": "单果\u003e700g",
            "SalePrice": 59.9,
            "WeighingPrice": "",
            "ReferencePrice": 81,
            "LimitCount": 1,
            "LimitMinBuyQty": 1,
            "Active": true,
            "LineType": 1,
            "CreateDate": "2018-02-22 23:31:41",
            "IsInCart": true,
            "Qty": 1,
            "Price": 59.9,
            "ExtInfo": "",
            "GiftThreadID": 0,
            "TempExtStringInfo": "",
            "TempExtFloatInfo": 0,
            "IsOutOfDeliveryRange": false,
            "PromotionIconText": "",
            "PromotionText": ""
        }
    ],
    "HasPackage": false,
    "IsNew": false,
    "CustomerID": 708041,
    "Amounts": {
        "NormalSubtotalAmount": 0,
        "ExchangeSubtotalAmount": 0,
        "GiftCodeSubtotalAmount": 0
    }
}

通过上面的结构进行计算,提供给APP的API数据结构示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
 {
    "SubtotalAmount": "59.9",
    "PromotionDeduct": 0,
    "DiscountAmount": 0,
    "ShippingAmount": 25,
    "InCartTotalQty": 1,
    "TotalQty": 1,
    "Promotions": [],
    "IsAllInCart": false,
    "Blocks": [
        {
            "Type": "block_discount",
            "IconText": "满减",
            "Text": "满118减40,再买118元即享40元优惠",
            "ActionText": "去凑单",
            "ActionURL": "https://www.freshfresh.com/ffweb/npmj0222",
            "GiftEnable": false,
            "GiftProducts": [],
            "Lines": [
                {
                    "LineID": "10_4302",
                    "ProductID": 4302,
                    "Image": "http://qiniu.freshfresh.com/a_fetch_4302_655c48a2376efcaff5ddf6b1b4bb9cef.jpg",
                    "Name": "[限购一件]泰国香水椰青 整箱/9个装(赠开椰器)",
                    "Standard": "单果\u003e700g",
                    "Price": "59.9",
                    "IsInCart": true,
                    "Qty": 1,
                    "PresaleTime": "",
                    "IconText": "",
                    "HasLeftSeconds": false,
                    "LeftSeconds": 0,
                    "HasQtyBtns": true,
                    "IsCanAdd": false,
                    "Text": "",
                    "PromotionIconText": "",
                    "PromotionText": "",
                    "GiftItems": []
                }
            ]
        }
    ]
}

API字段对应图例

API字段对应图例

使用者只需要将字段对号入座就可以完美实现需求,即使需求方要调整逻辑,也不用重新发布客户端,只要API逻辑稍做调整即可。

规则计算

规则数据结构

  • 类型 用于指定条件和结果的类型
  • 条件 指定商品数量,指定多个商品数量,购物车金额,是否需要红包
  • 结果 赠品,比例减,减去指定金额,减到指定金额

通过以上的结构可以玩出的花样

  • 满减
  • 满折
  • 满赠
  • 买一赠一
  • 买就赠
  • 每买赠
  • 每满赠
  • x元n件
  • 买减
  • 买折
  • 第二件半价
  • 分类商品满减
  • 第n件折
  • 第n件减 …

计算过程

  • 规则加载到内存
  • 通过监控数据库来更新规则
  • 有效期内规则过滤
  • 规则条件与购物车内容匹配计算
  • 符合要求的规则结果计算

红包列表与不可用原因的计算:每个红包都与一个规则绑定,红包所有的规则条件与购物车内容匹配找到可用红包和不可用红包原因

红包使用页截图

红包使用页截图