├── .nojekyll ├── .vscode └── settings.json ├── CNAME ├── README.md ├── _sidebar.md ├── guide.md ├── index.html ├── material ├── AStraw │ ├── market │ │ ├── README.md │ │ └── red-envelop.md │ └── shop │ │ ├── ER-shop.png │ │ ├── README.md │ │ ├── question-sql-180826.txt │ │ └── skr-shop.sql ├── Dayu │ ├── 20180901.md │ ├── 20181116-pay.sql │ ├── cart │ │ ├── cart-design.md │ │ ├── cart-requirement.md │ │ ├── imgs │ │ │ ├── activity-product.png │ │ │ ├── product-activity.png │ │ │ ├── product-relations.png │ │ │ ├── promotions.png │ │ │ ├── user-cart-c.png │ │ │ └── user-cart-s.png │ │ └── 购物车.xmind │ └── img │ │ └── bytedance.jpeg ├── LIYUNFAN │ └── monkey-play │ │ ├── assert │ │ └── form-1.png │ │ └── form.md ├── TIGERB │ ├── golden-flash.md │ ├── marketing │ │ ├── coupon-refactor.md │ │ ├── coupon.md │ │ ├── lottery-demand.md │ │ ├── lottery-sys.md │ │ ├── marketing.md │ │ ├── seckill-business.md │ │ ├── seckill-ppt.md │ │ └── seckill.md │ ├── order │ │ ├── checkout.md │ │ ├── order-status.md │ │ ├── pay.md │ │ └── submit.md │ ├── product.md │ └── search │ │ └── search.md ├── Veaer │ └── markering │ │ └── coupon-v3.md └── skr-shop.png ├── records ├── record-20180810.md ├── record-20180817.md └── record-20180830.md └── src ├── account └── README.md ├── aftersale └── README.md ├── base ├── README.md └── search │ ├── business.md │ └── tech.md ├── express └── README.md ├── order ├── README.md └── checkout.md ├── promotion ├── README.md ├── coupon.md ├── glue.md └── seckill.md ├── shopping ├── cart.md └── product.md ├── trade └── README.md └── warehouse └── README.md /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/.nojekyll -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | skrshop.tech 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

《电商设计手册 | SkrShop》

2 | 3 |

Do design No code | 只设计不码码

4 | 5 |

6 | Lisense 7 |

8 | 9 |

skrshop.tech

10 | 11 |

12 | 13 |

14 | 15 | # 版权声明 16 | - 未经版权所有者明确授权,禁止发行本手册及其被实质上修改的版本(形式包含文章、视频、播客等任意媒体等)。 17 | - 未经版权所有者事先授权,禁止将此作品及其衍生作品以标准(纸质)书籍形式发行。 18 | - 未与任何第三方以任何形式合作。 19 | 20 |

21 | 22 | 23 | 24 |

25 | 26 | # 大厂内推 27 | 28 | 为大家提供各个大厂渠道的内推。 29 | 30 | ## 抖音电商 31 | 32 | 1. 超高速发展的业务;意味着:技术挑战大、发挥空间大、钱多! 33 | 2. 业界最新技术;微服务、ServiceMesh、Faas 都已经有落地方案,只要想,内部资料随时学; 34 | 3. 技术氛围好;每周都有技术团队分享,随时约会交流,没有部门壁垒; 35 | 36 | 欢迎将简历投递至我的邮箱:**dayugog@gmail.com** 37 |
38 | 不限年限,只要你敢投,我就敢帮你推,快上车~ 39 | 40 | ## 小米海外电商 41 | 42 | - 简介:小米网海外电商平台 43 | - 技术栈:Go 44 | - 挑战,期待和你一起来解决: 45 | + 全球化翻译问题 46 | + 全球多机房问题 47 | + 海外快速复制电商系统平台问题 48 | + 反黄牛 49 | 50 | 如果你对这些挑战很感兴趣,欢迎将简历投递至我的邮箱:**tigerbcode@gmail.com** 51 |
52 | 不限年限,只要你敢投,我就敢帮你推,快上车~ 53 | 54 | # Star趋势 55 | 56 | [![Stargazers over time](https://starchart.cc/skr-shop/manuals.svg)](https://starchart.cc/skr-shop/manuals) 57 | 58 | # 架构 59 | 60 |

61 | 62 | 63 | 64 |

65 | 66 | # 目录 67 | 68 | - [前言](http://skrshop.tech/#/) 69 | - [目录](http://skrshop.tech/#/guide) 70 | - [技术栈选型](http://skrshop.tech/#/?id=技术栈选型) 71 | - [代码仓库](http://skrshop.tech/#/?id=代码仓库) 72 | - [用户体系](/src/account/?id=用户体系) 73 | + [账户服务](/src/account/?id=架构设计) 74 | + [权限服务](/src/account/?id=后台权限管理) 75 | - [交易体系](/src/shopping/cart?id=交易体系) 76 | + [购物车服务](/src/shopping/cart?id=购物车服务) 77 | + [购物车架构](/src/shopping/cart?id=购物车架构) 78 | + [订单模块](/src/order/) 79 | * [订单结算页](/src/order/checkout) 80 | - [营销体系](/src/promotion/?id=营销体系) 81 | + 活动体系 82 | * [通用抽奖工具(Glue万能胶)](/src/promotion/glue) 83 | + 营销体系 84 | * [秒杀服务](/src/promotion/seckill?id=秒杀服务) 85 | * [优惠券服务](/src/promotion/coupon?id=优惠券服务) 86 | * 积分服务 87 | - [基础服务](/src/base/) 88 | + [商品体系(Temporal万物)](/src/shopping/product?id=商品系统) 89 | + [支付体系](/src/trade/) 90 | * [常见第三方支付流程](/src/trade/?id=常见第三方支付流程) 91 | * [支付系统设计](/src/trade/?id=支付系统设计) 92 | * 收银台 93 | + 搜索服务 94 | * [电商搜索业务介绍](/src/base/search/business) 95 | * [由浅到深,入门搜索原理](/src/base/search/tech) 96 | + 接口静态化服务 97 | + 上传服务 98 | + 消息服务 99 | * 短信 100 | * 邮件 101 | * 微信模板消息 102 | * 站内信 103 | - [仓储系统](http://skrshop.tech/#/src/warehouse/) 104 | + 地址服务 105 | - [物流系统](http://skrshop.tech/#/src/express/) 106 | - [售后服务](http://skrshop.tech/#/src/aftersale/) 107 | 108 | # 前言 109 | 110 | 一直从事互联网电商开发三年多的时间了,回头想想却对整个业务流程不是很了解,说出去很是惭愧。但是身处互联网电商的环境中,或多或少接触了其中的各个业务,其次周边还有很多从事电商的同事和朋友,这都是资源。于是,我决定和我的同事、盆友们、甚至还有你们去梳理整个流程并分享出来,谈不上结果要做的多么好,至少在每一个我们有能力去做好的地方,一定会细致入微。 111 | 112 | 除此之外,同时为了满足我们自身在工作中可能得不到的技术满足感,我们在做整个系统设计的过程中,会去使用我们最想用的技术栈。技术栈这一点我们借助docker去实现,所以最终的结果:一方面我们掌握了业务的东西,另一方面又得到了技术上的满足感,二者兼得。 113 | 114 | 最后,出于时间的考虑,我们提出了一个想法**Do design No code**。**【只设计不码码】** 这句话的意思:最终我们设计出来整个系统的数据模型,接口文档,甚至交互过程,以及环境部署等,但是最后我们却不写代码。是吧?如果这样了写代码还有什么意义。当然,也不全是这样,出于时间的考虑当然也会用代码实现出来的,说不定最后正是对面的你去实现的。 115 | 116 | 其次,这些内容肯定有考虑不全面或者在上规模的业务中存在更复杂的地方,欢迎指出,我们也希望学习和分享您的经验。 117 | 118 | # 技术栈选型 119 | 120 | ``` 121 | - 基础环境 122 | + k8s 123 | + docker 124 | - 存储 125 | + mysql 126 | + redis 127 | * codis 128 | * redis主从 129 | - queue 130 | + kafka 131 | + rocketmq 132 | + rabbitmq 133 | - gw 134 | + kong 135 | + zuul 136 | - webserver 137 | + nginx/openresty 138 | + envoy 139 | - server 140 | + go 141 | + php 142 | - frontend 143 | + vue 144 | - rpc 145 | + grpc 146 | + thrift 147 | - 基础能力 148 | + 监控 149 | * zipkin 150 | * elk 151 | * falcon 152 | + 服务发现 153 | * zookeeper 154 | * etcd 155 | + 持续集成 156 | * ci/cd 157 | - 搜索 158 | + es 159 | + solr 160 | ``` 161 | 162 | # 代码仓库 163 | 164 | 请您耐心等待... 165 | 166 | # Skr Shop项目成员简介 167 | 168 | 排名不分先后,字典序 169 | 170 | 昵称|简介|个人博客 171 | --------|--------|-------- 172 | AStraw|研究生创业者|公众号“稻草人生” 173 | 大愚Dayu|国内大多人使用的PHP第三方支付聚合项目[Payment](https://github.com/helei112g/payment)作者,创过业|[大愚Talk](http://dayutalk.cn/) 174 | lwhcv|曾就职于百度/融360|-------- 175 | TIGERB|PHP框架[EasyPHP](http://easy-php.tigerb.cn/#/)作者| [TIGERB的技术博客](http://tigerb.cn) 176 | Veaer|宇宙无敌风火轮全干工程师| [Veaer](http://veaer.com) 177 | -------------------------------------------------------------------------------- /_sidebar.md: -------------------------------------------------------------------------------- 1 | - [前言](/) 2 | - [目录](/guide) 3 | - [技术栈选型](/?id=技术栈选型) 4 | - [代码仓库](/?id=代码仓库) 5 | - [用户体系](/src/account/?id=用户体系) 6 | + [账户服务](/src/account/?id=架构设计) 7 | + [权限服务](/src/account/?id=后台权限管理) 8 | - [交易体系](/src/shopping/cart?id=交易体系) 9 | + [购物车服务](/src/shopping/cart?id=购物车服务) 10 | + [购物车架构](/src/shopping/cart?id=购物车架构) 11 | + [订单模块](/src/order/) 12 | * [订单结算页](/src/order/checkout) 13 | - [营销体系](/src/promotion/?id=营销体系) 14 | + 活动体系 15 | * [通用抽奖工具(Glue万能胶)](/src/promotion/glue) 16 | + 营销体系 17 | * [秒杀服务](/src/promotion/seckill?id=秒杀服务) 18 | * [优惠券服务](/src/promotion/coupon?id=优惠券服务) 19 | * 积分服务 20 | - [基础服务](/src/base/) 21 | + [商品体系(Temporal万物)](/src/shopping/product?id=商品系统) 22 | + [支付体系](/src/trade/) 23 | * [常见第三方支付流程](/src/trade/?id=常见第三方支付流程) 24 | * [支付系统设计](/src/trade/?id=支付系统设计) 25 | * 收银台 26 | + 搜索服务 27 | * [电商搜索业务介绍](/src/base/search/business) 28 | * [由浅到深,入门搜索原理](/src/base/search/tech) 29 | + 接口静态化服务 30 | + 上传服务 31 | + 消息服务 32 | * 短信 33 | * 邮件 34 | * 微信模板消息 35 | * 站内信 36 | - [仓储系统](/src/warehouse/) 37 | + 地址服务 38 | - [物流系统](/src/express/) 39 | - [售后服务](/src/aftersale/) 40 | 41 | -------------------------------------------------------------------------------- /guide.md: -------------------------------------------------------------------------------- 1 | - [前言](/) 2 | - [目录](/guide) 3 | - [技术栈选型](/?id=技术栈选型) 4 | - [代码仓库](/?id=代码仓库) 5 | - [用户体系](/src/account/?id=用户体系) 6 | + [账户服务](/src/account/?id=架构设计) 7 | + [权限服务](/src/account/?id=后台权限管理) 8 | - [购物体系](/src/shopping/cart?id=购物体系) 9 | + [商品系统(Temporal万物)](/src/shopping/product?id=商品系统) 10 | + [购物车服务](/src/shopping/cart?id=购物车服务) 11 | + [购物车架构](/src/shopping/cart?id=购物车架构) 12 | - [营销体系](/src/promotion/) 13 | + 活动营销系统 14 | * [通用抽奖工具(Glue万能胶)](/src/promotion/glue) 15 | + 销售营销系统 16 | + 基础服务 17 | * [秒杀服务](/src/promotion/seckill) 18 | * [优惠券服务](/src/promotion/coupon) 19 | * 积分服务 20 | - [交易中心](/src/trade/) 21 | + [常见第三方支付流程](/src/trade/?id=常见第三方支付流程) 22 | + [支付系统设计](/src/trade/?id=支付系统设计) 23 | + 收银台 24 | - [订单中心](/src/order/) 25 | + [订单结算页](/src/order/checkout) 26 | - [仓储系统](/src/warehouse/) 27 | + 地址服务 28 | - [物流系统](/src/express/) 29 | - [售后服务](/src/aftersale/) 30 | - [基础服务](/src/base/) 31 | + 搜索服务 32 | * [电商搜索业务介绍](/src/base/search/business) 33 | * [由浅到深,入门搜索原理](/src/base/search/tech) 34 | + 接口静态化服务 35 | + 上传服务 36 | + 消息服务 37 | * 短信 38 | * 邮件 39 | * 微信模板消息 40 | * 站内信 -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 电商设计手册 | SkrShop 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 20 | 26 | 27 | 28 |
29 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /material/AStraw/market/README.md: -------------------------------------------------------------------------------- 1 | ### skr-shop 之 抢红包方案设计 2 | 3 | 方案:多种随机实现方案 4 | 公平性:比较先抢、后抢的公平性 5 | 性能:先计算还是抢的时候再计算?多方案性能比较 6 | 7 |
8 | 9 | > 更多更新,敬请期待... 10 | -------------------------------------------------------------------------------- /material/AStraw/market/red-envelop.md: -------------------------------------------------------------------------------- 1 | ## 微信红包的架构设计 & Demo 2 | 3 | ### 1. 微信的金额什么时候算? 4 | 5 | 答:微信金额是拆的时候实时算出来,不是预先分配的,采用的是纯内存计算,不需要预算空间存储。采取实时计算金额的考虑:预算需要占存储,实时效率很高,预算才效率低。 6 | 7 | ### 2. 实时性:为什么明明抢到红包,点开后发现没有? 8 | 9 | 答:2014 年的红包一点开就知道金额,分两次操作,先抢到金额,然后再转账。 10 | 2015 年的红包的拆和抢是分离的,需要点两次,因此会出现抢到红包了,但点开后告知红包已经被领完的状况。进入到第一个页面不代表抢到,只表示当时红包还有。 11 | 12 | ### 3. 分配:红包里的金额怎么算?为什么出现各个红包金额相差很大? 13 | 14 | 答:随机,额度在 0.01 和剩余平均值 *2 之间。 15 | 例如:发 100 块钱,总共 10 个红包,那么平均值是 10 块钱一个,那么发出来的红包的额度在 0.01元~20元 之间波动。 16 | 当前面3个红包总共被领了 40 块钱时,剩下 60 块钱,总共 7 个红包,那么这 7 个红包的额度在:0.01~(60/7\*2)=17.14 之间。 17 | 注意:这里的算法是每被抢一个后,剩下的会再次执行上面的这样的算法。 18 | 19 | 这样算下去,会超过最开始的全部金额,因此到了最后面如果不够这么算,那么会采取如下算法:保证剩余用户能拿到最低 1 分钱即可。 20 | 21 | 如果前面的人手气不好,那么后面的余额越多,红包额度也就越多,因此实际概率一样的。 22 | 23 | > `Demo` 24 | ```go 25 | package main 26 | 27 | import ( 28 | "fmt" 29 | "math/rand" 30 | "time" 31 | ) 32 | 33 | // 最少1分钱 34 | const MinAmount = int64(1) 35 | 36 | func main() { 37 | // 总金额100元(转为最小分单位),发10个红包 38 | count, amount := int64(10), int64(10000) 39 | var res []float64 40 | 41 | remain := amount 42 | for i := int64(0); i < count; i++ { 43 | x := CalculateEnvelop(count-i, remain) 44 | remain -= x 45 | res = append(res, float64(x)/100) 46 | } 47 | 48 | fmt.Println(res) 49 | } 50 | 51 | // CalculateEnvelop 计算随机红包序列 52 | // count - 红包数量 53 | // amount - 总金额(单位:分) 54 | func CalculateEnvelop(count, amount int64) int64 { 55 | if count == 1 { 56 | return amount 57 | } 58 | 59 | // 2倍均值范围,避免分散过大 60 | maxAvailable := ((amount - count*MinAmount) / count) * 2 61 | //fmt.Println(maxAvailable) 62 | 63 | rand.Seed(time.Now().UnixNano()) 64 | x := rand.Int63n(maxAvailable) + MinAmount 65 | 66 | return x 67 | } 68 | 69 | ``` 70 | 71 | > 运行结果: 72 | [18.63 4.3 4.37 0.89 19.92 11.11 7.33 7.66 20.06 5.73] 73 | [15.58 5.94 11.12 14.19 12.39 6.02 10.71 14.95 1.96 7.14] 74 | [7.98 10.51 13.59 2.47 6.97 6.78 24.2 2.22 2.27 23.01] 75 | [6.27 6.71 15.48 3.24 21.55 9.69 11.77 5.79 3.05 16.45] 76 | [5.98 13.95 9.98 17.06 3.8 0.33 2.06 19.64 26.4 0.8] 77 | 78 |
79 | 80 | > 方案优化?(之后将带来多种优化方案) 81 | 比较先抢、后抢的公平性; 82 | 性能测试比较; 83 | 先计算红包序列,还是抢的时候再计算? 84 | 85 | 86 | ### 4. 红包的设计 87 | 88 | 答:微信从财付通拉取金额数据过来,生成个数/红包类型/金额放到 `redis` 集群里,`app` 端将红包 `ID` 的请求放入请求队列中,如果发现超过红包的个数,直接返回。根据红包的裸祭处理成功得到令牌请求,则由财付通进行一致性调用,通过像比特币一样,两边保存交易记录,交易后交给第三方服务审计,如果交易过程中出现不一致就强制回归。 89 | 90 | ### 5. 并发性处理:红包如何计算被抢完? 91 | 92 | 答:`cache` 会抵抗无效请求,将无效的请求过滤掉,实际进入到后台的量不大。`cache` 记录红包个数,原子操作进行个数递减,到 0 表示被抢光。财付通按照 `20万` 笔每秒入账准备,但实际还不到 `8万` 每秒。 93 | 94 | ### 6. 通如何保持 `8w` 每秒的写入? 95 | 96 | 答:多主 `sharding`,水平扩展机器。 97 | 98 | ### 7. 数据容量多少? 99 | 100 | 答:一个红包只占一条记录,有效期只有几天,因此不需要太多空间。 101 | 102 | ### 8. 轮询红包分配,压力大不? 103 | 104 | 答:抢到红包的人数和红包都在一条 `cache` 记录上,没有太大的查询压力。 105 | 106 | ### 9. 一个红包一个队列? 107 | 108 | 答:没有队列,一个红包一条数据,数据上有一个计数器字段。 109 | 110 | ### 10. 有没有从数据上证明每个红包的概率是不是均等? 111 | 112 | 答:不是绝对均等,就是一个简单的拍脑袋算法。 113 | 114 | ### 11. 拍脑袋算法,会不会出现两个最佳? 115 | 116 | 答:会出现金额一样的,但是手气最佳只有一个,先抢到的那个最佳。 117 | 118 | ### 12. 每领一个红包就更新数据么? 119 | 120 | 答:每抢到一个红包,就 `cas(compare and swap)` 更新剩余金额和红包个数。 121 | 122 | ### 13. 红包如何入库入账? 123 | 124 | 数据库会累加已经领取的个数与金额,插入一条领取记录。入账则是后台异步操作(调用财付通实现)。 125 | 126 | ### 14. 入帐出错怎么办?比如红包个数没了,但余额还有? 127 | 128 | 答:最后会有一个 `take all` 操作。另外还有一个对账来保障。 129 | 130 |
131 | 132 | @来源:[QCon某高可用架构群整理](https://www.zybuluo.com/yulin718/note/93148) 133 | 134 | -------------------------------------------------------------------------------- /material/AStraw/shop/ER-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/AStraw/shop/ER-shop.png -------------------------------------------------------------------------------- /material/AStraw/shop/README.md: -------------------------------------------------------------------------------- 1 | ### skr-shop 之 店铺设计 2 | 3 | 店铺管理:多店铺实现方案 4 | 购物车:多店铺商品购物车设计 5 | 订单:多店铺商品订单设计 6 | 7 |
8 | 9 | > 更多更新,敬请期待... 10 | -------------------------------------------------------------------------------- /material/AStraw/shop/question-sql-180826.txt: -------------------------------------------------------------------------------- 1 | 1.update_time是使用int,还是on update current_timestamp自动更新好? 2 | 3 | 2.相关tag是标记在shop还是在goods合适? 4 | 5 | 3.品牌应该是标记在goods维度还是shop维度? 6 | 7 | 4.店铺可能需要支付认证费用,需要加shop_pay表; 8 | 9 | 5.支持一个用户多店铺; 10 | 11 | 6.运费模板设置:店铺维度统一设置还是商品维度每个设置? -------------------------------------------------------------------------------- /material/AStraw/shop/skr-shop.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Navicat MySQL Data Transfer 3 | 4 | Source Server : phpStudy_localhost 5 | Source Server Type : MySQL 6 | Source Server Version : 50553 7 | Source Host : localhost:3306 8 | Source Schema : skr-shop 9 | 10 | Target Server Type : MySQL 11 | Target Server Version : 50553 12 | File Encoding : 65001 13 | 14 | Date: 31/08/2018 11:44:40 15 | */ 16 | 17 | SET NAMES utf8mb4; 18 | SET FOREIGN_KEY_CHECKS = 0; 19 | 20 | -- ---------------------------- 21 | -- Table structure for shop 22 | -- ---------------------------- 23 | DROP TABLE IF EXISTS `shop`; 24 | CREATE TABLE `shop` ( 25 | `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 26 | `uid` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户uid', 27 | `type` tinyint(2) UNSIGNED NOT NULL DEFAULT 0 COMMENT '店铺类型:0-个人类型,1-企业类型', 28 | `industry_ids` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '对应的行业,逗号隔开', 29 | `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '店铺名称', 30 | `logo` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '店铺Logo地址', 31 | `intro` text CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '店铺简介', 32 | `area_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '店铺最下级地址id', 33 | `area_address` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '店铺详细地址,不包含省市区的信息', 34 | `shop_notice` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '店铺留言,如休息中留言', 35 | `status` tinyint(2) UNSIGNED NOT NULL DEFAULT 0 COMMENT '店铺状态:默认0-新申请,10-审核通过,11-审核中,20-审核失败,30-营业中,40-休息中', 36 | `sort` mediumint(5) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序值', 37 | `last_verify_time` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后审核时间', 38 | `create_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '添加时间', 39 | `update_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', 40 | PRIMARY KEY (`id`) USING BTREE 41 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '店铺基本信息表' ROW_FORMAT = Dynamic; 42 | 43 | -- ---------------------------- 44 | -- Table structure for shop_certificate 45 | -- ---------------------------- 46 | DROP TABLE IF EXISTS `shop_certificate`; 47 | CREATE TABLE `shop_certificate` ( 48 | `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 49 | `shop_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '店铺id', 50 | `identity_card` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '店主身份证信息,json格式存正反面地址', 51 | `other_card` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '其他证件信息,json格式存多张图片地址', 52 | `verify_memo` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '提交审核留言', 53 | `create_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '添加时间', 54 | `update_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', 55 | PRIMARY KEY (`id`) USING BTREE 56 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '店铺相关证件信息表' ROW_FORMAT = Dynamic; 57 | 58 | -- ---------------------------- 59 | -- Table structure for shop_goods_category 60 | -- ---------------------------- 61 | DROP TABLE IF EXISTS `shop_goods_category`; 62 | CREATE TABLE `shop_goods_category` ( 63 | `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 64 | `pid` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '父id,一级分类pid为0', 65 | `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '分类名称', 66 | `icon_url` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '分类图标地址', 67 | `status` tinyint(2) UNSIGNED NOT NULL DEFAULT 0 COMMENT '有效状态:默认0-有效,1-无效', 68 | `sort` mediumint(5) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序值', 69 | `create_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '添加时间', 70 | `update_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', 71 | PRIMARY KEY (`id`) USING BTREE 72 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '店铺自定义商品分类表' ROW_FORMAT = Dynamic; 73 | 74 | -- ---------------------------- 75 | -- Table structure for shop_industry 76 | -- ---------------------------- 77 | DROP TABLE IF EXISTS `shop_industry`; 78 | CREATE TABLE `shop_industry` ( 79 | `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 80 | `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '行业名称', 81 | `intro` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '行业简介', 82 | `status` tinyint(2) UNSIGNED NOT NULL DEFAULT 0 COMMENT '有效状态:默认0-有效,1-无效', 83 | `sort` mediumint(5) UNSIGNED NOT NULL DEFAULT 0 COMMENT '排序值', 84 | `create_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '添加时间', 85 | `update_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', 86 | PRIMARY KEY (`id`) USING BTREE 87 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '店铺行业表' ROW_FORMAT = Dynamic; 88 | 89 | -- ---------------------------- 90 | -- Table structure for shop_verify 91 | -- ---------------------------- 92 | DROP TABLE IF EXISTS `shop_verify`; 93 | CREATE TABLE `shop_verify` ( 94 | `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT, 95 | `shop_id` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '店铺id', 96 | `verify_info` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '审核时对应的店铺信息,json存店铺图片/留言等', 97 | `verify_note` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '审核备注', 98 | `operator_staffid` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '审核人后台staffid', 99 | `operator_staffname` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '审核人信息', 100 | `verify_time` int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后审核时间', 101 | `create_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '添加时间', 102 | `update_at` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', 103 | PRIMARY KEY (`id`) USING BTREE 104 | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '店铺审核记录表' ROW_FORMAT = Dynamic; 105 | 106 | SET FOREIGN_KEY_CHECKS = 1; 107 | -------------------------------------------------------------------------------- /material/Dayu/20180901.md: -------------------------------------------------------------------------------- 1 | # 文章划分 2 | 3 | 1. 支付的流程介绍 4 | 2. 支付的数据结构设计、接口设计 5 | -------------------------------------------------------------------------------- /material/Dayu/20181116-pay.sql: -------------------------------------------------------------------------------- 1 | -- ----------------------------------------------------- 2 | -- Table 创建支付流水表 3 | -- ----------------------------------------------------- 4 | CREATE TABLE IF NOT EXISTS `pay_transaction` ( 5 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 6 | `app_id` VARCHAR(32) NOT NULL COMMENT '应用id', 7 | `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式id,可以用来识别支付,如:支付宝、微信、Paypal等', 8 | `app_order_id` VARCHAR(64) NOT NULL COMMENT '应用方订单号', 9 | `transaction_id` VARCHAR(64) NOT NULL COMMENT '本次交易唯一id,整个支付系统唯一,生成他的原因主要是 order_id对于其它应用来说可能重复', 10 | `total_fee` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付金额,整数方式保存', 11 | `scale` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '金额对应的小数位数', 12 | `currency_code` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '交易的币种', 13 | `pay_channel` VARCHAR(64) NOT NULL COMMENT '选择的支付渠道,比如:支付宝中的花呗、信用卡等', 14 | `expire_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '订单过期时间', 15 | `return_url` VARCHAR(255) NOT NULL COMMENT '支付后跳转url', 16 | `notify_url` VARCHAR(255) NOT NULL COMMENT '支付后,异步通知url', 17 | `email` VARCHAR(64) NOT NULL COMMENT '用户的邮箱', 18 | `sing_type` VARCHAR(10) NOT NULL DEFAULT 'RSA' COMMENT '采用的签方式:MD5 RSA RSA2 HASH-MAC等', 19 | `intput_charset` CHAR(5) NOT NULL DEFAULT 'UTF-8' COMMENT '字符集编码方式', 20 | `payment_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '第三方支付成功的时间', 21 | `notify_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '收到异步通知的时间', 22 | `finish_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '通知上游系统的时间', 23 | `trade_no` VARCHAR(64) NOT NULL COMMENT '第三方的流水号', 24 | `transaction_code` VARCHAR(64) NOT NULL COMMENT '真实给第三方的交易code,异步通知的时候更新', 25 | `order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0:等待支付,1:待付款完成, 2:完成支付,3:该笔交易已关闭,-1:支付失败', 26 | `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', 27 | `update_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', 28 | `create_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建的ip,这可能是自己服务的ip', 29 | `update_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新的ip', 30 | PRIMARY KEY (`id`), 31 | UNIQUE INDEX `uniq_tradid` (`transaction_id`), 32 | INDEX `idx_trade_no` (`trade_no`), 33 | INDEX `idx_ctime` (`create_at`)), 34 | ENGINE = InnoDB 35 | DEFAULT CHARACTER SET = utf8mb4 36 | COMMENT = '发起支付的数据'; 37 | 38 | -- ----------------------------------------------------- 39 | -- Table 交易扩展表 40 | -- ----------------------------------------------------- 41 | CREATE TABLE IF NOT EXISTS `pay_transaction_extension` ( 42 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 43 | `transaction_id` VARCHAR(64) NOT NULL COMMENT '系统唯一交易id', 44 | `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0, 45 | `transaction_code` VARCHAR(64) NOT NULL COMMENT '生成传输给第三方的订单号', 46 | `call_num` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '发起调用的次数', 47 | `extension_data` TEXT NOT NULL COMMENT '扩展内容,需要保存:transaction_code 与 trade no 的映射关系,异步通知的时候填充', 48 | `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', 49 | `create_ip` INT UNSIGNED NOT NULL COMMENT '创建ip', 50 | PRIMARY KEY (`id`), 51 | INDEX `idx_trads` (`transaction_id`, `pay_status`), 52 | UNIQUE INDEX `uniq_code` (`transaction_code`)), 53 | ENGINE = InnoDB 54 | DEFAULT CHARACTER SET = utf8mb4 55 | COMMENT = '交易扩展表'; 56 | 57 | -- ----------------------------------------------------- 58 | -- Table 交易系统全部日志 59 | -- ----------------------------------------------------- 60 | CREATE TABLE IF NOT EXISTS `pay_log_data` ( 61 | `id` BIGINT UNSIGNED NOT NULL, 62 | `app_id` VARCHAR(32) NOT NULL COMMENT '应用id', 63 | `app_order_id` VARCHAR(64) NOT NULL COMMENT '应用方订单号', 64 | `transaction_id` VARCHAR(64) NOT NULL COMMENT '本次交易唯一id,整个支付系统唯一,生成他的原因主要是 order_id对于其它应用来说可能重复', 65 | `request_header` TEXT NOT NULL COMMENT '请求的header 头', 66 | `request_params` TEXT NOT NULL COMMENT '支付的请求参数', 67 | `log_type` VARCHAR(10) NOT NULL COMMENT '日志类型,payment:支付; refund:退款; notify:异步通知; return:同步通知; query:查询', 68 | `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', 69 | `create_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建ip', 70 | PRIMARY KEY (`id`), 71 | INDEX `idx_tradt` (`transaction_id`, `log_type`)), 72 | ENGINE = InnoDB 73 | DEFAULT CHARACTER SET = utf8mb4 74 | COMMENT = '交易日志表'; 75 | 76 | 77 | -- ----------------------------------------------------- 78 | -- Table 重复支付的交易 79 | -- ----------------------------------------------------- 80 | CREATE TABLE IF NOT EXISTS `pay_repeat_transaction` ( 81 | `id` BIGINT UNSIGNED NOT NULL, 82 | `app_id` VARCHAR(32) NOT NULL COMMENT '应用的id', 83 | `transaction_id` VARCHAR(64) NOT NULL COMMENT '系统唯一识别交易号', 84 | `transaction_code` VARCHAR(64) NOT NULL COMMENT '支付成功时,该笔交易的 code', 85 | `trade_no` VARCHAR(64) NOT NULL COMMENT '第三方对应的交易号', 86 | `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式', 87 | `total_fee` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '交易金额', 88 | `scale` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '小数位数', 89 | `currency_code` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '支付选择的币种,CNY、HKD、USD等', 90 | `payment_time` INT NOT NULL COMMENT '第三方交易时间', 91 | `repeat_type` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '重复类型:1同渠道支付、2不同渠道支付', 92 | `repeat_status` TINYINT UNSIGNED DEFAULT 0 COMMENT '处理状态,0:未处理;1:已处理', 93 | `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', 94 | `update_at` INT UNSIGNED NOT NULL COMMENT '更新时间', 95 | PRIMARY KEY (`id`), 96 | INDEX `idx_trad` ( `transaction_id`), 97 | INDEX `idx_method` (`pay_method_id`), 98 | INDEX `idx_time` (`create_at`)), 99 | ENGINE = InnoDB 100 | DEFAULT CHARACTER SET = utf8mb4 101 | COMMENT = '记录重复支付'; 102 | 103 | 104 | -- ----------------------------------------------------- 105 | -- Table 通知上游应用日志 106 | -- ----------------------------------------------------- 107 | CREATE TABLE IF NOT EXISTS `pay_notify_app_log` ( 108 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 109 | `app_id` VARCHAR(32) NOT NULL COMMENT '应用id', 110 | `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式', 111 | `transaction_id` VARCHAR(64) NOT NULL COMMENT '交易号', 112 | `transaction_code` VARCHAR(64) NOT NULL COMMENT '支付成功时,该笔交易的 code', 113 | `sign_type` VARCHAR(10) NOT NULL DEFAULT 'RSA' COMMENT '采用的签名方式:MD5 RSA RSA2 HASH-MAC等', 114 | `input_charset` CHAR(5) NOT NULL DEFAULT 'UTF-8', 115 | `total_fee` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '涉及的金额,无小数', 116 | `scale` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '小数位数', 117 | `pay_channel` VARCHAR(64) NOT NULL COMMENT '支付渠道', 118 | `trade_no` VARCHAR(64) NOT NULL COMMENT '第三方交易号', 119 | `payment_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付时间', 120 | `notify_type` VARCHAR(10) NOT NULL DEFAULT 'paid' COMMENT '通知类型,paid/refund/canceled', 121 | `notify_status` VARCHAR(7) NOT NULL DEFAULT 'INIT' COMMENT '通知支付调用方结果;INIT:初始化,PENDING: 进行中; SUCCESS:成功; FAILED:失败', 122 | `create_at` INT UNSIGNED NOT NULL DEFAULT 0, 123 | `update_at` INT UNSIGNED NOT NULL DEFAULT 0, 124 | PRIMARY KEY (`id`), 125 | INDEX `idx_trad` (`transaction_id`), 126 | INDEX `idx_app` (`app_id`, `notify_status`) 127 | INDEX `idx_time` (`create_at`)), 128 | ENGINE = InnoDB 129 | DEFAULT CHARACTER SET = utf8mb4 130 | COMMENT = '支付调用方记录'; 131 | 132 | 133 | -- ----------------------------------------------------- 134 | -- Table 退款 135 | -- ----------------------------------------------------- 136 | CREATE TABLE IF NOT EXISTS `pay_refund` ( 137 | `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, 138 | `app_id` VARCHAR(64) NOT NULL COMMENT '应用id', 139 | `app_refund_no` VARCHAR(64) NOT NULL COMMENT '上游的退款id', 140 | `transaction_id` VARCHAR(64) NOT NULL COMMENT '交易号', 141 | `trade_no` VARCHAR(64) NOT NULL COMMENT '第三方交易号', 142 | `refund_no` VARCHAR(64) NOT NULL COMMENT '支付平台生成的唯一退款单号', 143 | `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式', 144 | `pay_channel` VARCHAR(64) NOT NULL COMMENT '选择的支付渠道,比如:支付宝中的花呗、信用卡等', 145 | `refund_fee` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '退款金额', 146 | `scale` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '小数位数', 147 | `refund_reason` VARCHAR(128) NOT NULL COMMENT '退款理由', 148 | `currency_code` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '币种,CNY USD HKD', 149 | `refund_type` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '退款类型;0:业务退款; 1:重复退款', 150 | `refund_method` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '退款方式:1自动原路返回; 2人工打款', 151 | `refund_status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0未退款; 1退款处理中; 2退款成功; 3退款不成功', 152 | `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建时间', 153 | `update_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新时间', 154 | `create_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '请求源ip', 155 | `update_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '请求源ip', 156 | PRIMARY KEY (`id`), 157 | UNIQUE INDEX `uniq_refno` (`refund_no`), 158 | INDEX `idx_trad` (`transaction_id`), 159 | INDEX `idx_status` (`refund_status`), 160 | INDEX `idx_ctime` (`create_at`)), 161 | ENGINE = InnoDB 162 | DEFAULT CHARACTER SET = utf8mb4 163 | COMMENT = '退款记录'; 164 | -------------------------------------------------------------------------------- /material/Dayu/cart/cart-design.md: -------------------------------------------------------------------------------- 1 | # 购物车数据结构 2 | 3 | 购物车中元素的结构体 4 | 5 | ```go 6 | type cartItem struct { 7 | itemNo string 8 | } 9 | 10 | ``` 11 | 12 | 购物车用什么结构体来存储? 13 | 14 | ```json 15 | { 16 | "item":{ 17 | "itemId":"4183600003_0_buy", 18 | "scenario":0, 19 | "goodsId":4183600003, 20 | "getType":"buy", 21 | "source":"common", 22 | "num":1, 23 | "sel_status":1, 24 | "properties":{ 25 | "addedCartPrice":"14888" 26 | }, 27 | "addTime":1577103810, 28 | "updateTime":1577103810 29 | }, 30 | "updateTime":1577103810, 31 | "version":2 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /material/Dayu/cart/cart-requirement.md: -------------------------------------------------------------------------------- 1 | 对于一个电商来讲,购物车是整个购买流程最重要的一步。因为电商发展到今天购物车不仅仅只是为了完成打包下单的功能;也是收藏、对比、促销提醒、相关推荐的重要展示窗口。如此多的能力我们该如何设计保证购物车的高性能、以及良好的扩展能力来满足未来的发展呢? 2 | 3 | 今天开始我们就以一个假定的场景来输出一个购物车设计:某某电商平台,是一个多租户模式(我们前面的诸多设计都是多租户模式),用户可以把商品加入到购物车,并切按照商户纬度来展示、排序。当然购物车也支持常规的各种操作:选择、删除、清空、商品失效等。并且有相关的促销能够提醒用户。同时为了监控、运营,要支撑购物车数据同步到监控、数仓等能力。 4 | 5 | 本文会从用户使用的角度以及服务端两个角度来讲解系统的能力。本篇我们的主要目的是说清楚购物车的能力以及一些逻辑。下一篇会进行购物车模型设计以及接口定义。 6 | 7 | # 用户视角 8 | 9 | 我们先来定义一下在用户侧用户操作购物车的功能有哪些? 10 | 11 | ![用户则需求](./imgs/user-cart-c.png) 12 | 13 | 一个购物车基本的能力基本上都在上图中,下面我们一一来分解。 14 | 15 | ## 操作 16 | 17 | 我们从用户的角度来看,购物车对于用户来说可以添加商品到购物车(加购物车、立即购买都属于一种添加方式);加入进购物车后,不想要了可以删除该商品(删一个、删多个、清空);想多买可以修改购买数量,发现钱不够可以减少购买数量;或者发现红色的比白色更漂亮,可以在购物车方便的进行更换规格;对于一些价格很贵的商品,能够在购物车添加一些保障服务(其实是绑定的虚拟商品);在要去结算的时候,还会提供选择能力让用户决定哪些商品真的本次要购买。 18 | 19 | 通过上面的描述我们可以看到这个过程是有其内在联系的。这里说一下关于选中功能,业界有两种做法,各有优劣,我们来看一下。淘宝的产品选中状态是保存在客户端的,并且默认不选中,刷新、重新打开APP状态会消失;京东、苏宁这一类是保存在服务端,会记录用户选中状态。针对这两种情况各有优劣。 20 | 21 | **客户端:** 22 | 23 | 1. 性能,选中/不选中的逻辑直接放在本地做,减少网络请求 24 | 2. 体验,多端不能同步,但是购物车相对来说更像是一个收藏夹,每次用户自己选择也无可厚非 25 | 3. 计算,价格计算时需要上传本地选中商品(也可以本地计算) 26 | 4. 实现,主要靠客户端实现,与服务端无关,研发解耦合 27 | 28 | **服务端:** 29 | 30 | 1. 性能,每次操作选中都需要调用服务端,而该操作可能很频繁,除了网络损耗,服务端也需要考虑该如何快速找到修改的商品 31 | 2. 体验,多端同步状态,记录历史状态 32 | 3. 计算,服务端可获取数据,请求时无须上传额外数据 33 | 4. 实现,服务端与客户端需要商定如何交互,以及返回数据(每次选中会导致价格变化),耦合在一起 34 | 35 | 个人认为这两种方式并无谁具备明显优势,完全是一种基于业务模式以及团队情况来做选择。我们这里后续的设计会基于在服务端保存商品选中状态。 36 | 37 | 在整个操作逻辑中,有个两个比较重要的地方单独说明一下:购买方式与购物车内修改购买属性 38 | 39 | ### 购买方式 40 | 41 | 主要的购买方式有立即购买、加入购物车、拼团购两种方式。 42 | 43 | 首先普通的加入购物车没什么太多要说的。重点来看下立即购买与拼团。 44 | 45 | 立即购买在于操作上来说就是选择商品后直接到了订单确认页面,没有购物车中去结算这一步。但是它的实现却可以依赖购物车的逻辑来做,我们来看一下使用购物车与不使用购物车实现这个逻辑有什么差别? 46 | 47 | 如果使用购物车来实现,也就是用户点击立即购买时,商品本质上还是加入到购物车中,但这个购物车却与原型的购物车不同,因为该购物车只能加一个商品,并且每次操作都会被覆盖。在视角效果上也是直接从商品详情页面跳转到订单确认页面。来看看这种方式的好处 48 | 49 | 1. 与购物车在订单确认、下单逻辑上一致,内部可以直接通过购物车获取数据 50 | 2. 需要一个独立的专门用于一键购买的购物车来实现,内存有消耗 51 | 52 | 另外一种实现方式使用一个新的数据结构,因为一般来说一键购买更简单,它只需要商品信息、价格信息即可。每次交互均可以根据sku_id来获取。 53 | 54 | 1. 订单确认、下单逻辑上需要进行改造,每次请求之间要传递约定参数 55 | 2. 节省内存,上下交互通过sku_id来保证 56 | 57 | 我们会采用使用在服务端一键购买以独立的购物车形式来实现。购物车的数据模型一致,保证了后续处理流程上的一致。 58 | 59 | 对于拼团,他其实分为两部分,首先先发团这个信息,当团成立后。我们可以选择将成团的商品加入普通购物车,同时可以加购其它商品。也可以选择将成团商品加入一键购买的购物车,保证成团商品只能买一个。拼团模式更像是加入购物车的一个前置条件。本质上它对于购物车的设计没有影响。 60 | 61 | ### 购物车内修改购买属性 62 | 63 | 这里主要是指可以在购物车便捷的操作一些需要在spu纬度操作的事情,比如:变更规格(也就是更换sku),以及选择绑定到spu纬度的服务(保险、延保等)。 64 | 65 | 我们重点说一下选择绑定的服务。例如:我们买一个手机,厂家提供了延保、各种其它附加服务,一般情况这种服务都是虚拟商品。但是这有个特殊情况。这些保障服务首先不能单独购买,其次他是跟主商品的数量息息相关。比如买两个手机,如果选择了加购服务,那么这些服务的数量必须是2,这会是一个联动关系。 66 | 67 | 这些保障服务是不能进行单独购买的,它一定要跟特定的商品捆绑销售。 68 | 69 | 服务端在存储这部分数据时一定需要考虑如何保存这种层级关系,这部分我们后面模型设计的时候大家会看到。 70 | 71 | ![绑定商品关系](./imgs/product-relations.png) 72 | 73 | ## 提醒 74 | 75 | 促销提醒很简单,返回的购物车数据,每一个商品应该携带当前的促销信息。这部分重点在于怎么获取促销信息,会在服务端看到。 76 | 77 | 然后说下购物车数量的提醒,也就是显示当前购物车商品的数量。一般来说进入到APP就会调用一个接口,获取用户的未读消息数、购物车商品数等。这里是需要非常高的读取速度。那么这种需求该如何满足呢? 78 | 79 | **方案一:** 我们可以设计一个结构保存了用户相关的这种提醒信息数量,每次直接读取这个数据即可。不需要去跟消息服务、购物车服务打交道拿这些数据。 80 | 81 | **方案二:** 在消息、购物车的模型中均设计一个保存总数量的字段,在读取数据的接口中,通过并发的方式调用这些服务拿到数据后进行聚合,这样在速度上只取决于最慢的服务。 82 | 83 | 这里我们的设计会采用 **方案二**,因为这样在某种程度上效率可以得到保证,同时整个系统的结构数据的一致性更容易得到保障。当然这里有个系统一定要注意,并发读取一定要设计超时,不要因为某个服务读数问题而导致拖累整个接口的性能。 84 | 85 | 接下来再来看看促销,这部分除了提醒,还需要提供对应的入口,让用户完成促销的操作。比如说某个商品有券,那么可以直接提供入口去领取;可凑单,有入口进入凑单列表并选择商品等。这部分需要解决的问题是服务端该如何及时从商品纬度拿到这些促销活动。 86 | 87 | 从用户的视角看完了,我们再来站在研发的角度看看服务端有哪些事情要做 88 | 89 | # 研发视角 90 | 91 | 还是先来看看需求的汇总图: 92 | 93 | ![服务端则需求](./imgs/user-cart-s.png) 94 | 95 | ## 存储 96 | 97 | 对于存储,首选肯定是内存存储,至于要不要落库,我觉得没有必要。说下我的理由: 98 | 99 | 1. 购物车的数据相对变化非常频繁,落库成本比较高,如果异步方式落库,很难保障一致性 100 | 2. 极端情况,cache奔溃了,仅仅需要用户重新加入购物车,并且我们可以通过cache的持久化机制来保证数据的恢复 101 | 102 | 所以对于购物车,我们只会把数据完全保存在内存中。 103 | 104 | ## 商品销售类型发生变化 105 | 106 | 现在我们来讨论 **商品销售类型发生变化** 这个问题。这是什么意思呢?大家想一下:比如我把A商品加入到购物车,但是一直没有结算。这时运营说针对A商品搞一个活动,拿出10个库存5折购。那么问题来了,对于之前购物车中就有该商品的用户该如何处理?**这里解决的主要问题是:购物车有该商品的用户不能直接以5折买**。几种方案,我们来看一下: 107 | 108 | **方案一:** 促销配置后,所有购物车中有该商品的用户失效或删除,这个方案首先被pass,操作成本太高,并且用户体验差 109 | 110 | **方案二:** 购物车中要区分同一个SKU,不同销售类型。也就是说在我们的购物车中不是按照SKU的纬度来加商品,而是通过 **SKU+售卖类型** 来生成一个唯一识别码。 111 | 112 | 可以看到 **方案二** 解决了同一个sku在购物车并存的问题,并且库存之前互相不影响。不过这里又有一个问题?商品的售卖类型(或者说这个标记),该怎么什么地方设置?好像商品系统可以设计、促销系统也可以设置。我们的逻辑中会在促销系统中进行配置。因为商品属于基础逻辑,如果一改就是全局库存受到影响。活动结束后很难做到自动正常售卖。因此这个标记应该落到活动中进行设置(活动设置时会通过促销系统获取该商品之前的活动是否互斥,以确保配置的活动不会互相矛盾)。 113 | 114 | ## 依赖系统 115 | 116 | 购物车系统依赖了非常多的其它系统。 117 | 118 | - 购物车监控 119 | - 数据分析 120 | - 商品系统 121 | - 库存系统 122 | - 促销系统 123 | - 结算系统 124 | 125 | 这些依赖的系统,有的是为了传输数据,有的是为了获取数据。我们按照这两个纬度来看一下。 126 | 127 | ### 购物车数据分析 128 | 129 | 对于购物车数据来说,前端会通过埋点记录加入购物车数据的情况,但是前端埋点一般是记录触发了某个前端操作,但是并不知道该操作是否成功与否。以及无法及时了解当前整体购物车的数据情况。 130 | 131 | 为了让运营团队更完整的了解购物车当前情况,我们通过后端埋点的方式,将数据记录到本机文件,然后通过日志收集的方式将日志同步给数据收集服务。 132 | 133 | ### 促销提醒与计算 134 | 135 | 还剩下服务端要解决的是促销的提醒与价格计算问题。 136 | 137 | 现来说计算,针对这部分最佳的方式是,调用结算中心的价格计算。我们来看一下购物车中的价格计算与订单结算时的价格计算的差异。 138 | 139 | 首先购物车中计算价格时不知道用户的地址,这会影响运费的计算;再是不知道用券的情况。那么其实如果解决了这两个问题,我们就可以让价格计算出自同一个逻辑,仅仅是部分入参不同罢了。因此我们这里计算时可以按照最高运费来计算,同时用券默认在购物车都不使用券。对于促销问题这里是可以通过促销系统确认选中的商品可以享受哪些价格的。因此促销的价格应该计算在内。 140 | 141 | 接下来在再来说说如何为用户高效的提供促销的信息。先从我们的配置视野出发。 142 | 143 | 我们在配置一个促销活动或者发一张券时,都是将多个商品归到一个促销活动或者券的下面。如果按照活动、券的纬度来获取商品效率相对比较高。 144 | 145 | ![活动-商品](./imgs/activity-product.png) 146 | 147 | 但是在购物车的场景中发生了一个变化。我们是需要从商品纬度获取到该商品的所有活动信息(全平台活动、店铺活动); 148 | 那么购物车中为了展示这些信息该怎么做?很常规的一个做法(也确实不少公司是这样):把所有活动信息取出来,遍历出所有跟该商品相关的信息。这种做法效率很低,并且无法满足大规模的应用场景,比如双十一期间。 149 | 150 | 因此这里为了满足该需求,促销系统需要提供一个能力按照商品获取对应促销(活动、券)。因此一般来讲促销系统配置的活动不能仅仅是按照活动纬度存储,同时还需要生成一份商品纬度的促销信息。 151 | 152 | ![商品-活动](./imgs/product-activity.png) 153 | 154 | ## 失效与排序 155 | 156 | 还有两个小部分没有讲到,一是商品该如何失效,比如:库存没有了、下架了;二是购物车中的商品是多个店铺的,排序的策略是什么? 157 | 158 | 由于本文我们还只是讨论需求,不涉及具体的模型设计,因此只是介绍方案。首先是商品失效,这很像一个软删除操作,一旦设置,用户侧看到的商品将是无法进行结算的,只能进行删除操作。 159 | 160 | 对于排序我们会采用的设计是:根据某个店铺在购物车中最后发生操作的时间,最新的操作肯定在最上面。 161 | 162 | # 结尾 163 | 164 | 通过上面我们基本上搞清楚了购物车设计中我们要做什么,依赖的系统要提供什么能力。下篇开始进入数据模型的设计、前后端接口设计。 165 | 166 | 如果你对购物车上面的需求还有哪些补充,欢迎留言。我们一起来完善。 -------------------------------------------------------------------------------- /material/Dayu/cart/imgs/activity-product.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/cart/imgs/activity-product.png -------------------------------------------------------------------------------- /material/Dayu/cart/imgs/product-activity.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/cart/imgs/product-activity.png -------------------------------------------------------------------------------- /material/Dayu/cart/imgs/product-relations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/cart/imgs/product-relations.png -------------------------------------------------------------------------------- /material/Dayu/cart/imgs/promotions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/cart/imgs/promotions.png -------------------------------------------------------------------------------- /material/Dayu/cart/imgs/user-cart-c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/cart/imgs/user-cart-c.png -------------------------------------------------------------------------------- /material/Dayu/cart/imgs/user-cart-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/cart/imgs/user-cart-s.png -------------------------------------------------------------------------------- /material/Dayu/cart/购物车.xmind: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/cart/购物车.xmind -------------------------------------------------------------------------------- /material/Dayu/img/bytedance.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/Dayu/img/bytedance.jpeg -------------------------------------------------------------------------------- /material/LIYUNFAN/monkey-play/assert/form-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/LIYUNFAN/monkey-play/assert/form-1.png -------------------------------------------------------------------------------- /material/LIYUNFAN/monkey-play/form.md: -------------------------------------------------------------------------------- 1 | [通用抽奖工具(Glue万能胶)](http://skrshop.tech/#/?id=%25e9%2580%259a%25e7%2594%25a8%25e6%258a%25bd%25e5%25a5%2596%25e5%25b7%25a5%25e5%2585%25b7glue%25e4%25b8%2587%25e8%2583%25bd%25e8%2583%25b6)一文对抽奖需求进行了简要的分析。 2 | 3 | 这里跳出来抽奖需求,只对抽奖的形式进行分析。 4 | 5 | 抽奖形式目前看来分为以下两类: 6 | 7 | 1. 【独立开奖】从箱子里拿出一张奖券,上面写着 =XX洗衣机一台= 或者 =谢谢惠顾= ,可以立即获得抽奖结果。 8 | 9 | 2. 【统一开奖】每个人发一个号码,在某个时间段公布中奖号码,拿到该号码的人则为中奖者。 10 | 11 | # 独立开奖 12 | 13 | 独立开奖最重要的内容自然是每次抽奖后的中奖内容。那么什么时间、如何生成中奖信息便是重点。 14 | 15 | ## 保证整场活动的中奖概率 16 | 17 | 1. 根据准备发放的奖品数量生成列表,并对其进行随机排序,保证每个奖品出现在每个位置的概率相等。空奖也是奖品的一种。 18 | 2. 用户每次抽奖的时,只需要从奖堆的顶端拿出一个奖品交给用户即可。 19 | 20 | 也可以每次抽的时候在奖品余量的范围内进行一次随机,根据随机数在数轴上的位置获得抽中的奖品。 21 | 抽中之后对应奖品数量-1。 22 | 23 | ### 优点 24 | 1. 由于所有的抽奖内容都是提前生成好的,所以抽奖的时候效率较高。 25 | 2. 可以设置每种奖品的数量,不会存在用户抽到了但没有库存的情况。 26 | 27 | ### 缺点 28 | 1. 当需要准备的奖品数量多的时候,可能需要较长的时间来生成以及较大的空间来保存。 29 | 2. 无法做到无限制抽取。因为如果在活动中对奖品数量进行了追加,整场活动的中奖概率便可能会发生变化。 30 | 31 | ## 保证单次抽奖的中奖概率 32 | 1. 设置奖品获奖概率。 33 | 2. 在获奖概率范围内生成一个随机数,根据随机数在数轴上的位置获得抽中的奖品。 34 | 35 | ### 优点 36 | 1. 中奖概率稳定。 37 | 2. 可以发放无数的奖品,奖品库存数量变化也不会导致概率变化。 38 | 39 | ### 缺点 40 | 1. 可能会超发。因为每次抽奖都是一个独立的事件。在每个独立事件发生概率为1/10的前提下,连续N次中每次该事件都发生的概率为10的N次方分之一。 41 | 42 | # 统一开奖 43 | 44 | 【统一开奖】方式的重点在于如何 **客观** **公正** **第三方** 的生成中奖的N个号码。 45 | 46 | 假定我们的彩票号码是从0开始到N的连续数字(非此类情况则需要一个可以将生成数字转化为彩票号码的转化函数)。 47 | 48 | 在开奖时生成M个号码作为中奖号码。 49 | 50 | ## 如何生成M个号码 51 | 52 | 常见方式是使用随机函数进行生成。 53 | 54 | ## 如何让生成结果可被复查 55 | 56 | 随机函数一般都是伪随机,实际上是一连串由随机数种子决定的数字。当种子一定时,随机数列便是恒定的。 57 | 58 | 那么如果想要结果可被宠复查,只需要记录随机数生成器的种子即可。 59 | 60 | ## 如何让生成结果不可预测 61 | 62 | 只要随机数生成器的种子是不可预测的,则生成出来的数列变是不可预测的。 63 | 64 | 具体的实践方法有很多,例如使用当天某某报纸的头条的处理值作为种子,或者使用某个时间点后区块链上产生的第一个新块的值。这些值都是公开且不可预知的。 65 | 66 | ## 如何提升人为干预结果的难度 67 | 68 | 实际上这个需要参考具体的成本。种子越难提前预测,难度越高。 69 | 70 | 例如开奖时间的时间戳很容易获得,而未来某个时间后的第一个区块链的哈希值却很难预测。 71 | 72 | # 总结 73 | ![form-summary](https://github.com/skr-shop/manuals/blob/master/material/LIYUNFAN/monkey-play/assert/form-1.png?raw=true) 74 | -------------------------------------------------------------------------------- /material/TIGERB/golden-flash.md: -------------------------------------------------------------------------------- 1 | # Golden Flash 金色闪光 2 | 3 | 金色闪光来自火影中四代火影**波风水门**的外号,速度的代表,适合顺势高并发的场景。 4 | 5 | # 前言 6 | 7 | 针对任何电商平台等高并发场景,比如**限量销售**、**限时销售**、**降价销售**、**抢优惠券**、**抽奖**等。 8 | 9 | 本质上是一个**和业务相关**的限流工具。 10 | 11 | 数据一致性要求并不高 且 可以容忍数据丢失 12 | 13 | # 架构 14 | 15 | 16 | 17 | # 交互设计 18 | 19 | # API 20 | 21 | 1. 获取活动信息接口 GET {version}/activity/info 22 | 23 | 请求参数: 24 | 25 | 字段|类型|是否必传|描述 26 | ------------|------------|------------|------------ 27 | act_id|number|Y|活动ID,本次秒抢活动的ID 28 | 29 | 响应内容: 30 | ```json 31 | { 32 | "code": "200", 33 | "msg": "OK", 34 | "result": { 35 | "server_time": "int, 服务器时间,时间戳", 36 | "lists": [ 37 | { 38 | "object_id": "number, 事物ID,被抢物品的ID", 39 | "object_name": "number, 事物ID,被抢物品的名称", 40 | "object_pic": [ 41 | "string, 被抢物品的图片", 42 | "string, 被抢物品的图片", 43 | ], 44 | "start_time": "int, 开始时间,时间戳", 45 | "end_time": "int, 结束时间,时间戳", 46 | "process": "int, 进度百分比 0~100,令牌剩余数量/令牌总数", 47 | } 48 | ] 49 | } 50 | } 51 | ``` 52 | 53 | 2. 加入排队接口 POST {version}/queue/add 54 | 55 | 请求参数: 56 | 57 | 字段|类型|是否必传|描述 58 | ------------|------------|------------|------------ 59 | act_id|number|Y|活动ID,本次秒抢活动的ID 60 | object_id|number|Y|事物ID,被抢物品的ID 61 | user_id|number|Y|用户ID 62 | 63 | 响应内容: 64 | ```json 65 | { 66 | "code": "200", 67 | "msg": "OK", 68 | "result": { 69 | "interval_time": "number, 查询排队结果的轮询时间", 70 | "voucher": "string, 排队成功的凭证,用于获取排队结果" 71 | } 72 | } 73 | ``` 74 | 75 | 3. 查询排队结果的接口 GET {version}/queue/result 76 | 77 | 请求参数: 78 | 79 | 字段|类型|是否必传|描述 80 | ------------|------------|------------|------------ 81 | voucher|number|Y|活动ID,本次秒抢活动的ID 82 | user_id|number|Y|用户ID 83 | 84 | 响应内容: 85 | ```json 86 | { 87 | "code": "200", 88 | "msg": "OK", 89 | "result": { 90 | "token": "string, 令牌,用户可以拿去此令牌去进行后续流程,比如添加抢购商品到购物车、抽奖" 91 | } 92 | } 93 | ``` -------------------------------------------------------------------------------- /material/TIGERB/marketing/coupon-refactor.md: -------------------------------------------------------------------------------- 1 | # 优惠券服务整理 2 | 3 | 备选文章标题: 4 | > [Skr-Shop]你想知道的优惠券业务,SkrShop来告诉你 5 | 6 | 经过两年的更新「SkrShop」已经构成了下面的架构: 7 | 8 |

9 | 10 | 11 | 12 |

13 | 14 | 图中**紫色的内容**就是本编文章的主要内容:营销体系的基础服务「优惠券服务」。但是呢,首先要说的是关于**不断被催更**的事。 15 | 16 | > 关于催更? 17 | 18 | 我给出了如下解释:人逢假日懒🤷‍♀️(我没错😭)、工作紧、需要保证质量,就酱。但是我一定能保证的是**一直会更新下去**,希望得到大家理解。 19 | 20 | > 关于下期内容? 21 | 22 | 之前在Github上的Issues大家一致想看关于订单相关的内容,所以更新完本期「优惠券」之后就开始了**订单之旅**。 23 | 24 | Issues如下: 25 | 26 | ``` 27 | 1. https://github.com/skr-shop/manuals/issues/25 28 | 2. https://github.com/skr-shop/manuals/issues/18 29 | ``` 30 | 31 | 进入正题,营销体系的基础服务「优惠券服务」。通过如下问题来介绍优惠券: 32 | 33 | - 优惠券有**哪些类型**? 34 | - 优惠券有**哪些适用范围**? 35 | - 优惠券有**哪些常见的场景**? 36 | - 优惠券服务要有**哪些服务能力**? 37 | - 优惠券服务的**风控**怎么做? 38 | 39 | ## 优惠券有哪些类型? 40 | 41 | 对于获取优惠券的用户而言:关注的是优惠券的优惠能力,所以按优惠能力维度优惠券主要分为下面三类: 42 | 43 | 优惠能力维度|描述 44 | ------------|------------ 45 | 满减券|满多少金额(不含邮费)可以减多少金额 46 | 现金券|抵扣多少现金(无门槛) 47 | 抵扣券|抵扣某Sku全部金额(一个数量) 48 | 折扣券|打折 49 | 50 | 对于发放优惠券的运营人员而言: 51 | 52 | 一种是「**固定有效期**」,优惠券的生效时间戳和过期时间戳,在创建优惠券的时候已经确定。用户在任意时间领取该券,该券的有效时间都是之前设置的有效时间的开始结束时间。 53 | 54 | 另一种是「**动态有效期**」,创建优惠券设置的是有效时间段,比如7天有效时间、12小时有效时间等。这类优惠券以用户领取优惠券的时间为优惠券的有效时间的开始时间,以以用户领取优惠券的时间+有效时间为有效时间的结束时间。 55 | 56 | 有效期维度|优惠券类型|优惠券生效时间|优惠券失效时间|描述 57 | ------------|------------|------------|------------|------------ 58 | 固定|固定有效期|优惠券类型被创建时已确定|优惠券类型被创建时已确定|无论用户什么时间领取该优惠券,优惠券生效的时间都是设置好的统一时间 59 | 动态|动态有效期|用户领取优惠券时,当前时间戳|用户领取优惠券时,当前时间戳 + N\*24\*60\*60|优惠券类型被创建时,只确定了该优惠券的有效,例如6小时、7天、一个月 60 | 61 | 小结如下: 62 | 63 |

64 | 65 | 66 | 67 |

68 | 69 | ## 优惠券有哪些适用范围? 70 | 71 | #### 运营策略 72 | 73 | 运营策略|描述 74 | ------------|------------ 75 | (非)指定Sku|Sku券 76 | (非)指定Spu|Spu券 77 | (非)指定类别|类别券 78 | 指定店铺|店铺券 79 | 全场通用|平台券 80 | 81 | #### 适用终端 82 | 83 | 适用终端(复选框)|描述 84 | ------------|------------ 85 | Android|安卓端 86 | iOS|iOS端 87 | PC|网页电脑端 88 | Mobile|网页手机端 89 | Wechat|微信端 90 | 微信小程序|微信小程序 91 | All|以上所有 92 | 93 | #### 适用人群 94 | 95 | 适用人群|描述 96 | ------------|------------ 97 | 白名单|测试用户 98 | 会员|会员专属 99 | 100 | 小结如下: 101 | 102 |

103 | 104 | 105 | 106 |

107 | 108 | ## 优惠券有哪些常见的场景? 109 | 110 | #### 领取优惠券场景 111 | 112 | 领取优惠券场景|描述 113 | ------------|------------ 114 | 活动页面|大促、节假日活动页面展示获取优惠券的按钮 115 | 游戏页面|通过游戏获取优惠券 116 | 店铺首页|店铺首页展示领券入口 117 | 商品详情|商品详情页面展示领券入口 118 | 积分中心|积分兑换优惠券 119 | 120 | #### 展示优惠券场景 121 | 122 | 展示优惠券场景|描述 123 | ------------|------------ 124 | 活动页面|大促、节假日活动页面展示可以领取的优惠券 125 | 商品详情|商品详情页面展示可以领取、可以使用的优惠券列表 126 | 个人中心-我的优惠券|我的优惠券列表 127 | 订单结算页面|结算页面,适用该订单的优惠券列表以及推荐 128 | 积分中心|展示可以兑换的优惠券详情 129 | 130 | 131 | #### 选择优惠券场景 132 | 133 | 选择优惠券场景|描述 134 | ------------|------------ 135 | 商品详情|商品详情页面展示该用户已有的,且适用于该商品的优惠券 136 | 订单结算页面-优惠券列表|选择可用优惠券结算 137 | 订单结算页面-输入优惠码|输入优惠码结算 138 | 139 | #### 返还优惠券场景 140 | 141 | 返还优惠券场景|描述 142 | ------------|------------ 143 | 未支付订单取消|未支付的订单,用户主动取消返还优惠券,或超时关单返还优惠券 144 | 已支付订单全款取消|已支付的订单,订单部分退款不返还,当整个订单全部退款返还优惠券 145 | 146 | #### 场景示例 147 | 148 | 场景示例|描述 149 | ------------|------------ 150 | 活动页领券|大促、节假日活动页面展示获取优惠券的按钮 151 | 游戏发券|游戏奖励 152 | 商品页领券|- 153 | 店铺页领券|- 154 | 购物返券|购买某个Sku,订单妥投后发放优惠券 155 | 新用户发券|新用户注册发放优惠券 156 | 积分兑券|积分换取优惠券 157 | 158 | 小结如下: 159 | 160 |

161 | 162 | 163 | 164 |

165 | 166 | ## 优惠券服务要有哪些服务能力? 167 | 168 | #### 服务能力1: 发放优惠券 169 | 170 | 发放方式|描述 171 | ------------|------------ 172 | 同步发放|适用于用户点击领券等实时性要求较高的获取券场景 173 | 异步发放|适用于实时性要求不高的发放券场景,比如新用户注册发券等场景 174 | 175 | 发放能力|描述 176 | ------------|------------ 177 | 单张发放|指定一个优惠券类型ID,且指定一个UID只发一张该券 178 | 批量发放|指定一个优惠券类型ID,且指定一批UID,每个UID只发一张该券 179 | 180 | 发放类型|描述 181 | ------------|------------ 182 | 优惠券类型标识|通过该优惠券类型的身份标识发放,比如创建一个优惠券类型时会生成一个16位标识码,用户通过`16位标识码`领取优惠券;这里不使用自增ID(避免对外泄露历史创建了的优惠券数量), 183 | 优惠码code|创建一个优惠券类型时,运营人员会给该券填写一个6位左右的Ascall码,比如`SKR6a6`,用户通过该码领取优惠券 184 | 185 | #### 服务能力2: 撤销优惠券 186 | 187 | 撤销能力|描述 188 | ------------|------------ 189 | 单张撤销|指定一个优惠券类型ID,且指定一个UID只撤销一张该券 190 | 批量撤销|指定一个优惠券类型ID,且指定一批UID,每个UID撤销一张该券 191 | 192 | #### 服务能力3: 查询优惠券 193 | 194 | 用户优惠券列表|子类|描述 195 | ------------|------------|------------ 196 | 全部|-|查询该用户所有的优惠券 197 | 可以使用|全部|查询该用户所有可以使用的优惠券 198 | -|适用于某个spu或sku|查询该用户适用于某个spu或sku可以使用的优惠券 199 | -|适用于某个类别|查询该用户适用于某个类别可以使用的优惠券 200 | -|适用于某个店铺|查询该用户适用于某个店铺可以使用的优惠券 201 | 无效|全部|查询该用户所有无效的优惠券 202 | -|过期|查询该用户所有过期的优惠券 203 | -|失效|查询该用户所有失效的优惠券 204 | 205 | #### 服务能力4: 结算页优惠券推荐 206 | 207 | 订单结算页面推荐一张最适合该订单的优惠券 208 | 209 | 小结如下: 210 | 211 |

212 | 213 | 214 | 215 |

216 | 217 | ## 优惠券服务的风控怎么做? 218 | 219 | 一旦有发生风险的可能则触发风控: 220 | 221 | - 对用户,提示稍后再试或联系客服 222 | - 对内部,报警提示,核查校验报警是否存在问题 223 | 224 | #### 频率限制 225 | 226 | 领取|描述 227 | ------------|------------ 228 | 设备ID|每天领取某优惠券的个数限制 229 | UID|每天领取某优惠券的个数限制 230 | IP|每天领取某优惠券的个数限制 231 | 232 | 使用|描述 233 | ------------|------------ 234 | 设备ID|每天使用某优惠券的个数限制 235 | UID|每天使用某优惠券的个数限制 236 | IP|每天使用某优惠券的个数限制 237 | 手机号|每天使用某优惠券的个数限制 238 | 邮编|比如注重邮编的海外地区,每天使用某优惠券的个数限制 239 | 240 | #### 用户风险等级 241 | 242 | 依托用户历史订单数据,得到用户成功完成交易(比如成功妥投15天+)的比率,根据此比率对用户进行等级划分,高等级进入通行Unblock名单,低等级进入Block名单,根据不同用户级别设置限制策略。等其他大数据分析手段。 243 | 244 | #### 阈值 245 | 246 | - 发券预算 247 | - 实际使用券预算 248 | 249 | 根据预算值设置发券总数阈值,当触发阈值时阻断并报警。 250 | 251 | #### 优惠券不要支持虚拟商品 252 | 253 | 优惠券尽量不要支持虚拟商品以防止可能被利用的不法活动。 254 | 255 |

256 | 257 | 258 | 259 |

-------------------------------------------------------------------------------- /material/TIGERB/marketing/coupon.md: -------------------------------------------------------------------------------- 1 | # 优惠券服务 2 | 3 | ## 优惠券属性 4 | 5 | ### 有效期维度 6 | 7 | 有效期|优惠券类型|优惠券生效时间|优惠券失效时间|描述 8 | ------------|------------|------------|------------|------------ 9 | 固定|固定时间优惠券|优惠券类型被创建时已确定|优惠券类型被创建时已确定|无论用户什么时间领取该优惠券,优惠券生效的时间都是设置好的统一时间 10 | 动态|动态时间优惠券|用户领取该优惠券时间为开始|用户领取该优惠券时间 + 有效时间|优惠券类型被创建时,只确定了该优惠券的有效,例如6小时、7天、一个月 11 | 12 | ### 优惠能力维度 13 | 14 | 优惠能力维度|描述 15 | ------------|------------ 16 | 满减券|达到一定订单金额才能使用,抵现金 17 | 现金券|金额抵现金 18 | 折扣券|折扣 19 | 20 | ### 归属维度 21 | 22 | 归属维度|描述 23 | ------------|------------ 24 | 指定Sku| 25 | 指定Spu| 26 | 指定类别| 27 | 指定店铺| 28 | 全场通用| 29 | 30 | ### 终端维度 31 | 32 | 终端维度|描述 33 | ------------|------------ 34 | Android| 35 | iOS| 36 | PC| 37 | Mobile| 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 | ### 用户优惠券展示场景维度 74 | 75 | - 商品详情推荐 76 | + 获取适用于该商品的优惠券列表(用户已经领取) 77 | - 个人中心 78 | + 用户优惠券列表 79 | 80 | ## 优惠券的逆向流 81 | 82 | ## 风险控制 83 | 84 | ## 常见业务场景 85 | 86 | - 活动页领券 87 | - 游戏发券 88 | - 商品页领券 89 | - 店铺页领券 90 | - 购物返券 -------------------------------------------------------------------------------- /material/TIGERB/marketing/lottery-demand.md: -------------------------------------------------------------------------------- 1 | # [Skr-Shop]通用抽奖工具之需求分析 2 | 3 | ## 前言 4 | 5 | 首先我们先来回顾下**营销体系**的组成: 6 | 7 | |营销体系| 8 | |---| 9 | |活动营销系统| 10 | |销售营销系统| 11 | 12 | 今天带来的是**活动营销系统**下的第一个独立子系统**通用抽奖工具**的介绍,本篇文章主要分为如下4部分: 13 | 14 | - 常见抽奖场景与归类 15 | - 抽奖需求配置 16 | - 常见奖品类型 17 | - 抽奖五要素 18 | 19 | ## 常见抽奖场景与归类 20 | 21 | 下面是我列出来的一些常见的抽奖场景,红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等活动营销场景。 22 | 23 | |活动名称|描述| 24 | |------|------| 25 | |红包雨|每日整点抢红包🧧抽奖,每个整点一般可参与一次| 26 | |糖果雨|每日整点抢糖果🍬抽奖,每个整点一般可参与一次| 27 | |打地鼠|每日整点打地鼠抽奖,每个整点一般可参与一次| 28 | |大转盘(九宫格)|某个时间段,转盘抽奖,每个场一般可参N次| 29 | |考眼力|某个时间段,旋转杯子猜小球在哪个被子里,猜对可抽奖,一般每日可参与N次| 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 | |红包雨|是|1|N| 62 | |糖果雨|是|1|N| 63 | |打地鼠|是|N|N| 64 | |幸运大转盘(九宫格)|否|N|N| 65 | |考眼力|否|N|N| 66 | 67 | 通过上面的分析我们得到了**活动**和**场次**的概念: 一个活动需要支持多场次的配置。 68 | 69 | - 活动activity:配置活动的日期范围 70 | - 场次session:配置每场的具体时间范围 71 | 72 | **红包雨的需求配置示例:** 73 | 74 | > 活动特征:红包雨需要支持多场次。 75 | 76 | 比如双十二期间三天、每天三场整点红包雨配置如下: 77 | 78 | 活动、场次配置: 79 | 80 | |双十二红包雨| 81 | |------| 82 | |活动配置:| 83 | |2019-12-10 ~ 2019-12-12| 84 | |场次配置:| 85 | |10:00:00 ~ 10:01:00| 86 | |12:00:00 ~ 12:01:00| 87 | |18:00:00 ~ 18:01:00| 88 | 89 | 奖品配置: 90 | 91 | |场次|奖品1|奖品2|---|奖品N| 92 | |------|------|------|---|------| 93 | |场次10:00:00 ~ 10:01:00|优惠券2元|空奖|---|无| 94 | |场次12:00:00 ~ 12:01:00|优惠券5元|空奖|---|无| 95 | |场次18:00:00 ~ 18:01:00|优惠券10元|优惠券20元|---|空奖| 96 | 97 | ```md 98 | 上面配置的结果如下: 99 | 100 | 2019-12-10日三场整点红包雨: 101 | 2019-12-10 10:00:00 ~ 10:01:00 102 | 2019-12-10 12:00:00 ~ 12:01:00 103 | 2019-12-10 18:00:00 ~ 18:01:00 104 | 105 | 2019-12-11日三场整点红包雨: 106 | 2019-12-11 10:00:00 ~ 10:01:00 107 | 2019-12-11 12:00:00 ~ 12:01:00 108 | 2019-12-11 18:00:00 ~ 18:01:00 109 | 110 | 2019-12-12日三场整点红包雨: 111 | 2019-12-12 10:00:00 ~ 10:01:00 112 | 2019-12-12 12:00:00 ~ 12:01:00 113 | 2019-12-12 18:00:00 ~ 18:01:00 114 | ``` 115 | 116 | **幸运大转盘的需求配置示例:** 117 | 118 | > 活动特征:幸运大转盘不需要多场次。 119 | 120 | 比如年货节2020-01-20 ~ 2020-02-10期间幸运大转盘配置如下: 121 | 122 | 活动、场次配置: 123 | 124 | |双十二幸运大转盘| 125 | |------| 126 | |活动配置:| 127 | |2019-12-10 ~ 2019-12-12| 128 | |场次配置:| 129 | |00:00:00 ~ 23:59:59| 130 | 131 | 奖品配置: 132 | 133 | |场次|奖品1|奖品2|---|奖品N| 134 | |------|------|------|---|------| 135 | |场次00:00:00 ~ 23:59:59|优惠券2元|空奖|---|无| 136 | 137 | ```md 138 | 上面配置的结果如下: 139 | 140 | 幸运大转盘抽奖活动将于 2019-12-10 00:00:00 ~ 2019-12-12 23:59:59 进行 141 | ``` 142 | 143 | 注意与思考:双十二幸运大转盘不需要多个场次,只配置一个场次即可,完全复用活动场次模型。 144 | 145 | ### 接着,第二类: `按抽奖次数抽奖`的需求配置 146 | 147 | |类型|活动名称|特点| 148 | |-|-|-| 149 | |按抽奖次数抽奖|答题闯关、游戏闯关|(成功参与)当前活动次数维度| 150 | 151 | **答题闯关的需求配置示例:** 152 | 153 | > 活动特征:每一关的奖品不同,一般越到后面中大奖的几率越大。 154 | 155 | 活动、场次配置: 156 | 157 | |双十二答题闯关| 158 | |------| 159 | |活动配置:| 160 | |2019-12-10 ~ 2019-12-12| 161 | |场次配置:| 162 | |00:00:00 ~ 23:59:59| 163 | 164 | 奖品配置: 165 | 166 | |双十二答题闯关|奖品| 167 | |------|------| 168 | |第一关|优惠券2元| 169 | |第二关|优惠券5元| 170 | |第三关|优惠券10元| 171 | |第四关|优惠券20元| 172 | |第五关|优惠券50元| 173 | |第六关|优惠券100元| 174 | 175 | 注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。 176 | 177 | ### 最后,第三类: `按数额范围区间抽奖`的需求配置: 178 | 179 | |类型|活动名称|特点| 180 | |-|-|-| 181 | |按数额范围区间抽奖|支付刮刮乐、积分刮刮乐|数额区间维度| 182 | 183 | **支付刮刮乐的需求配置示例:** 184 | 185 | > 活动特征:不同的订单金额,一般金额越大中大奖的几率越大。 186 | 187 | 活动、场次配置: 188 | 189 | |双十二答题闯关| 190 | |------| 191 | |活动配置:| 192 | |2019-12-10 ~ 2019-12-12| 193 | |场次配置:| 194 | |00:00:00 ~ 23:59:59| 195 | 196 | 奖品配置: 197 | 198 | |订单金额|奖品1|奖品2|---|奖品N| 199 | |------|------|------|---|------| 200 | |0~100|优惠券2元|空奖|---|无| 201 | |100~200|优惠券5元|空奖|---|无| 202 | |200~1000|优惠券10元|优惠券20元|---|空奖| 203 | |1000以上|优惠券50元|笔记本电脑|---|空奖| 204 | 205 | 注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。 206 | 207 | > 总结: 通过上面的分析我们得到了抽奖工具的两个要素**活动**和**场次**。 208 | 209 | ## 常见奖品类型 210 | 211 | > 抽奖抽什么? 212 | 213 | |常见奖品类型| 214 | |-| 215 | |优惠券| 216 | |积分| 217 | |实物| 218 | |空奖| 219 | 220 | > 总结: 我们得到了抽奖工具的另一个要素**奖品**。 221 | 222 | ## 抽奖五要素 223 | 224 | 通过上面的分析我们已经得到了抽奖的**三要素** 225 | 226 | - 活动 227 | - 场次 228 | - 奖品 229 | 230 | > 那还有什么要素我们还没聊到呢?接下来来看。 231 | 232 | #### 第四要素:中奖概率 233 | 234 | 抽奖自然离不开奖品的中奖概率的设置。关于中奖概率我们支持如下灵活的配置: 235 | 236 | 1. 手动设置奖品中奖概率 237 | 2. 自动概率,根据当前奖品的数量、奖品的权重得到中奖概率 238 | 239 | 比如我们某次大促活动红包雨的配置如下: 240 | 241 | 活动配置|描述 242 | ------|------ 243 | 活动时间|2019-12-10~2019-12-12 244 | 活动名称|2019双十二大促整点红包雨 245 | 活动描述|2019双十二大促全端整点红包雨活动 246 | 手动设置奖品概率|是 247 | 248 | |场次|奖品类型|具体奖品|奖品数量|中奖概率 249 | |-|-|-|-|-| 250 | |10:00:00 ~ 10:01:00|优惠券|2元优惠券|2000|50%| 251 | |-|优惠券|5元优惠券|1000|20%| 252 | |-|空奖|-|5000|30%| 253 | |12:00:00 ~ 12:01:00|优惠券|2元优惠券|2000|50%| 254 | |-|优惠券|5元优惠券|1000|20%| 255 | |-|空奖|-|5000|30%| 256 | |18:00:00 ~ 18:01:00|优惠券|2元优惠券|2000|50%| 257 | |-|优惠券|5元优惠券|1000|20%| 258 | |-|空奖|-|5000|30%| 259 | 260 | 备注:每轮场次中奖概率之和必须为100%,否则剩余部分默认添加为空奖的中奖概率。 261 | 262 | #### 第五要素:均匀投奖 263 | 264 | > 如何均匀的抽走奖品? 265 | 266 | 答案: 均匀投奖。 267 | 268 | 具体方式为拆分总奖品数量,到各个细致具体的时间段。以双十二幸运大转盘为例: 269 | 270 | |场次|奖品类型|具体奖品|奖品数量|中奖概率|投奖时间(默认提前5分钟投奖)|投奖数量 271 | |-|-|-|-|-|-|-| 272 | |00:00:00 ~ 23:59:59|优惠券|2元优惠券|2000|50%|-|-| 273 | |-|-|-|-|-|00:00:00|2000| 274 | |-|-|-|-|-|06:00:00|2000| 275 | |-|-|-|-|-|12:00:00|2000| 276 | |-|-|-|-|-|18:00:00|2000| 277 | 278 | 这里我们就得到了抽奖的**第五个要素:均匀投奖**。 279 | 280 | ## 结语 281 | 282 | 通过上面的分析,我们得到抽奖五要素如下: 283 | 284 | 抽奖五要素|要素名称 285 | ------|------ 286 | 第一要素|活动 287 | 第二要素|场次 288 | 第三要素|奖品 289 | 第四要素|中奖概率 290 | 第五要素|均匀投奖 291 | 292 | 同时我们通过**抽奖五要素**也得到了**通用抽奖工具**配置一场抽奖活动的5个基本步骤: 293 | 294 | 1. 活动配置 295 | 2. 场次配置 296 | 3. 奖品配置 297 | 4. 奖品中奖概率配置 298 | 5. 奖品投奖配置 299 | 300 | 最后,接着一篇文章,我们将来介绍**通用抽奖工具**的DB设计和配置后台设计。 -------------------------------------------------------------------------------- /material/TIGERB/marketing/lottery-sys.md: -------------------------------------------------------------------------------- 1 | # [Skr-Shop]通用抽奖工具之系统设计 2 | 3 | ## 前言 4 | 5 | 上篇文章[《SkrShop通用抽奖工具之需求分析》](http://tigerb.cn/2019/12/23/skr-lottery/)我们已经通过一些常见的抽奖场景,得到了符合这些抽奖场景的抽奖工具五要素: 6 | 7 | 抽奖五要素|要素名称 8 | ------|------ 9 | 第一要素|活动 10 | 第二要素|场次 11 | 第三要素|奖品 12 | 第四要素|中奖概率 13 | 第五要素|均匀投奖 14 | 15 | 以及创建一个抽奖活动的5个基本步骤,如下: 16 | 17 | 1. 活动配置 18 | 2. 场次配置 19 | 3. 奖品配置 20 | 4. 奖品中奖概率配置 21 | 5. 奖品投奖配置 22 | 23 | > 上篇文章回顾 [《Skr-Shop通用抽奖工具之需求分析》](http://tigerb.cn/2019/12/23/skr-lottery/) 24 | 25 | 需求已经分析完了,今天我们就来看看这通用抽奖工具具体的设计,分为如下三个部分: 26 | 27 | - DB设计 28 | - 配置后台设计 29 | - 接口设计 30 | 31 | ## DB设计 32 | 33 | 第一要素`活动配置`的`抽奖活动表`: 34 | 35 | ```sql 36 | -- 通用抽奖工具(万能胶Glue) glue_activity 抽奖活动表 37 | CREATE TABLE `glue_activity` ( 38 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '活动ID', 39 | `serial_no` char(16) unsigned NOT NULL DEFAULT '' COMMENT '活动编号(md5值中间16位)', 40 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '活动名称', 41 | `description` varchar(255) NOT NULL DEFAULT '' COMMENT '活动描述', 42 | `activity_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖', 43 | `probability_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '中奖概率类型1: static 2: dynamic', 44 | `times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制', 45 | `start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动开始时间', 46 | `end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动结束时间', 47 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 48 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 49 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 50 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 51 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable', 52 | PRIMARY KEY (`id`) 53 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖活动表'; 54 | ``` 55 | 56 | 第二要素`场次配置`的`抽奖场次表`: 57 | 58 | ```sql 59 | -- 通用抽奖工具(万能胶Glue) glue_session 抽奖场次表 60 | CREATE TABLE `glue_session` ( 61 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '场次ID', 62 | `activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID', 63 | `times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制', 64 | `start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次开始时间', 65 | `end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次结束时间', 66 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 67 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 68 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 69 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 70 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable', 71 | PRIMARY KEY (`id`) 72 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次表'; 73 | ``` 74 | 75 | 第三、四要素`奖品配置`的`抽奖场次奖品表`: 76 | 77 | ```sql 78 | -- 通用抽奖工具(万能胶Glue) glue_session_prizes 抽奖场次奖品表 79 | CREATE TABLE `glue_session_prizes` ( 80 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 81 | `session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID', 82 | `node` varchar(255) NOT NULL DEFAULT '' COMMENT '节点标识 按时间抽奖: 空值, 按抽奖次数抽奖: 第几次参与值, 按数额范围区间抽奖: 数额区间上限值', 83 | `prize_type` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型 1:优惠券, 2:积分, 3:实物, 4:空奖 ...', 84 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖品名称', 85 | `pic_url` varchar(255) NOT NULL DEFAULT '' COMMENT '奖品图片', 86 | `value` varchar(255) NOT NULL DEFAULT '' COMMENT '奖品抽象值 优惠券:优惠券ID, 积分:积分值, 实物: sku ID', 87 | `probability` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '中奖概率1~100', 88 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 89 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 90 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 91 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 92 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable', 93 | PRIMARY KEY (`id`) 94 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品表'; 95 | 96 | ``` 97 | 98 | 第五要素`均匀投奖`的`抽奖场次奖品定时投放器表`: 99 | 100 | ```sql 101 | -- 通用抽奖工具(万能胶Glue) glue_session_prizes_timer 抽奖场次奖品定时投放器表 102 | CREATE TABLE `glue_session_prizes_timer` ( 103 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 104 | `session_prizes_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖场次奖品ID', 105 | `delivery_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '定时投放奖品数量的时间', 106 | `prize_quantity` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '奖品数量', 107 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 108 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 109 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 110 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 111 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:wait, 1:success', 112 | PRIMARY KEY (`id`) 113 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品定时投放器表'; 114 | 115 | ``` 116 | 117 | 其他表,抽奖记录&奖品发放记录表: 118 | 119 | ```sql 120 | -- 通用抽奖工具(万能胶Glue) glue_user_draw_record 用户抽奖记录表 121 | CREATE TABLE `glue_user_draw_record` ( 122 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 123 | `activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID', 124 | `session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID', 125 | `prize_type_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型ID', 126 | `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人user_id', 127 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 128 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 129 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:未中奖, 1:已中奖 , 2: 发奖失败 , 3: 已发奖', 130 | `log` text COMMENT '操作信息等记录', 131 | PRIMARY KEY (`id`) 132 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户抽奖记录表'; 133 | ``` 134 | 135 | ## 配置后台设计 136 | 137 | #### 创建活动 138 | 139 |

140 | 141 | 142 | 143 |

144 | 145 | #### 创建活动场次-按数额范围区间抽奖 146 | 147 |

148 | 149 | 150 | 151 |

152 | 153 |

154 | 155 | 156 | 157 |

158 | 159 |

160 | 161 | 162 | 163 |

164 | 165 | #### 活动列表 166 | 167 |

168 | 169 | 170 | 171 |

172 | 173 | 174 | ## 接口设计 175 | 176 | 1. 获取活动信息 GET {version}/glue/activity 177 | 178 | 请求参数: 179 | 180 | 字段|类型|是否必传|描述 181 | ------------|------------|------------|------------ 182 | serial_no|string|Y|活动编号 183 | 184 | 响应内容: 185 | ```json 186 | { 187 | "code": "200", 188 | "msg": "OK", 189 | "result": { 190 | "serial_no": "string, 活动编号", 191 | "type": "number, 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖", 192 | "name": "string, 活动名称", 193 | "description": "string, 活动描述", 194 | "start_time": "number, 活动开始时间", 195 | "end_time": "number, 活动开始时间", 196 | "remaining_times": "number, 活动抽奖次数限制,0不限制", 197 | "sessions_list":[ 198 | { 199 | "start_time": "number, 场次开始时间", 200 | "end_time": "number, 场次开始时间", 201 | "remaining_times": "number, 场次抽奖次数限制,0不限制", 202 | "prizes_list": [ 203 | { 204 | "name": "string, 奖品名称", 205 | "pic_url": "string, 奖品图片" 206 | } 207 | ] 208 | } 209 | ] 210 | } 211 | } 212 | ``` 213 | 214 | 2. 抽奖 POST {version}/glue/activity/draw 215 | 216 | 请求参数: 217 | 218 | 字段|类型|是否必传|描述 219 | ------------|------------|------------|------------ 220 | serial_no|string|Y|活动编号 221 | uid|number|Y|用户ID 222 | 223 | 响应内容: 224 | ```json 225 | // 中奖 226 | { 227 | "code": "200", 228 | "msg": "OK", 229 | "result": { 230 | "serial_no": "string, spu id", 231 | "act_remaining_times": "number, 本活动抽奖剩余次数,0不限制", 232 | "session_remaining_times": "number, 本场次抽奖剩余次数,0不限制", 233 | "prizes_info": 234 | { 235 | "name": "string, 奖品名称", 236 | "pic_url": "string, 奖品图片" 237 | } 238 | } 239 | } 240 | 241 | // 未中奖 242 | { 243 | "code": "401", 244 | "msg": "", 245 | "result": { 246 | 247 | } 248 | } 249 | ``` 250 | 251 | ## 结语 252 | 253 | 活动营销系统中的第一个字系统**通用抽奖工具**今天讲完了,希望对大家有一定的帮助或启示。 254 | 255 | ## 彩蛋 256 | 257 | 想告诉大家,通用抽奖工具的代码设计特别适合设计模式中的`模板模式`,你们觉着呢😏😏😏。所以,新的一年我会再写一篇《[Skr-Shop]通用抽奖工具之代码设计》吗? 258 | 259 | (O_O)? 260 | 261 | ## 2020 262 | 263 | 最后后,祝大家2020年新年🆕快乐~ 264 | -------------------------------------------------------------------------------- /material/TIGERB/marketing/marketing.md: -------------------------------------------------------------------------------- 1 | # [Skr-Shop]营销体系开篇 2 | 3 | ## 前言 4 | 5 | 营销体系在一个电商系统中承担着尖刀般的作用,为电商团队拼下无数订单,就如同现实生活中的一线销售人员,引流、促销、提高成单率和客单价。 6 | 7 | 一个营销体系拥有各种提升GMV的手段,并且仍然在不断的推陈出新,着实令人敬佩。 8 | 9 | ## 营销体系拆分 10 | 11 | 按照不同的维度,我们把营销体系划分为两大系统: 12 | 13 | 系统|描述|维度 14 | ---|---|--- 15 | 活动营销系统|拥有各种手段**提升PV\UV**,并引导消费|PV、UV维度 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 | 满减|单笔订单满多少减多少,例如,“满500减20” 57 | 满赠|单笔订单满多少增送其他商品,例如,“满999送秋裤” 58 | 买送|买就送,例如,“全场下单送秋裤” 59 | 限时购|限定时间区间,降价或折扣 60 | 秒杀|白送价格,数量极少的销售场景,例如,“一元秒杀” 61 | 加价购|再加多少钱,可以低价购买其他推荐商品 62 | 多买|单笔订单购买多个sku,享受折扣 63 | 全款预售|预售库存一次付清 64 | 订金膨胀预售|包含订金阶段、尾款阶段,订金翻倍抵现尾款,订金可退 65 | 定金膨胀预售|包含定金阶段、尾款阶段,定金翻倍抵现尾款,定金不退 66 | 盲售|包含订金(定金)阶段,尾款阶段;不知道尾款价格,不知道sku的具体信息,只知道spu的信息(可选) 67 | 拼团|限定时间凑够多人成功支付订单(享优惠价格) 68 | 砍价(点赞降价)|限定时间邀请好友砍价到指定金额 69 | 众筹|限定时间购买人数达到指定人数及以上即可成功享受优惠价格 70 | 组合套装|多个sku捆绑销售 71 | ---|--- 72 | 满减券|单笔订单满多少才能使用的优惠券 73 | 折扣券|单笔订单折扣 74 | 现金券|单笔订单抵现金 75 | 76 | ## 结语 77 | 78 | 接下来我们将开始介绍**活动营销系统**中第一个系统**抽奖系统**的设计: 79 | 80 | > [Skr-Shop]通用抽奖工具之需求分析 81 | 82 | 尽情期待。 83 | 84 | -------------------------------------------------------------------------------- /material/TIGERB/marketing/seckill-business.md: -------------------------------------------------------------------------------- 1 | # [Skr-Shop]做电商还搞不清一元秒杀、常规秒杀、限时购? 2 | 3 | ## 前言 4 | 5 | 今天来**补**一下秒杀系统的业务分析,前几天发了PPT,今天把业务这块内容摘出来补充到「http://skrshop.tech/」的文档里。另外,关于秒杀系统核心设计可以看之前的文章[《什么,秒杀系统也有这么多种!》](http://tigerb.cn/2020/05/05/skrshop/seckill/)。 6 | 7 | 做业务的都知道: 8 | 9 | > 做系统概念很重要 10 | 11 | 因为在同事和同事间沟通中,这些概念可以精准的告诉别人你想表达的。尤其是电商系统,众所周知电商里有很多的概念,比如Sku、Spu等。 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 | |概念1|活动| 37 | |概念2|场次的概念,场次是活动的子集| 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 | |时间维度| 74 | |-------| 75 | |限时| 76 | 77 | 把上面的维度按照运营需求组合就得到了不同的秒杀活动类型,如下: 78 | 79 | ### 首先,一元秒杀之类:白菜价+极少+(爆品或者非爆品)+限时 80 | 81 |

82 | 83 | 84 | 85 |

86 | 87 | ### 其次,限时购(又称常规秒杀):非白菜价+(极少或非极少)+(爆品或者非爆品)+限时 88 | 89 |

90 | 91 | 92 | 93 |

94 | 95 | ### 接着,爆品抢购:非白菜价+(极少或非极少)+爆品+限时 96 | 97 |

98 | 99 | 100 | 101 |

102 | 103 | |秒杀活动类型|营销维度| 104 | |-------|-------| 105 | |一元秒杀之类|白菜价+极少+(爆品或者非爆品)+限时| 106 | |限时购(又称常规秒杀) |非白菜价+(极少或非极少)+(爆品或者非爆品)+限时 -> | 107 | |爆品抢购|非白菜价+(极少或非极少)+爆品+限时| 108 | 109 | 110 | ## 技术方案补充 111 | 112 | 在之前的文章[《什么,秒杀系统也有这么多种!》](http://tigerb.cn/2020/05/05/skrshop/seckill/)只关注了**秒杀这个动作**(核心秒杀接口),没有对整个流程做个讲解。今天这里就对这部分做个补充完善,具体如下: 113 | 114 | ### 补充完整流程方案① 115 | 116 | 完整流程主要涉及三个接口: 117 | 118 |

119 | 120 | 121 | 122 |

123 | 124 | |秒杀服务接口|对内还是对外|描述| 125 | |-------|-------|-------| 126 | |秒杀信息获取接口|对外|QPS要求高、所以可以直接对外 127 | |获取秒杀资格|对外|用户获取加入此商品加入购物车的资格| 128 | |校验并获取秒杀价格接口|对内|购物车接口校验资格,并返回该商品当前活动的秒杀资格| 129 | 130 | ### 补充完整流程方案② 131 | 132 |

133 | 134 | 135 | 136 |

137 | 138 | 这个方案和上面的有什么区别呢?答:把获取秒杀活动信息的接口统一收敛到了「营销中心」,目的: 139 | 140 | > 把所有的营销活动都抽象到一个「商品活动信息」的接口 141 | 142 | 这样,我们的商品详情页面的读的逻辑就很清晰,如下: 143 | 144 | - 第1类:商品基础信息接口,获取商品的基础信息(图片、名称、描述、价格、库存等等) 145 | - 第2类:商品活动信息接口,获取该商品参加的所有营销活动信息(满减、满赠、买送、秒杀等等) 146 | 147 | 图示: 148 | 149 |

150 | 151 | 152 | 153 |

-------------------------------------------------------------------------------- /material/TIGERB/marketing/seckill-ppt.md: -------------------------------------------------------------------------------- 1 | 注意是探索两字 2 | 3 | 换个角度讲 4 | 5 | 大家可能以为 我是来讲一个秒杀系统应该怎么做 然而并不主要是 6 | 7 | 而是 在这这个过程中 8 | 9 | 多问为什么 去探索为什么 10 | 11 | 提出问题 去探索解决问题的思路 12 | 13 | 14 | 理解inode https://www.ruanyifeng.com/blog/2011/12/inode.html 15 | 16 | 17 | # 业务概念 18 | 19 | ## 秒杀是什么 20 | 21 | 22 | 做系统概念很重要 尤其是电商系统 23 | 24 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200712224532.jpeg) 25 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200712224556.jpeg) 26 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200712224606.jpeg) 27 | 28 | ``` 29 | 页面上的电商概念: 1. 活动场次 活动的概念、场次的概念 场次是活动的子 30 | 31 | 页面属性: 32 | 33 | 1. 活动信息 34 | 2. 秒杀商品信息(商品图片、商品名称、商品加车价格、商品售价、其他描述信息) 35 | 3. 秒杀进度 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 | 问题 74 | - 流量带来的高并发问题 75 | - 高并发带来的超售问题 76 | 77 | 1. 怎么解决瞬时高并发的问题? 78 | 2. 怎么解决超售的问题? 79 | 80 | 用户多 -> 网络链接多 81 | 82 | 83 | 高并发问题 84 | - 多进程 -> php fpm模式 85 | - 多线程 -> java 86 | - 异步 -> node 87 | 88 | 为什么nginx、redis并发能力强? -> epoll 89 | 90 | 什么是epoll? 91 | 92 | i/o多路复用 网络i/o 以一次读为例 93 | 94 | - 等待内核准备数据 95 | - 内核拷贝数到用户态 96 | 97 | - 阻塞i/o 98 | - 非阻塞i/o 99 | - i/o多路复用 100 | - 异步i/o 101 | 102 | ![](https://segmentfault.com/img/bVm1c3) 103 | ![](https://segmentfault.com/img/bVm1c4) 104 | ![](https://segmentfault.com/img/bVm1c5) 105 | ![](https://segmentfault.com/img/bVm1c8) 106 | 107 | - `int epoll_create(int size)` 创建一个句柄`int epfd` 108 | - `int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)` 注册事件 `op 操作,增删改事件` `fd 待监测的链接套接字` `event 事件` 109 | - `int epoll_wait(int eqfd, struct epoll_event* events, int maxevents, int timeout)` 返回已发生的事件们 `events 已发生的事件们` 110 | 111 | ```c 112 | struct eventpoll { 113 | ... 114 | // 红黑树 储存epoll_ctl注册的事件 115 | // 添加的事件会与与设备驱动程序建立回调关系 116 | struct rb_root rbr; 117 | // 双向链表 存储epoll_wait返回已发生的事件们 118 | // 内核的ep_poll_callback会把发生的事件放在这里 119 | struct list_head rdllist; 120 | ... 121 | } 122 | ``` 123 | 124 | https://docs.huihoo.com/doxygen/linux/kernel/3.7/eventpoll_8c_source.html 125 | 126 | ``` 127 | // eventpoll->rbr里每一个事件都对应这个结构 128 | struct epitem { 129 | ... 130 | // 红黑树节点 131 | struct rb_node rbn; 132 | // 双向链表 133 | struct list_head rdllink; 134 | // 当前epitem指向的文件描述符 135 | struct epoll_filefd ffd; 136 | // 指向注册事件时的eventpoll的对象 137 | struct eventpoll *ep; 138 | // 当前fd注册的事件 139 | struct epoll_event event; 140 | ... 141 | }; 142 | 143 | // 红黑树节点 144 | struct rb_node 145 | { 146 | unsigned long rb_parent_color; 147 | #define RB_RED 0 148 | #define RB_BLACK 1 149 | struct rb_node *rb_right; 150 | struct rb_node *rb_left; 151 | } __attribute__((aligned(sizeof(long)))); 152 | 153 | // 红黑树根结点 154 | struct rb_root 155 | { 156 | struct rb_node *rb_node; 157 | }; 158 | 159 | // 事件epoll_event 160 | struct epoll_event { 161 | // 具体的事件 比如读、写等 162 | __u32 events; 163 | // 上下文 164 | __u64 data; 165 | } EPOLL_PACKED; 166 | ``` 167 | 168 | 169 | 客户端主动发起 ng被动接受的TCP连接 170 | 171 | ngx_connection_t 172 | 173 | ```list_head 174 | 而在linux内核中,list_head链表结构只包含指针域 175 | 176 | 无论是什么样的指针,它的大小都是一样的,32位的系统中,指针的大小都是32位(即4个字节),只是不同类型的指针在解释的时候不一样而已 177 | 178 | 偏移量 179 | 180 | struct list_head { 181 | struct list_head *next, *prev; 182 | }; 183 | ``` 184 | 185 | ```rb_root 186 | Linux内核中红黑树节点的定义如下,其中rb_node是节点类型,而rb_root是仅包含一个节点指针的类,用来表示根节点。 187 | 188 | struct rb_node 189 | { 190 | unsigned long rb_parent_color; 191 | #define RB_RED 0 192 | #define RB_BLACK 1 193 | struct rb_node *rb_right; 194 | struct rb_node *rb_left; 195 | } __attribute__((aligned(sizeof(long)))); 196 | 197 | struct rb_root 198 | { 199 | struct rb_node *rb_node; 200 | }; 201 | ``` 202 | 203 | 204 | 205 | 结果:一个进程或线程可以同时处理多个链接,i/o多路复用复用的是进程或者线程 206 | 207 | 协程 -> go 208 | 209 | 为什么Go的并发能力强? 210 | 211 | GMP 212 | 213 | ![](https://user-gold-cdn.xitu.io/2019/12/30/16f55033b4edcf10?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 214 | 215 | ```go 216 | func (srv *Server) Serve(l net.Listener) error { 217 | ... 218 | 219 | ctx := context.WithValue(baseCtx, ServerContextKey, srv) 220 | for { 221 | rw, e := l.Accept() 222 | 223 | ... 224 | 225 | c := srv.newConn(rw) 226 | c.setState(c.rwc, StateNew) 227 | go c.serve(connCtx) 228 | } 229 | } 230 | ``` 231 | 232 | 如何解决超售问题? -> 并发问题 233 | 234 | - 加锁 235 | - 串行化 236 | - 原子操作 237 | 238 | 原子操作高性能的原因? 239 | 240 | 轻量,基于CPU指令实现,CAS(Compare-and-Swap),即比较并替换,类似乐观锁 241 | 242 | 竞争条件是由于异步的访问共享资源,并试图同时读写该资源而导致的,使用互斥锁和通道的思路都是在线程获得到访问权后阻塞其他线程对共享内存的访问,而使用原子操作解决数据竞争问题则是利用了其不可被打断的特性。 https://juejin.im/post/5ee2066fe51d4578455f3caa 243 | 244 | 结论:Go + epoll + 原子操作 245 | 246 | # 业务代码 247 | 248 | ### 读操作 249 | 250 | 关于读,我们一般遵循如下优先级: 251 | 252 | 优先级|技术方案|说明|示例 253 | -------|-------|-------|------- 254 | 最高|尽可能静态化|对实时性要去不高的数据,尽可能全走CDN|例如获取基础商品信息 255 | 高|就近使用内存|优先级服务器内存、远程内存服务|例如秒杀、抢购库存(优先分配库存到服务器内存,其次远程内存服务<又涉及额外网络IO>) 256 | 极低|数据库(能不读就不要读)|连接池、sql优化|常见业务 257 | 258 | ### 写操作 259 | 260 | 关于写,我们一般会按照数据的一致性要求级别来看: 261 | 262 | 数据一致性要求|技术方案 263 | ------------|------------ 264 | 不高|先写内存(优先级从服务器内存到远程内存服务) 再异步储存 265 | 高|同步完成最关键的任务 异步保证其他任务最终成功 266 | 267 | 268 | ### 削峰限流 269 | 270 | 从简单到复杂: 271 | 272 | 简单程度|技术方案 273 | -------|------- 274 | 最简单|百分比流量拒绝 275 | 简单|原子操作限流(优先级使用服务器内存、其次远程内存服务) 276 | 稍麻烦|漏桶、令牌桶限流 277 | 麻烦|队列限流 278 | 279 | 280 | 一个简单的秒杀系统 281 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200501175532.png) 282 | 283 | 一个够用的秒杀系统 284 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200501183037.png) 285 | 286 | 性能再好点的秒杀系统 287 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200501200309.png) 288 | 289 | 支持动态伸缩容的秒杀系统 290 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200501200846.png) 291 | 292 | 公平的秒杀系统 293 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200502195413.png) 294 | 295 | ![](https://blog-1251019962.cos.ap-beijing.myqcloud.com/qiniu_img_2022/20200502200723.png) 296 | 297 | ``` 298 | +------------+ 299 | | 秒杀服务 | 300 | +------------+ 301 | 302 | +------------+ 303 | | 购物车服务 | 304 | +------------+ 305 | 306 | ``` -------------------------------------------------------------------------------- /material/TIGERB/marketing/seckill.md: -------------------------------------------------------------------------------- 1 | # [Skr-Shop]什么,秒杀系统也有这么多种! 2 | 3 | 4 | # 前言 5 | 6 | 本文结构很简单: 7 | 8 | > 5张图送你5种秒杀系统,再加点骚操作,再顺带些点心里话🤷‍♀️。 9 | 10 | 11 | ## 一个简单的秒杀系统 12 | 13 | **实现原理:** 通过redis原子操作减库存 14 | 15 | **图一** 16 |

17 | 18 | 19 | 20 |

21 | 22 | 优点|缺点 23 | ------------|------------ 24 | 简单好用|考验redis服务能力 25 | 26 | |是否公平| 27 | |-------| 28 | |公平| 29 | |先到先得| 30 | 31 | 我们称这类秒杀系统为: 32 | 33 | > 简单秒杀系统 34 | 35 | 如果刚开始QPS并不高,redis完全抗的下来的情况,完全可以依赖这个「简单秒杀系统」。 36 | 37 | ## 一个够用的秒杀系统 38 | 39 | **实现原理:** 服务内存限流算法 + redis原子操作减库存 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 | 74 |

75 | 76 | 优点|缺点 77 | ------------|------------ 78 | 高性能|不支持动态伸缩容(活动进行期间),因为库存是活动开始前分配好的 79 | 释放redis压力|- 80 | 81 | |是否公平| 82 | |-------| 83 | |不是很公平| 84 | |不是绝对的先到先得| 85 | 86 | 87 | 我们称这类秒杀系统为: 88 | 89 | > 预备库存秒杀系统 90 | 91 | ## 支持动态伸缩容的秒杀系统 92 | 93 | **实现原理:** 服务本地协程Coroutine**定时redis原子操作减部分库存**到本地内存 + 服务本地内存原子操作减库存 94 | 95 | **图四** 96 |

97 | 98 | 99 | 100 |

101 | 102 | 优点|缺点 103 | ------------|------------ 104 | 高性能|支持动态伸缩容(活动进行期间) 105 | 释放redis压力|- 106 | **具备通用性**|- 107 | 108 | |是否公平| 109 | |-------| 110 | |不是很公平,但是好了点| 111 | |几乎先到先得| 112 | 113 | 我们称这类秒杀系统为: 114 | 115 | > 实时预备库存秒杀系统 116 | 117 | ## 公平的秒杀系统 118 | 119 | **实现原理:** 服务本地Goroutine**定时同步是否售罄**到本地内存 + 队列 + 排队成功轮训(或主动Push)结果 120 | 121 | **图五** 122 |

123 | 124 | 125 | 126 |

127 | 128 | 优点|缺点 129 | ------------|------------ 130 | 高性能|开发成本高(需主动通知或轮训排队结果) 131 | 真公平|- 132 | **具备通用性**|- 133 | 134 | |是否公平| 135 | |-------| 136 | |很公平| 137 | |绝对的先到先得| 138 | 139 | 我们称这类秒杀系统为: 140 | 141 | > 公平排队秒杀系统 142 | 143 | ## 骚操作 144 | 145 | > 上面的秒杀系统还不够完美吗? 146 | 147 | 答案:是的。 148 | 149 | > 还有什么优化的空间? 150 | 151 | 答案:静态化获取秒杀活动信息的接口。 152 | 153 | > 静态化是什么意思? 154 | 155 | 答案:比如获取秒杀活动信息是通过接口 `https://seckill.skrshop.tech/v1/acticity/get` 获取的。现在呢,我们需要通过`https://static-api.skrshop.tech/seckill/v1/acticity/get` 这个接口获取。有什么区别呢?看下面: 156 | 157 | 服务名|接口|数据存储位置 158 | ------|------|------ 159 | 秒杀服务|https://seckill.skrshop.tech/v1/acticity/get|秒杀服务内存或redis等 160 | 接口静态化服务|https://static-api.skrshop.tech/seckill/v1/acticity/get|CDN、本地文件 161 | 162 | **以前是这样** 163 |

164 | 165 | 166 | 167 |

168 | 169 | **变成了这样** 170 |

171 | 172 | 173 | 174 |

175 | 176 | 结果:可以通过接口`https://static-api.skrshop.tech/seckill/v1/acticity/get`就获取到了秒杀活动信息,流量都分摊到了cdn,秒杀服务自身没了这部分的负载。 177 | 178 | > 小声点说:“秒杀结果我也敢推CDN😏😏😏。” 179 | 180 | ``` 181 | 备注: 182 | 之后我们会分享`如何用Golang设计一个好用的「接口静态化服务」`。 183 | ``` 184 | 185 | # 总结 186 | 187 | 上面我们得到了如下几类`秒杀系统` 188 | 189 | |秒杀系统| 190 | ------------| 191 | |简单秒杀系统| 192 | |够用秒杀系统| 193 | |预备库存秒杀系统| 194 | |实时预备库存秒杀系统| 195 | |公平排队秒杀系统| 196 | 197 | 我想说的是里面没有最好的方案,也没有最坏的方案,只有**适合你**的。 198 | 199 | 拿`先到先得`来说,一定要看你们的产品对外宣传,切勿上来就追逐绝对的先到先得。其实你看所有的方案,相对而言都是“先到先得”,比如,活动开始一个小时了你再来抢,那相对于准时的用户自然抢不过,对吧。 200 | 201 | 又如`预备库存秒杀系统`,虽然不支持动态伸缩容。但是如果你的环境满足如下任意条件,就完全够用了。 202 | 203 | - 秒杀场景结束时间之快,通常几秒就结束了,真实活动可能会发生如下情况: 204 | + 服务压力大还没挂:根本就来不及动态伸缩容 205 | + 服务压力大已经挂了:可以先暂停活动,服务起来&扩容结束,用剩余库存重新推送 206 | - 运维自身不具备动态伸缩容的能力 207 | 208 | 所以: 209 | 210 | > 合适好用就行,切勿过度设计。 211 | 212 | # 最后 213 | 214 | 这次算是把老本都吐露出来了,真是慌得一匹。 -------------------------------------------------------------------------------- /material/TIGERB/order/checkout.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 截止目前为止SkrShop《电商设计手册》系列梳理的内容已经涵盖了如下几大块: 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 | - 订单相关金额 74 | - 等等 75 | 76 | ## 订单结算页面的组成 77 | 78 | > 我一直在思考前端可以模块化,后端接口数据不可以模块化吗? 79 | 80 | ``` 81 | 我的答案:是可以的。 82 | ``` 83 | 84 | 我们依据上面整理的内容,再通过以往的经验把**订单结算页面**进行模块化拆分和组合,得到如下订单结算页面的**模块化构成**: 85 | 86 |

87 | 88 | 89 | 90 |

91 | 92 | 关于这块代码如何设计,可以参考我的文章[《代码组件 | 我的代码没有else》](http://tigerb.cn/go-patterns/#/?id=%e7%bb%84%e5%90%88%e6%a8%a1%e5%bc%8f) 93 | 94 | ## 订单结算页面各模块分析 95 | 96 | 模块编号|模块名称|子模块编号|子模块名称|模块描述 97 | ------------|------------|------------|------------|------------ 98 | 1|地址模块|-|-|展示用户最优地址 99 | 2|支付方式模块|-|-|该订单支持的支付方式 100 | 3|店铺模块|-|-|包含店铺信息、商品信息、参与的优惠信息、可选的物流方式、商品售后信息等 101 | 3|-|3.1|商品模块|包含子模块:商品基础信息模块、商品优惠信息模块、售后模块 102 | 3|-|3.2.1|商品基础信息模块|商品的信息,名称、图片、价格、库存等 103 | 3|-|3.2.2|商品优惠信息模块|选择的销售活动优惠选项 104 | 3|-|3.2.3|售后模块|商品享有的售后权益信息 105 | 3|-|3.3|物流模块|可选择的配送方式 106 | 3|-|3.4|店铺商品金额信息模块|- 107 | 4|发票模块|-|-|选择开发票的类型、补充发票信息 108 | 5|优惠券模块|-|-|展示该订单可以使用的优惠券列表 109 | 6|礼品卡模块|-|-|展示可以选择使用礼品卡列表 110 | 7|平台积分模块|-|-|用户可以使用积分抵掉部分现金 111 | 8|订单金额信息模块|-|-|包含该订单的金额明细 112 | 113 | ## 地址模块 114 | 115 | > 展示用户的最优地址 116 | 117 | 最优地址逻辑: 118 | 119 | - 首先,用户设置的默认地址 120 | - 如果没有默认地址,则返回最近下单的地址 121 | 122 | 字段名称|类型|下级字段名称|类型|字段含义 123 | ------|------|------|------|------ 124 | consignee|string|-|-|收货人姓名 125 | email|string|-|-|收货人邮箱(返回值用户名部分打码) 126 | mobile|string|-|-|收货人手机号(返回值中间四位打码) 127 | country|object|id|int64|国家ID 128 | country|object|name|string|国家名称 129 | province|object|id|int64|省ID 130 | province|object|name|string|省名称 131 | city|object|id|int64|市ID 132 | city|object|name|string|市名称 133 | county|object|id|int64|区县ID 134 | county|object|name|string|区县名称 135 | street|object|id|int64|街道乡镇ID 136 | street|object|name|string|街道乡镇名称 137 | detailed_address|string|-|-|详细地址(用户手填) 138 | postal_code|string|-|-|邮编 139 | address_id|int64|-|-|地址ID 140 | is_default|bool|-|-|是否是默认地址 141 | label|string|-|-|地址类型标签,家、公司等 142 | longitude|string|-|-|经度 143 | latitude|string|-|-|纬度 144 | 145 |

146 | 147 | 148 | 149 |

150 | 151 | 模块数据demo: 152 | ```json 153 | { 154 | "address_module": { 155 | "consignee": "收货人姓名", 156 | "email": "收货人邮箱(返回值用户名部分打码)", 157 | "mobile": "收货人手机号(返回值中间四位打码)", 158 | "country": { 159 | "id": 666, 160 | "name": "国家名称" 161 | }, 162 | "province": { 163 | "id": 12123, 164 | "name": "省名称" 165 | }, 166 | "city": { 167 | "id": 212333, 168 | "name": "市名称" 169 | }, 170 | "county": { 171 | "id": 1233222, 172 | "name": "区县名称" 173 | }, 174 | "street": { 175 | "id": 9989999, 176 | "name": "街道乡镇名称" 177 | }, 178 | "detailed_address": "详细地址(用户手填)", 179 | "postal_code": "邮编", 180 | "address_id": 212399999393, 181 | "is_default": false, 182 | "label": "地址类型标签,家、公司等", 183 | "longitude": "经度", 184 | "latitude": "纬度" 185 | } 186 | } 187 | ``` 188 | 189 | ## 支付方式模块 190 | 191 | > 该订单支持的支付方式 192 | 193 | 支付方式选项: 194 | 195 | - 在线支付 196 | - 货到付款 197 | 198 | 字段名称|类型|下级字段名称|类型|字段含义 199 | ------|------|------|------|------ 200 | pay_method_list|array|id|int|支付方式ID 201 | pay_method_list|array|name|string|支付方式名称 202 | pay_method_list|array|desc|string|支付方式描述 203 | 204 | 205 |

206 | 207 | 208 | 209 |

210 | 211 | 模块数据demo: 212 | ```json 213 | { 214 | "pay_method_module": { 215 | "pay_method_list": [ 216 | { 217 | "id": 1, 218 | "name": "在线支付", 219 | "desc": "在线支付的描述" 220 | }, 221 | { 222 | "id": 2, 223 | "name": "货到付款", 224 | "desc": "货到付款的描述" 225 | } 226 | ] 227 | } 228 | } 229 | ``` 230 | 231 | ## 店铺模块 232 | 233 | > 包含店铺信息、商品信息、参与的优惠信息、可选的物流方式、商品售后信息等 234 | 235 | 店铺模块由如下子模块组成: 236 | 237 | - 商品模块 238 | + 商品基础信息模块 239 | + 商品优惠信息模块 240 | + 售后模块 241 | - 商品物流模块 242 | - 店铺商品总金额信息模块 243 | 244 |

245 | 246 | 247 | 248 |

249 | 250 | 由于此处内容比较多我们之后再来单独分析。 251 | 252 | ## 发票模块 253 | 254 | > 用户选择开发票的类型以及补充发票信息 255 | 256 | 选择开发票的类型: 257 | 258 | - 个人 259 | - 单位 260 | 261 | 字段名称|类型|下级字段名称|类型|字段含义 262 | ------|------|------|------|------ 263 | type_id|int|-|-|发票类型:个人;单位 264 | type_name|string|-|-|发票类型名称 265 | type_desc|string|-|-|发票类型描述 266 | 267 | 268 |

269 | 270 | 271 | 272 |

273 | 274 | 模块数据demo: 275 | ```json 276 | { 277 | "invoice_module": { 278 | "type_list": [ 279 | { 280 | "type_id": 1, 281 | "type_name": "个人", 282 | "type_desc": "描述" 283 | }, 284 | { 285 | "type_id": 2, 286 | "type_name": "公司", 287 | "type_desc": "描述" 288 | } 289 | ] 290 | } 291 | } 292 | ``` 293 | 294 | ## 优惠券模块 295 | 296 | > 返回该订单可以使用的优惠券列表,以及默认选择对于当前订单而言的最优优惠券 297 | 298 | - 展示用户的优惠券列表:当前订单可用的排最前面其他放最后面 299 | - 默认选中最优优惠券:对于当前订单优惠力度最大的一张优惠券 300 | 301 | 关于优惠券的其他内容可以阅读优惠券章节内容。 302 | 303 | ## 礼品卡模块 304 | 305 | > 展示可以选择使用礼品卡列表 306 | 307 | 字段名称|类型|下级字段名称|类型|字段含义 308 | ------|------|------|------|------ 309 | giftcard_list|array|id|int64|礼品卡id 310 | giftcard_list|array|name|string|礼品卡名称 311 | giftcard_list|array|desc|string|礼品卡描述 312 | giftcard_list|array|pic_url|string|礼品卡图片 313 | giftcard_list|array|total_amount|float64|礼品卡初始总金额 314 | giftcard_list|array|total_amount_txt|string|礼品卡初始总金额-格式化后 315 | giftcard_list|array|remaining_amount|float64|礼品卡剩余金额 316 | giftcard_list|array|remaining_amount_txt|string|礼品卡剩余金额-格式化后 317 | 318 | 319 |

320 | 321 | 322 | 323 |

324 | 325 | 模块数据demo: 326 | ```json 327 | { 328 | "giftcard_module": { 329 | "giftcard_list": [ 330 | { 331 | "id": 341313121, 332 | "name": "礼品卡名称", 333 | "desc": "礼品卡描述", 334 | "pic_url": "礼品卡图片", 335 | "total_amount": 100.00, 336 | "total_amount_txt": "100.00", 337 | "remaining_amount": 21.00, 338 | "remaining_amount_txt": "21.00" 339 | } 340 | ] 341 | } 342 | } 343 | ``` 344 | 345 | ## 平台积分模块 346 | 347 | > 用户可以使用积分抵现 348 | 349 | 比如上线某东订单结算页面中的京豆。 350 | 351 | 字段名称|类型|下级字段名称|类型|字段含义 352 | ------|------|------|------|------ 353 | order_amount_min|float64|-|-|可使用积分抵现功能的订单金额下限 354 | total_points|int64|-|-|用户总积分 355 | can_use_points|int64|-|-|可使用的积分(可能存在冻结的积分) 356 | points2money_rate|int|-|-|积分转换为现金比率,比如每100积分抵1元,最低1积分抵0.01元 357 | points2money_min|int|-|-|用户最少满多少积分才可使用积分抵现 358 | points2money_max|int|-|-|单笔订单 最多可以使用积分的上限 359 | points_amount|float64|-|-|该订单积分可抵扣金额 360 | points_amount_txt|string|-|-|该订单积分可抵扣金额-格式化后 361 | 362 | 363 |

364 | 365 | 366 | 367 |

368 | 369 | 模块数据demo: 370 | ```json 371 | { 372 | "points_module": { 373 | "order_amount_min": 100.00, 374 | "total_points": 9999, 375 | "can_use_points": 9999, 376 | "points2money_rate": 100, 377 | "points2money_min": 1000, 378 | "points2money_max": 9999, 379 | "points_amount": 99.99, 380 | "points_amount_txt": "99.99" 381 | } 382 | } 383 | ``` 384 | 385 | ## 订单金额信息模块 386 | 387 | > 包含该订单的金额明细 388 | 389 | 字段名称|类型|下级字段名称|类型|字段含义 390 | ------|------|------|------|------ 391 | skus_amount|float64|-|-|商品的总金额 392 | promotion_amount|float64|-|-|优惠的总金额 393 | freight|float64|-|-|运费 394 | final_amount|float64|-|-|支付金额 395 | promotion_detail|object|coupon_amount|float64|优惠券优惠金额 396 | promotion_detail|object|sales_activity_amount|float64|销售活动优惠金额 397 | promotion_detail|object|giftcard_amount|float64|礼品卡使用金额 398 | promotion_detail|object|points_amount|float64|该订单积分抵扣金额 399 | 400 | ``` 401 | _txt字段略 402 | ``` 403 | 404 |

405 | 406 | 407 | 408 |

409 | 410 | 模块数据demo: 411 | ```json 412 | { 413 | "order_amount_module": { 414 | "skus_amount": 99.99, 415 | "skus_amount_txt": "99.99", 416 | "promotion_amount_total": 10.00, 417 | "promotion_amount_total_txt": "10.00", 418 | "freight_total": 8.00, 419 | "freight_total_txt": "8.00", 420 | "final_amount": 97.99, 421 | "final_amount_txt": "97.99", 422 | "promotion_detail": { 423 | "coupon_amount": 5.00, 424 | "coupon_amount_txt": "5.00", 425 | "sales_activity_amount": 5.00, 426 | "sales_activity_amount_txt": "5.00", 427 | "giftcard_amount": 0, 428 | "giftcard_activity_amount_txt": "0", 429 | "points_amount": 0, 430 | "points_amount_txt": "0" 431 | } 432 | } 433 | } 434 | ``` 435 | 436 | ## 结语 437 | 438 | 如上,订单结算页面的内容基本介绍完毕了,有任何问题随时到我们的github项目下留言 。 439 | 440 | 441 | ``` 442 | 关于我的常用画图软件: 443 | 444 | 1. Balsamiq Mockups 3 445 | 2. Processon 446 | ``` -------------------------------------------------------------------------------- /material/TIGERB/order/order-status.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ``` 8 | 代付款(0) 已关闭(-1) 9 | 10 | 已支付(1) 退款中(-2) 已关闭(未妥投的订单取消)(-1) 11 | 12 | 已完成(2) 13 | 14 | 已锁定(异常订单)(-3) 15 | ``` 16 | 17 | 18 | 19 | ``` 20 | 待发货(1) 待收货(2) 已妥投(3) 21 | 22 | 已锁定(异常发货单)(-3) 23 | 24 | 退款中(-2) 25 | 26 | 已关闭(-1) 27 | ``` -------------------------------------------------------------------------------- /material/TIGERB/order/pay.md: -------------------------------------------------------------------------------- 1 | # 收银台 -------------------------------------------------------------------------------- /material/TIGERB/order/submit.md: -------------------------------------------------------------------------------- 1 | # 创建订单 -------------------------------------------------------------------------------- /material/TIGERB/product.md: -------------------------------------------------------------------------------- 1 | 2 | # 商品数据模型 3 | 4 | ```sql 5 | 6 | -- 品牌表 product_brands 7 | CREATE TABLE `product_brands` ( 8 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '品牌ID', 9 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '品牌名称', 10 | `desc` varchar(255) NOT NULL DEFAULT '' COMMENT '品牌描述', 11 | `logo_url` varchar(255) NOT NULL DEFAULT '' COMMENT '品牌logo图片', 12 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 13 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 14 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 15 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 16 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 17 | PRIMARY KEY (`id`) 18 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品牌表'; 19 | 20 | -- 类别表 product_category 21 | CREATE TABLE `product_category` ( 22 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '分类ID', 23 | `pid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父ID', 24 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '分类名称', 25 | `desc` varchar(255) NOT NULL DEFAULT '' COMMENT '分类描述', 26 | `pic_url` varchar(255) NOT NULL DEFAULT '' COMMENT '分类图片', 27 | `path` varchar(255) NOT NULL DEFAULT '' COMMENT '分类地址{pid}-{child_id}-...', 28 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 29 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 30 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 31 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 32 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 33 | PRIMARY KEY (`id`) 34 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='类别表'; 35 | 36 | -- spu表 product_spu 37 | -- spu: standard product unit 标准产品单位 38 | CREATE TABLE `product_spu` ( 39 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'SPU ID', 40 | `brand_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '品牌ID', 41 | `category_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '分类ID', 42 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT 'spu名称', 43 | `desc` varchar(255) NOT NULL DEFAULT '' COMMENT 'spu描述', 44 | `selling_point` varchar(255) NOT NULL DEFAULT '' COMMENT '卖点', 45 | `unit` varchar(255) NOT NULL DEFAULT '' COMMENT 'spu单位', 46 | `banner_url` text COMMENT 'banner图片 多个图片逗号分隔', 47 | `main_url` text COMMENT '商品介绍主图 多个图片逗号分隔', 48 | `price_fee` int unsigned NOT NULL DEFAULT 0 COMMENT '售价,整数方式保存', 49 | `price_scale` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '售价,金额对应的小数位数', 50 | `market_price_fee` int unsigned NOT NULL DEFAULT 0 COMMENT '市场价,整数方式保存', 51 | `market_price_scale` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '市场价,金额对应的小数位数', 52 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 53 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 54 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 55 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 56 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 57 | PRIMARY KEY (`id`) 58 | ) ENGINE=InnoDB DEFAULT AUTO_INCREMENT=666666 CHARSET=utf8mb4 COMMENT='spu表'; 59 | 60 | -- sku表 product_sku 61 | -- sku: stock keeping unit 库存量单位 62 | CREATE TABLE `product_sku` ( 63 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'SKU ID', 64 | `spu_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'SPU ID', 65 | `attrs` text COMMENT '销售属性值{attr_value_id}-{attr_value_id} 多个销售属性值逗号分隔', 66 | `banner_url` text COMMENT 'banner图片 多个图片逗号分隔', 67 | `main_url` text COMMENT '商品介绍主图 多个图片逗号分隔', 68 | `price_fee` int unsigned NOT NULL DEFAULT 0 COMMENT '售价,整数方式保存', 69 | `price_scale` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '售价,金额对应的小数位数', 70 | `market_price_fee` int unsigned NOT NULL DEFAULT 0 COMMENT '市场价,整数方式保存', 71 | `market_price_scale` tinyint unsigned NOT NULL DEFAULT 0 COMMENT '市场价,金额对应的小数位数', 72 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 73 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 74 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 75 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 76 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 77 | PRIMARY KEY (`id`) 78 | ) ENGINE=InnoDB DEFAULT AUTO_INCREMENT=666666 CHARSET=utf8mb4 COMMENT='sku表'; 79 | 80 | -- 销售属性表 product_attr 81 | CREATE TABLE `product_attr` ( 82 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '销售属性ID', 83 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '销售属性名称', 84 | `desc` varchar(255) NOT NULL DEFAULT '' COMMENT '销售属性描述', 85 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 86 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 87 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 88 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 89 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 90 | PRIMARY KEY (`id`) 91 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售属性表'; 92 | 93 | -- 销售属性值 product_attr_value 94 | CREATE TABLE `product_attr_value` ( 95 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '销售属性值ID', 96 | `attr_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '销售属性ID', 97 | `value` varchar(255) NOT NULL DEFAULT '' COMMENT '销售属性值', 98 | `desc` varchar(255) NOT NULL DEFAULT '' COMMENT '销售属性值描述', 99 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 100 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 101 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 102 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 103 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 104 | PRIMARY KEY (`id`) 105 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售属性值'; 106 | 107 | -- 关联关系冗余表 product_spu_sku_attr_map 108 | -- 1. spu下 有哪些sku 109 | -- 2. spu下 有那些销售属性 110 | -- 3. spu下 每个销售属性对应的销售属性值(一对多) 111 | -- 4. spu下 每个销售属性值对应的sku(一对多) 112 | CREATE TABLE `product_spu_sku_attr_map` ( 113 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 114 | `spu_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'SPU ID', 115 | `sku_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'SKU ID', 116 | `attr_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '销售属性ID', 117 | `attr_name` varchar(255) NOT NULL DEFAULT '0' COMMENT '销售属性名称', 118 | `attr_value_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '销售属性值ID', 119 | `attr_value_name` varchar(255) NOT NULL DEFAULT '0' COMMENT '销售属性值', 120 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 121 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 122 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 123 | PRIMARY KEY (`id`) 124 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='关联关系冗余表'; 125 | 126 | -- sku库存表 product_sku_stock 127 | CREATE TABLE `product_sku_stock` ( 128 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 129 | `sku_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'SKU ID', 130 | `quantity` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '库存', 131 | `quantity_lock` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '锁定库存', 132 | `quantity_over` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '超卖库存 0:严格不准超卖', 133 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 134 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 135 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 136 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 137 | `status` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 138 | PRIMARY KEY (`id`) 139 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='sku库存表'; 140 | 141 | ``` 142 | 143 | ``` 144 | 仓储 --(incre sku quantity)push/pull--> update `product_sku_stock` & incr redis 145 | 146 | 回写脚本 ----> select `product_sku_stock`.`quantity` & update 147 | ``` 148 | 149 | # API 150 | 151 | 1. spu详情 GET {version}/product/spu/{spu_id} 152 | 153 | 请求参数: 154 | 155 | 字段|类型|是否必传|描述 156 | ------------|------------|------------|------------ 157 | spu_id|number|yes|spu ID 158 | 159 | 响应内容: 160 | ```json 161 | { 162 | "code": "200", 163 | "msg": "OK", 164 | "result": { 165 | "brand_info": { 166 | "id": "number, 品牌ID", 167 | "name": "string, 品牌名称", 168 | "desc": "string, 品牌描述", 169 | "logo_url": "string, 品牌logo图片", 170 | }, 171 | "category_info": { 172 | "id": "number, 分类ID", 173 | "name": "string, 品牌名称", 174 | "desc": "string, 品牌描述", 175 | "pic_url": "string, 分类图片", 176 | "path": "string, 分类地址{pid}-{child_id}-...", 177 | }, 178 | "spu_info": { 179 | "id": "number, spu id", 180 | "name": "string, spu名称", 181 | "desc": "string, spu描述", 182 | "selling_point": "string, 卖点", 183 | "unit": "string, spu单位", 184 | "banner_url": [ 185 | "string, banner 图片url", 186 | "string, banner 图片url", 187 | ], 188 | "main_url": [ 189 | "string, 商品介绍主图 图片url", 190 | "string, 商品介绍主图 图片url", 191 | ], 192 | "price": "string, 售价", 193 | "market_price": "string, 市场价", 194 | "attrs": [ // 有那些销售属性 195 | { 196 | "id": "销售属性ID", 197 | "name": "string, 销售属性名称", 198 | "desc": "string, 销售属性描述", 199 | "values": [ // 每个销售属性对应的销售属性值(一对多) 200 | { 201 | "id": "销售属性值ID", 202 | "name": "string, 销售属性值", 203 | "desc": "string, 销售属性值描述", 204 | // 每个销售属性值对应的sku(一对多) 205 | // 页面初始化时,按钮不可点击逻辑判断: 如果该销售属性值下所有sku没有库存,则该销售属性按钮不可点击 206 | // 选择销售属性值时,按钮不可点击逻辑判断:销售属性构成双向链表,每个销售属性又是一个单向链表存改销售属性对应的所有销售属性值。每当选择一个销售属性值时先前和后一个销售属性遍历,执销售属性值下所有sku售罄的按钮不可点击,且当前销售属性值map记录key为当前点击的销售属性值ID,值统一标示一下就行,目的记录是由于选择了哪个销售属性值使得当前的销售属性值为售罄状态 207 | // 取消选择销售属性值时,按钮不可点击逻辑恢复判断:数据结构同上,遍历,记录的map删除key为当前取消选中的销售属性值,并判断是否还有别的key使得该销售属性值为售罄状态,如果没有则恢复未售罄状态 208 | "skus": [ 209 | "number, sku id", 210 | "number, sku id", 211 | ], 212 | } 213 | ], 214 | } 215 | ], 216 | "skus": [ // 有哪些sku 217 | "number, sku id", 218 | "number, sku id", 219 | ], 220 | "skus_map": { 221 | "{attr_value_id}-{attr_value_id}-...": "number, sku id", 222 | "{attr_value_id}-{attr_value_id}-...": "number, sku id", 223 | "{attr_value_id}-{attr_value_id}-...": "number, sku id", 224 | "{attr_value_id}-{attr_value_id}-...": "number, sku id", 225 | "{attr_value_id}-{attr_value_id}-...": "number, sku id", 226 | "{attr_value_id}-{attr_value_id}-...": "number, sku id", 227 | } 228 | } 229 | } 230 | } 231 | ``` 232 | 233 | 2. 获取spu下所有skus库存 GET {version}/stock/spu/{spu_id} 234 | 235 | 请求参数: 236 | 237 | 字段|类型|是否必传|描述 238 | ------------|------------|------------|------------ 239 | spu_id|number|yes|spu ID 240 | 241 | 响应内容: 242 | ```json 243 | { 244 | "code": "200", 245 | "msg": "OK", 246 | "result": { 247 | "skus_stock": { 248 | "int, sku id": { 249 | "quantity": "int, 剩余库存数量" 250 | } 251 | } 252 | } 253 | } 254 | } 255 | ``` 256 | 257 | 3. sku详情 GET {version}/product/sku/{sku_id} 258 | 259 | 请求参数: 260 | 261 | 字段|类型|是否必传|描述 262 | ------------|------------|------------|------------ 263 | sku|number|yes|sku ID 264 | 265 | 响应内容: 266 | ```json 267 | { 268 | "code": "200", 269 | "msg": "OK", 270 | "result": { 271 | "id": "number, sku id", 272 | "name": "string, sku名称", 273 | "desc": "string, sku描述", 274 | "unit": "string, sku单位", 275 | "banner_url": [ 276 | "string, banner 图片url", 277 | "string, banner 图片url", 278 | ], 279 | "main_url": [ 280 | "string, 商品介绍主图 图片url", 281 | "string, 商品介绍主图 图片url", 282 | ], 283 | "price": "string, 售价", 284 | "market_price": "string, 市场价", 285 | } 286 | } 287 | ``` 288 | 289 | 4. spu列表 GET {version}/product/spu/list 290 | 291 | 请求参数: 292 | 293 | 字段|类型|是否必传|描述 294 | ------------|------------|------------|------------ 295 | 296 | 响应内容: 297 | ```json 298 | { 299 | "code": "200", 300 | "msg": "OK", 301 | "result": { 302 | "list": [ 303 | { 304 | "id": "number, spu id", 305 | "name": "string, spu名称", 306 | "desc": "string, spu描述", 307 | "unit": "string, spu单位", 308 | "banner_url": [ 309 | "string, banner 图片url", 310 | "string, banner 图片url", 311 | ], 312 | "price": "string, 售价", 313 | "market_price": "string, 市场价", 314 | } 315 | ] 316 | } 317 | } 318 | ``` 319 | -------------------------------------------------------------------------------- /material/Veaer/markering/coupon-v3.md: -------------------------------------------------------------------------------- 1 | # 优惠券服务 2 | 3 | ## 前言 4 | 5 | 进入正题,营销体系的基础服务「优惠券服务」。通过如下问题来介绍优惠券: 6 | 7 | - 优惠券有**哪些类型**? 8 | - 优惠券有**哪些适用范围**? 9 | - 优惠券有**哪些常见的场景**? 10 | - 优惠券本身应该有**哪些状态**? 11 | - 优惠券服务要有**哪些服务能力**? 12 | - 优惠券服务的**风控**怎么做? 13 | 14 | ## 优惠券有哪些类型? 15 | 16 | 对于获取优惠券的用户而言,关注的是优惠券的优惠能力,所以按优惠能力维度优惠券主要分为下面三类: 17 | 18 | 优惠能力维度|描述 19 | ------------|------------ 20 | 满减券|满多少金额(不含邮费)可以减多少金额 21 | 现金券|抵扣多少现金(无门槛) 22 | 抵扣券|抵扣某Sku全部金额(一个数量) 23 | 折扣券|打折 24 | 25 | 对于发放优惠券的运营人员而言: 26 | 27 | 一种是「**固定有效期**」,优惠券的生效时间戳和过期时间戳,在创建优惠券的时候已经确定。用户在任意时间领取该券,该券的有效时间都是之前设置的有效时间的开始结束时间。 28 | 29 | 另一种是「**动态有效期**」,创建优惠券设置的是有效时间段,比如7天有效时间、12小时有效时间等。这类优惠券以用户领取优惠券的时间为优惠券的有效时间的开始时间,以以用户领取优惠券的时间+有效时间为有效时间的结束时间。 30 | 31 | 有效期维度|优惠券类型|优惠券生效时间|优惠券失效时间|描述 32 | ------------|------------|------------|------------|------------ 33 | 固定|固定有效期|优惠券类型被创建时已确定|优惠券类型被创建时已确定|无论用户什么时间领取该优惠券,优惠券生效的时间都是设置好的统一时间 34 | 动态|动态有效期|用户领取优惠券时,当前时间戳|用户领取优惠券时,当前时间戳 + N\*24\*60\*60|优惠券类型被创建时,只确定了该优惠券的有效,例如6小时、7天、一个月 35 | 36 | 小结如下: 37 | 38 |

39 | 40 | 41 | 42 |

43 | 44 | 45 | ## 优惠券有哪些适用范围? 46 | 47 | ### 运营策略 48 | 49 | 限制纬度|限制细节|描述 50 | ------------|------------|------------ 51 | 商品纬度|| 52 | |(非)指定Sku|Sku券 53 | |(非)指定Spu|Spu券 54 | |(非)指定类别|类别券 55 | 平台纬度|| 56 | |指定店铺|店铺券 57 | |全场通用|平台券 58 | 59 | ### 适用终端 60 | 61 | 适用终端(复选框)|描述 62 | ------------|------------ 63 | Android|安卓端 64 | iOS|iOS端 65 | PC|网页电脑端 66 | Mobile|网页手机端 67 | Wechat|微信端 68 | 微信小程序|微信小程序 69 | All|以上所有 70 | 71 | ### 支付方式 72 | 73 | 支付类型|描述 74 | ------------|------------ 75 | 货到付款| 76 | 在线支付|支付宝/微信/银联 77 | All| 78 | 79 | ### 适用人群 80 | 81 | 适用人群|描述 82 | ------------|------------ 83 | 白名单|测试用户 84 | 会员|会员专属 85 | 86 | 小结如下: 87 | 88 |

89 | 90 | 91 | 92 |

93 | 94 | 95 | 96 | 97 | ## 优惠券有哪些常见的场景? 98 | 99 | ### 领取优惠券场景 100 | 101 | 领取优惠券场景|描述 102 | ------------|------------ 103 | 活动页面|大促、节假日活动页面展示获取优惠券的按钮 104 | 游戏页面|通过游戏获取优惠券 105 | 店铺首页|店铺首页展示领券入口 106 | 商品详情|商品详情页面展示领券入口 107 | 积分中心|积分兑换优惠券 108 | 109 | ### 展示优惠券场景 110 | 111 | 展示优惠券场景|描述 112 | ------------|------------ 113 | 活动页面|大促、节假日活动页面展示可以领取的优惠券 114 | 商品详情|商品详情页面展示可以领取、可以使用的优惠券列表 115 | 个人中心-我的优惠券|我的优惠券列表 116 | 订单结算页面|结算页面,适用该订单的优惠券列表以及推荐 117 | 积分中心|展示可以兑换的优惠券详情 118 | 119 | 120 | ### 选择优惠券场景 121 | 122 | 选择优惠券场景|描述 123 | ------------|------------ 124 | 商品详情|商品详情页面展示该用户已有的,且适用于该商品的优惠券 125 | 订单结算页面-优惠券列表|选择可用优惠券结算 126 | 订单结算页面-输入优惠码|输入优惠码结算 127 | 128 | ### 返还优惠券场景 129 | 130 | 返还优惠券场景|描述 131 | ------------|------------ 132 | 未支付订单取消|未支付的订单,用户主动取消返还优惠券,或超时关单返还优惠券 133 | 已支付订单全款取消|已支付(未收货)的订单,订单部分退款不返还,当整个订单全部退款返还优惠券 134 | 135 | ### 优惠券获取场景示例 136 | 137 | 场景示例|描述 138 | ------------|------------ 139 | 活动页领券|大促、节假日活动页面展示获取优惠券的按钮 140 | 游戏发券|游戏奖励 141 | 商品页领券|- 142 | 店铺页领券|- 143 | 购物返券|购买某个Sku,订单妥投后发放优惠券 144 | 新用户发券|新用户注册发放优惠券 145 | 积分兑券|积分换取优惠券 146 | 评论返券|用户订单收货之后,添加评论(审核后),给用户返回一张指定优惠券 147 | 148 | 小结如下: 149 | 150 |

151 | 152 | 153 | 154 |

155 | 156 | 157 | ## 优惠券本身应该有哪些状态? 158 | 159 |

160 | 161 | 162 | 163 |

164 | 165 | 166 | ## 优惠券服务要有哪些服务能力? 167 | 168 | ### 服务能力1: 发放优惠券 169 | 170 | 发放方式|描述 171 | ------------|------------ 172 | 同步发放|适用于用户点击领券等实时性要求较高的获取券场景 173 | 异步发放|适用于实时性要求不高的发放券场景,比如新用户注册发券等场景 174 | 175 | 发放能力|描述 176 | ------------|------------ 177 | 单张发放|指定一个优惠券类型ID,且指定一个UID只发一张该券 178 | 批量发放|指定一个优惠券类型ID,且指定一批UID,每个UID只发一张该券 179 | 180 | 发放类型|描述 181 | ------------|------------ 182 | 优惠券类型标识|通过该优惠券类型的身份标识发放,比如创建一个优惠券类型时会生成一个16位标识码,用户通过`16位标识码`领取优惠券;这里不使用自增ID(避免对外泄露历史创建了的优惠券数量) 183 | 优惠码code|创建一个优惠券类型时,运营人员会给该券填写一个6位左右的Ascall码,比如`SKR6a6`,用户通过该码领取优惠券 184 | 185 | ### 服务能力2: 后台撤销优惠券 186 | 187 | 撤销能力|描述 188 | ------------|------------ 189 | 单张撤销|指定一个优惠券ID,且指定一个UID,校验归属是否合法,之后撤销一张券 190 | 批量撤销|指定一个优惠券类型ID,且指定一批UID,每个UID撤销一张该券 191 | 192 | ### 服务能力3: 伴随订单系统进行状态变更 193 | 194 | | 变更类型 | 描述 | 195 | | -------- | ------------------------------------ | 196 | | 冻结 | 成单时对优惠券进行订单绑定占用 | 197 | | 解冻 | 用户未支付前取消订单,解冻归还优惠券 | 198 | | 核销 | 将已经冻结的优惠券标记为已使用 | 199 | 200 | | 变更能力 | 描述 | 201 | | ------------------ | ---------------------------------------------------- | 202 | | 单张冻结/解冻/核销 | 指定一个优惠券ID,一个订单 ID,更改优惠券状态 | 203 | | 批量冻结/解冻/核销 | 指定一个优惠券ID与订单 ID 的映射,进行优惠券状态变更 | 204 | 205 | ### 服务能力4: 列表查询优惠券 206 | 207 | 用户优惠券列表|子类|描述 208 | ------------|------------|------------ 209 | 全部|-|查询该用户所有的优惠券 210 | 可以使用|全部|查询该用户所有可以使用的优惠券 211 | -|适用于某个spu或sku|查询该用户适用于某个spu或sku可以使用的优惠券 212 | -|适用于某个类别|查询该用户适用于某个类别可以使用的优惠券 213 | -|适用于某个店铺|查询该用户适用于某个店铺可以使用的优惠券 214 | 已用|全部|查询该用户所有已经使用的优惠券 215 | -|正常使用|查询该用户所有履约完成的订单中使用的优惠券 216 | -|订单冻结|查询该用户所有未关闭订单中被冻结的优惠券 217 | 无效|全部|查询该用户拥有,但是不可用的优惠券 218 | -|过期|查询该用户所有过期的优惠券 219 | -|失效|查询该用户所有后台冻结的优惠券 220 | 221 | ### 服务能力5: 条件查询优惠券 222 | 223 | 有两种场景的条件查询 224 | 225 | 1. 商品/店铺展示页面可以展示此商品/店铺 已经领取/可以领取的券的集合 226 | 2. 下单时根据商品/店铺/支付方式过滤出当前可用的优惠券集合 227 | 228 | 注:活动页券集合一般会搭配相应的运营系统自定义,不在此列 229 | 230 | ### 服务能力6: 结算页优惠券推荐 231 | 232 | 订单结算页面推荐一张最适合(金额)该订单的优惠券 233 | 234 | 小结如下: 235 | 236 |

237 | 238 | 239 | 240 |

241 | 242 | 243 | ## 优惠券服务的风控怎么做? 244 | 245 | 一旦有发生风险的可能则触发风控: 246 | 247 | - 对用户,提示稍后再试或联系客服 248 | - 对内部,报警提示,核查校验报警是否存在问题 249 | 250 | ### 频率限制 251 | 252 | 领取|描述 253 | ------------|------------ 254 | 设备ID|每天领取某优惠券的个数限制 255 | UID|每天领取某优惠券的个数限制 256 | IP|每天领取某优惠券的个数限制 257 | 258 | 使用|描述 259 | ------------|------------ 260 | 设备ID|每天使用某优惠券的个数限制 261 | UID|每天使用某优惠券的个数限制 262 | IP|每天使用某优惠券的个数限制 263 | 手机号|每天使用某优惠券的个数限制 264 | 邮编|比如注重邮编的海外地区,每天使用某优惠券的个数限制 265 | 266 | ### 用户风险等级 267 | 268 | 依托用户历史订单数据,得到用户成功完成交易(比如成功妥投15天+)的比率,根据此比率对用户进行等级划分,高等级进入通行Unblock名单,低等级进入Block名单,根据不同用户级别设置限制策略。等其他大数据分析手段。 269 | 270 | ### 阈值 271 | 272 | - 发券预算 273 | - 实际使用券预算 274 | 275 | 根据预算值设置发券总数阈值,当触发阈值时阻断并报警。 276 | 277 | ### 优惠券不要支持虚拟商品 278 | 279 | 优惠券尽量不要支持虚拟商品以防止可能被利用的不法活动。 280 | 281 |

282 | 283 | 284 | 285 |

286 | 287 | -------------------------------------------------------------------------------- /material/skr-shop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skr-shop/manuals/a88818b73454f8ccc2e9c7fa1c02205e936cfd21/material/skr-shop.png -------------------------------------------------------------------------------- /records/record-20180810.md: -------------------------------------------------------------------------------- 1 | ### 技术栈选型 - 2018.08.10 2 | 3 | - 基础环境 4 | + k8s 5 | + docker 6 | - 存储 7 | + mysql 8 | + redis 9 | * codis 10 | * redis主从 11 | - queue 12 | + kafka 13 | + rocketmq 14 | + rabbitmq 15 | - gw 16 | + kong 17 | + zuul 18 | - webserver 19 | + nginx/openresty 20 | + envoy 21 | - server 22 | + go 23 | + php 24 | - frontend 25 | + vue 26 | - rpc 27 | + grpc 28 | + thrift 29 | - 基础能力 30 | + 监控 31 | * zipkin 32 | * elk 33 | * falcon 34 | + 服务发现 35 | * zookeeper 36 | * etcd 37 | + 持续集成 38 | * ci/cd 39 | - 搜索 40 | + es 41 | + solr 42 | 43 | --- 44 | 45 | ### 备注 46 | 47 | - 熔断: 涉及远程调用过程 48 | - 降级: 不涉及远程调用过程 49 | - 限流: -------------------------------------------------------------------------------- /records/record-20180817.md: -------------------------------------------------------------------------------- 1 | ### 用户模型 - 2018.08.15 2 | 3 | 4 | 5 | 6 | 7 | **账户模型** 8 | 9 | ```sql 10 | 11 | -- 账户模型 12 | CREATE TABLE `account_user` ( 13 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '账号id', 14 | `email` varchar(30) NOT NULL DEFAULT '' COMMENT '邮箱', 15 | `phone` varchar(15) NOT NULL DEFAULT '' COMMENT '手机号', 16 | `username` varchar(30) NOT NULL DEFAULT '' COMMENT '用户名', 17 | `password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码', 18 | `create_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建时间', 19 | `create_ip_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建ip', 20 | `last_login_at` varchar(12) NOT NULL DEFAULT '' COMMENT '最后一次登陆时间', 21 | `last_login_ip_at` varchar(12) NOT NULL DEFAULT '' COMMENT '最后一次登陆ip', 22 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 23 | PRIMARY KEY (`id`), 24 | KEY `idx_email` (`email`), 25 | KEY `idx_username` (`username`) 26 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户'; 27 | 28 | -- 第三方账户 29 | CREATE TABLE `account_platform` ( 30 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 31 | `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '账号id', 32 | `platform_id` varchar(60) NOT NULL DEFAULT '' COMMENT '平台id', 33 | `type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '平台类型 0:未知,1:facebook,2:google,3:wechat,4:qq,5:weibo,6:twitter', 34 | `nickname` varchar(60) NOT NULL DEFAULT '' COMMENT '昵称', 35 | `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '头像', 36 | PRIMARY KEY (`id`), 37 | KEY `idx_uid` (`uid`), 38 | KEY `idx_platform_id` (`platform_id`) 39 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方用户信息'; 40 | ``` 41 | 42 | **用户模型** 43 | 44 | ```sql 45 | 46 | -- 用户模型 47 | CREATE TABLE `skr_member` ( 48 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户id', 49 | `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '账号id', 50 | `nickname` varchar(30) NOT NULL DEFAULT '' COMMENT '昵称', 51 | `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '头像(相对路径)', 52 | `gender` enum('male','female','unknow') NOT NULL DEFAULT 'unknow' COMMENT '性别', 53 | `role` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '角色 0:普通用户 1:vip', 54 | PRIMARY KEY (`id`), 55 | KEY `idx_uid` (`user_id`) 56 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户信息'; 57 | ``` 58 | 59 | **员工模型** 60 | 61 | ```sql 62 | 63 | -- 员工表 64 | CREATE TABLE `staff_info` ( 65 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '员工id', 66 | `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '账号id', 67 | `email` varchar(30) NOT NULL DEFAULT '' COMMENT '员工邮箱', 68 | `phone` varchar(15) NOT NULL DEFAULT '' COMMENT '员工手机号', 69 | `name` varchar(30) NOT NULL DEFAULT '' COMMENT '员工姓名', 70 | `nickname` varchar(30) NOT NULL DEFAULT '' COMMENT '员工昵称', 71 | `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '员工头像(相对路径)', 72 | `gender` enum('male','female','unknow') NOT NULL DEFAULT 'unknow' COMMENT '员工性别', 73 | `create_at` varchar(12) NOT NULL DEFAULT '' COMMENT '入职时间', 74 | PRIMARY KEY (`id`), 75 | KEY `idx_uid` (`uid`), 76 | KEY `idx_email` (`email`), 77 | KEY `idx_phone` (`phone`) 78 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息(这里列了大概的信息,多的可以垂直拆表)'; 79 | 80 | ``` 81 | 82 | **系统权限管理模型** 83 | 84 | ```sql 85 | 86 | -- 权限管理: 系统map 87 | CREATE TABLE `auth_ms` ( 88 | `id` smallint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 89 | `ms_name` varchar(255) NOT NULL DEFAULT '0' COMMENT '系统名称', 90 | `ms_desc` varchar(255) NOT NULL DEFAULT '0' COMMENT '系统描述', 91 | `ms_domain` varchar(255) NOT NULL DEFAULT '0' COMMENT '系统域名', 92 | `create_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建时间', 93 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 94 | `update_at` varchar(12) NOT NULL DEFAULT '' COMMENT '更新时间', 95 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 96 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 97 | PRIMARY KEY (`id`) 98 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统map(登记目前存在的后台系统信息)'; 99 | 100 | -- 权限管理: 系统menu 101 | CREATE TABLE `auth_ms_menu` ( 102 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 103 | `ms_id` smallint(11) unsigned NOT NULL DEFAULT '0' COMMENT '系统id', 104 | `parent_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父菜单id', 105 | `menu_name` varchar(255) NOT NULL DEFAULT '0' COMMENT '菜单名称', 106 | `menu_desc` varchar(255) NOT NULL DEFAULT '0' COMMENT '菜单描述', 107 | `menu_uri` varchar(255) NOT NULL DEFAULT '0' COMMENT '菜单uri', 108 | `create_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建时间', 109 | `is_show` enum('yes','no') NOT NULL DEFAULT 'no' COMMENT '是否展示菜单', 110 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 111 | `update_at` varchar(12) NOT NULL DEFAULT '' COMMENT '更新时间', 112 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 113 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 114 | PRIMARY KEY (`id`) 115 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统menu'; 116 | 117 | -- 权限管理: 系统权限 118 | CREATE TABLE `auth_item` ( 119 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 120 | `ms_id` tinyint(11) unsigned NOT NULL DEFAULT '0' COMMENT '系统id', 121 | `menu_id` varchar(255) NOT NULL DEFAULT '0' COMMENT '页面/接口uri', 122 | `create_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建时间', 123 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 124 | `update_at` varchar(12) NOT NULL DEFAULT '' COMMENT '更新时间', 125 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 126 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 127 | PRIMARY KEY (`id`) 128 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统权限'; 129 | 130 | -- 权限管理: 系统权限(权限的各个集合) 131 | CREATE TABLE `auth_role` ( 132 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 133 | `name` varchar(255) NOT NULL DEFAULT '0' COMMENT '角色名称', 134 | `desc` varchar(255) NOT NULL DEFAULT '0' COMMENT '角色描述', 135 | `auth_item_set` text COMMENT '权限集合 多个值,号隔开', 136 | `create_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建时间', 137 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 138 | `update_at` varchar(12) NOT NULL DEFAULT '' COMMENT '更新时间', 139 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 140 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 141 | PRIMARY KEY (`id`) 142 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工角色'; 143 | 144 | -- 权限管理: 角色与员工关系 145 | CREATE TABLE `auth_role_staff` ( 146 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 147 | `staff_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '员工id', 148 | `role_set` text COMMENT '角色集合 多个值,号隔开', 149 | `create_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建时间', 150 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 151 | `update_at` varchar(12) NOT NULL DEFAULT '' COMMENT '更新时间', 152 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 153 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 154 | PRIMARY KEY (`id`), 155 | KEY `idx_staff_id` (`staff_id`), 156 | KEY `idx_role_id` (`role_id`) 157 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限角色与员工关系'; 158 | 159 | ``` 160 | -------------------------------------------------------------------------------- /records/record-20180830.md: -------------------------------------------------------------------------------- 1 | ### 用户模型 - 2018.08.30 2 | 3 | **账户模型** 4 | 5 | ```sql 6 | 7 | -- 账户模型 8 | CREATE TABLE `account_user` ( 9 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '账号id', 10 | `email` varchar(30) NOT NULL DEFAULT '' COMMENT '邮箱', 11 | `phone` varchar(15) NOT NULL DEFAULT '' COMMENT '手机号', 12 | `username` varchar(30) NOT NULL DEFAULT '' COMMENT '用户名', 13 | `password` varchar(32) NOT NULL DEFAULT '' COMMENT '密码', 14 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 15 | `create_ip_at` varchar(12) NOT NULL DEFAULT '' COMMENT '创建ip', 16 | `last_login_at` int(11) NOT NULL DEFAULT '0' COMMENT '最后一次登陆时间', 17 | `last_login_ip_at` varchar(12) NOT NULL DEFAULT '' COMMENT '最后一次登陆ip', 18 | `login_times` int(11) NOT NULL DEFAULT '0' COMMENT '登录次数', 19 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 20 | PRIMARY KEY (`id`), 21 | KEY `idx_email` (`email`), 22 | KEY `idx_phone` (`phone`), 23 | KEY `idx_username` (`username`) 24 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户'; 25 | 26 | -- 第三方账户 27 | CREATE TABLE `account_platform` ( 28 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 29 | `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '账号id', 30 | `platform_id` varchar(60) NOT NULL DEFAULT '' COMMENT '平台id', 31 | `platform_token` varchar(60) NOT NULL DEFAULT '' COMMENT '平台access_token', 32 | `type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '平台类型 0:未知,1:facebook,2:google,3:wechat,4:qq,5:weibo,6:twitter', 33 | `nickname` varchar(60) NOT NULL DEFAULT '' COMMENT '昵称', 34 | `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '头像', 35 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 36 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 37 | PRIMARY KEY (`id`), 38 | KEY `idx_uid` (`uid`), 39 | KEY `idx_platform_id` (`platform_id`) 40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方用户信息'; 41 | ``` 42 | 43 | **用户模型** 44 | 45 | ```sql 46 | 47 | -- 用户模型 48 | CREATE TABLE `skr_member` ( 49 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用户id', 50 | `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '账号id', 51 | `nickname` varchar(30) NOT NULL DEFAULT '' COMMENT '昵称', 52 | `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '头像(相对路径)', 53 | `gender` enum('male','female','unknow') NOT NULL DEFAULT 'unknow' COMMENT '性别', 54 | `role` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '角色 0:普通用户 1:vip', 55 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 56 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 57 | PRIMARY KEY (`id`), 58 | KEY `idx_uid` (`uid`) 59 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户信息'; 60 | ``` 61 | 62 | **员工模型** 63 | 64 | ```sql 65 | 66 | -- 员工表 67 | CREATE TABLE `staff_info` ( 68 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '员工id', 69 | `uid` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '账号id', 70 | `email` varchar(30) NOT NULL DEFAULT '' COMMENT '员工邮箱', 71 | `phone` varchar(15) NOT NULL DEFAULT '' COMMENT '员工手机号', 72 | `name` varchar(30) NOT NULL DEFAULT '' COMMENT '员工姓名', 73 | `nickname` varchar(30) NOT NULL DEFAULT '' COMMENT '员工昵称', 74 | `avatar` varchar(255) NOT NULL DEFAULT '' COMMENT '员工头像(相对路径)', 75 | `gender` enum('male','female','unknow') NOT NULL DEFAULT 'unknow' COMMENT '员工性别', 76 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 77 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 78 | PRIMARY KEY (`id`), 79 | KEY `idx_uid` (`uid`), 80 | KEY `idx_email` (`email`), 81 | KEY `idx_phone` (`phone`) 82 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工信息(这里列了大概的信息,多的可以垂直拆表)'; 83 | 84 | ``` 85 | 86 | **系统权限管理模型** 87 | 88 | ```sql 89 | 90 | -- 权限管理: 系统map 91 | CREATE TABLE `auth_ms` ( 92 | `id` smallint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 93 | `ms_name` varchar(255) NOT NULL DEFAULT '0' COMMENT '系统名称', 94 | `ms_desc` varchar(255) NOT NULL DEFAULT '0' COMMENT '系统描述', 95 | `ms_domain` varchar(255) NOT NULL DEFAULT '0' COMMENT '系统域名', 96 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 97 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 98 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 99 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 100 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 101 | PRIMARY KEY (`id`), 102 | KEY `idx_domain` (`domain`) 103 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统map(登记目前存在的后台系统信息)'; 104 | 105 | -- 权限管理: 系统menu 106 | CREATE TABLE `auth_ms_menu` ( 107 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 108 | `ms_id` smallint(11) unsigned NOT NULL DEFAULT '0' COMMENT '系统id', 109 | `parent_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '父菜单id', 110 | `menu_name` varchar(255) NOT NULL DEFAULT '0' COMMENT '菜单名称', 111 | `menu_desc` varchar(255) NOT NULL DEFAULT '0' COMMENT '菜单描述', 112 | `menu_uri` varchar(255) NOT NULL DEFAULT '0' COMMENT '菜单uri', 113 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 114 | `is_show` enum('yes','no') NOT NULL DEFAULT 'no' COMMENT '是否展示菜单', 115 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 116 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 117 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 118 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 119 | PRIMARY KEY (`id`), 120 | KEY `idx_ms_id` (`ms_id`), 121 | KEY `idx_parent_id` (`parent_id`) 122 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统menu'; 123 | 124 | -- 权限管理: 系统权限 125 | CREATE TABLE `auth_item` ( 126 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 127 | `ms_id` tinyint(11) unsigned NOT NULL DEFAULT '0' COMMENT '系统id', 128 | `menu_id` varchar(255) NOT NULL DEFAULT '0' COMMENT '页面/接口uri', 129 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 130 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 131 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 132 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 133 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 134 | PRIMARY KEY (`id`), 135 | KEY `idx_ms_menu` (`ms_id`, `menu_id`) 136 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统权限'; 137 | 138 | -- 权限管理: 系统权限(权限的各个集合) 139 | CREATE TABLE `auth_role` ( 140 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 141 | `name` varchar(255) NOT NULL DEFAULT '0' COMMENT '角色名称', 142 | `desc` varchar(255) NOT NULL DEFAULT '0' COMMENT '角色描述', 143 | `auth_item_set` text COMMENT '权限集合 多个值,号隔开', 144 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 145 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 146 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 147 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 148 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 149 | PRIMARY KEY (`id`) 150 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工角色'; 151 | 152 | -- 权限管理: 角色与员工关系 153 | CREATE TABLE `auth_role_staff` ( 154 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增id', 155 | `staff_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '员工id', 156 | `role_set` text COMMENT '角色集合 多个值,号隔开', 157 | `create_at` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', 158 | `create_by` int(11) NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 159 | `update_at` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', 160 | `update_by` int(11) NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 161 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 1:enable, 0:disable, -1:deleted', 162 | PRIMARY KEY (`id`), 163 | KEY `idx_staff_id` (`staff_id`) 164 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限角色与员工关系'; 165 | 166 | ``` 167 | 168 | 169 | # 地址存id还是文字 170 | 171 | -------------------------------------------------------------------------------- /src/aftersale/README.md: -------------------------------------------------------------------------------- 1 | # 售后服务 2 | 3 | 请您耐心等待... -------------------------------------------------------------------------------- /src/base/README.md: -------------------------------------------------------------------------------- 1 | # 基础服务 2 | 3 | 请您耐心等待... -------------------------------------------------------------------------------- /src/base/search/business.md: -------------------------------------------------------------------------------- 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 | ## 搜索激活页 74 | 75 |

76 | 77 | 78 | 79 |

80 | 81 | ## 搜索建议词 82 | 83 | ### 普通搜索建议词 84 | 85 |

86 | 87 | 88 | 89 |

90 | 91 | ### 搜索直达 92 | 93 | #### 活动直达 94 | 95 | 搜索一个关键字之后,直接跳转到一个活动专题页面。 96 | 97 | #### 商品直达 98 | 99 | 搜索一个关键字之后,直接跳转到一个商品详情页面。 100 | ## 搜索热词 101 | 102 |

103 | 104 | 105 | 106 |

107 | 108 | ## 搜索历史词 109 | 110 |

111 | 112 | 113 | 114 |

115 | 116 | # 搜索后 117 | 118 |

119 | 120 | 121 | 122 |

123 | 124 | ## 搜索结果页面 125 | 126 |

127 | 128 | 129 | 130 |

131 | 132 | ## 大搜 133 | 134 |

135 | 136 | 137 | 138 |

139 | 140 | ## 垂搜 141 | 142 |

143 | 144 | 145 | 146 |

147 | 148 | ## 纠错 149 | 150 | 先来看看中纠错: 151 | 152 | ### 中纠错 153 | 154 |

155 | 156 | 157 | 158 |

159 | 160 | 如果输入了一个错别字,会自动帮纠错为正确的关键字,但是**会**友情提示用户是否还是按输入的词搜索。 161 | 162 | ### 强纠错 163 | 164 | 相对于中纠错,如果输入了一个错别字,会自动帮纠错为正确的关键字,但是**不会**友情提示用户是否还是按输入的词搜索。 165 | 166 |

167 | 168 | 169 | 170 |

171 | 172 | ### 弱纠错 173 | 174 | 略。 175 | 176 | # 总结 177 | 178 |

179 | 180 | 181 | 182 |

-------------------------------------------------------------------------------- /src/base/search/tech.md: -------------------------------------------------------------------------------- 1 | # 由浅到深,入门搜索原理 2 | 3 | 本次带来电商搜索业务的介绍,电商搜索系列分为两篇文章: 4 | 5 | - 电商搜索业务介绍 6 | - 由浅到深,入门搜索原理 7 | 8 | > 本文均以开源搜索引擎ES(Elasticsearch)为例,下文简称ES。 9 | 10 | 首先,本篇文章对于初次接触的同学来讲,涉及的概念会比较多,主要如下: 11 | 12 | 搜索名词概念|描述 13 | ------|------ 14 | 文档(Doc)|? 15 | 词条(Term)|? 16 | 倒排索引(Inverted Index)|? 17 | 关键字(Query)|? 18 | 召回(Recall)|? 19 | 词频(tf:Term Frequency)|? 20 | 逆文档率(idf:Inverse Document Frequency)|? 21 | 粗排|? 22 | 精排|? 23 | 24 | 本篇文章由简到繁入门搜索原理,并逐步揭开上面这些概念的面纱。本文结构如下: 25 | 26 | - 搜索引擎ES的诞生 27 | - 简易版搜索过程 28 | + 索引过程 29 | + 查询过程 30 | - 进阶版搜索过程 31 | + 索引过程 32 | * 什么是文档(Doc) 33 | * 什么是词条(Term) 34 | * 什么是倒排索引(Inverted Index) 35 | * 文档(Doc)分析 36 | - 字符过滤器 37 | - 分词器 38 | - 分词过滤器 39 | * 创建倒排索引 40 | + 查询过程 41 | * 关键字(Query)分析 42 | - 字符过滤器 43 | - 分词器 44 | - 分词过滤器 45 | * 召回(Recall) 46 | - 什么是召回(Recall) 47 | * 排序 48 | + 什么是词频(tf:Term Frequency) 49 | + 什么是逆文档率(idf:Inverse Document Frequency) 50 | + 粗排/精排 51 | + 搜索过程总结 52 | - 搜索引擎ES进阶 53 | + 索引(名词)的基本结构 54 | + 搜索引擎ES的逻辑结构 55 | 56 | ## 搜索引擎ES的诞生 57 | 58 | ES诞生于一个开源的JAVA库`Lucene`。通过`Lucene`官网的描述我们可以发现`Lucene`具备如下能力: 59 | 60 | - `Lucene`是一个JAVA库 61 | - `Lucene`实现了拼写检查 62 | - `Lucene`实现了命中字符高亮 63 | - `Lucene`实现了分析、分词功能 64 | 65 | `Lucene`不具备的能力: 66 | 67 | - 分布式 68 | - 高可用 69 | - 开箱即用 70 | - 等等 71 | 72 |

73 | 74 | 75 | 76 |

77 | 78 | 所以多年之前名叫`Shay Banon`的开发者,通过`Lucene`实现了一个高可用的开源分布式搜索引擎`Elasticsearch`。 79 | 80 |

81 | 82 | 83 | 84 |

85 | 86 | 常见的搜索功能都是基于「搜索引擎」实现的,接着我们来看看**简易版搜索过程**。 87 | 88 | ## 简易版搜索过程 89 | 90 | 简易版搜索过程如下: 91 | 92 | - 第①步:索引过程,需要被搜索的源数据被索引(动词)到搜索引擎中,并建立索引(名词)。 93 | - 第②步:查询过程,用户输入关键字(Query),搜索引擎分析Query并返回查询结果。 94 | 95 |

96 | 97 | 98 | 99 |

100 | 101 | ## 进阶版搜索过程 102 | 103 | ### 索引过程 104 | 105 |

106 | 107 | 108 | 109 |

110 | 111 | #### 什么是文档(Doc) 112 | 113 | 举个栗子,比如《电商设计手册 | SkrShop》网页内容需要被搜索到,那这页网页的全部内容就称之为一个`文档Doc`。 114 | 115 |

116 | 117 | 118 | 119 |

120 | 121 | `文档Doc`内容如下: 122 | 123 | ```html 124 | 125 | 126 | 127 | 128 | 电商设计手册 | SkrShop 129 | 130 | 131 | 132 |

秒杀是电商的一种营销手段

133 | 134 | ``` 135 | 136 | 搜索名词概念|描述 137 | ------|------ 138 | 文档(doc)|需要被搜索的文本内容,可以是某个商品详细信息、某个网页信息等等文本。 139 | 140 | #### 什么是词条(Term) 141 | 142 | 继续以上文的`文档Doc`为例。为了简化对`词条(Term)`的理解,把上述`文档Doc`的内容简化为一句话`秒杀是电商的一种营销手段`。 143 | 144 | `词条(Term)`就是`文档Doc`经过分词处理得到的词条结果集合。比如`秒杀是电商的一种营销手段`被中文分词之后得到: 145 | 146 | ``` 147 | 秒杀 / 是 / 电商 / 的 / 一种 / 营销 / 手段 148 | ``` 149 | 150 | 秒杀、是、电商、的、一种、营销、手段分别称之为`词条(Term)`,该集合称之为`Terms`。 151 | 152 | 搜索名词概念|描述 153 | ------|------ 154 | 词条(Term)|被搜索的文本Doc被分词器拆解成N个标准的语句。 155 | 156 | #### 什么是倒排索引(Inverted Index) 157 | 158 | 「倒排索引」是索引(动词)源数据时,创建的索引(名词)的具体实现。 159 | 160 | 我们以如下文档(Doc)为例,解释倒排索引: 161 | 162 | 文档ID|文档内容(Doc) 163 | ------|------ 164 | 1|电商设计手册SkrShop 165 | 2|秒杀是电商的一种营销手段 166 | 3|购物车是电商购买流程最重要的一步 167 | 168 | 分词器:文档(Doc)拆解为多个独立词条(Doc -> Terms)。 169 | 170 | 开源中文分词器: 171 | 172 | - IK Analyzer 173 | - jieba 174 | - 等 175 | 176 | 以jieba分词器在线演示为例:https://app.gumble.pw/jiebademo/ 177 | 178 | 文档ID|文档内容(Doc)|中文分词结果(Terms) 179 | ------|------|------ 180 | 1|电商设计手册SkrShop|电商 / 设计 / 手册 / SkrShop 181 | 2|秒杀是电商的一种营销手段|秒杀 / 是 / 电商 / 的 / 一种 / 营销 / 手段 182 | 3|购物车是电商购买流程最重要的一步|购物车 / 是 / 电商 / 购买 / 流程 / 最 / 重要 / 的 / 一步 183 | 184 | 每个词条对应的文档ID如下: 185 | 186 | 词条|文档IDs 187 | ------|------ 188 | 电商|1、2、3 189 | 设计|1 190 | 手册|1 191 | SkrShop|1 192 | 秒杀|2 193 | 是|2、3 194 | 的|2、3 195 | 一种|2 196 | 营销|2 197 | 手段|2 198 | 购物车|3 199 | 购买|3 200 | 流程|3 201 | 最|3 202 | 重要|3 203 | 一步|3 204 | 205 | 以上就是建立倒排索引的基本过程。 206 | 207 | 完成倒排索引建立之后,模拟搜索过程,假设: 208 | 209 | - 搜索`电商`,能快速找到文档1、2、3 210 | - 搜索`营销`,能快速找到文档2 211 | 212 | (这个过程叫做「召回」) 213 | 214 | 以上就是「倒排索引」的概念。 215 | 216 | 搜索名词概念|描述 217 | ------|------ 218 | 倒排索引(Inverted Index)|索引(动词)源数据时,创建的索引(名词)的具体实现。 219 | 220 | #### 文档(Doc)分析 221 | 222 | 分析就是标准化文档(Doc)文本的过程,以及把文档(Doc)转换成标准化词条(Term)的过程。搜索引擎ES分析过程的实现依赖于分析器。 223 | 224 | 分析器基本组成: 225 | 226 | - 字符过滤器 227 | - 分词器 228 | - 分词过滤器 229 | 230 |

231 | 232 | 233 | 234 |

235 | 236 | ##### 字符过滤器 237 | 238 | > 一个分析器对应一个字符过滤器。 239 | 240 | 格式化为标准文本(text -> standard text),例如去掉文本中的html标签。 241 | 242 |

243 | 244 | 245 | 246 |

247 | 248 | 比如`

电商设计手册SkrShop

`--->`电商设计手册SkrShop` 249 | 250 | ##### 分词器 251 | 252 | > 一个分析器对应一个分词器。 253 | 254 | 文档(Doc)拆解为多个独立词条(doc -> terms)的过程。举个例子: 255 | 比如`电商设计手册SkrShop`--->`电商 / 设计 / 手册 / SkrShop` 256 | 257 | 这里还需要提到的是**自定义词库**:原始词库不具备的词汇,比如最近新产生的网络词汇。 258 | 259 |

260 | 261 | 262 | 263 |

264 | 265 | ##### 分词过滤器 266 | 267 | > 一个分析器对应N个分词过滤器。 268 | 269 | - 统一转小写 270 | - 近义词转换 271 | - 停用词 272 | - 提取词干 273 | - 纠错 274 | - 自动补全 275 | - 等等... 276 | 277 |

278 | 279 | 280 | 281 |

282 | 283 | 分词过滤器|示例 284 | ------|------ 285 | 统一转小写|适用于英文等。比如统一把英文字母转换为小写,例`Word -> word` 286 | 近义词转换|适用于各语言。例`宽敞 -> 宽阔` 287 | 停用词|适用于各语言。去除含义宽泛不具备代表性的词语和其他人工指定停用的词语,例`的`、`是`。中文停用词库:https://github.com/goto456/stopwords 288 | 提取词干|适用于英文等。例`words -> word` 289 | 纠错|适用于各语言。例`宽肠 -> 宽敞` 290 | 自动补全|适用于各语言。 291 | 292 | #### 索引过程总结 293 | 294 | ### 查询过程 295 | 296 |

297 | 298 | 299 | 300 |

301 | 302 | 搜索名词概念|描述 303 | ------|------ 304 | 关键字(Query)|发起搜索是用户输入的关键字 305 | 306 | #### 关键字(Query)分析 307 | 308 | 关键字(Query)同样需要经过`分析器`,且和文档索引过程是相同的`分析器`。 309 | 310 | 相同分析器: 311 | 312 | - 相同字符过滤器 313 | - 相同分词器 314 | - 相同分词过滤器 315 | 316 |

317 | 318 | 319 | 320 |

321 | 322 | 分词器: 323 | 324 | 关键字(Query)|中文分词结果(Terms) 325 | ------|------ 326 | 秒杀系统的设计|秒杀 / 系统 / 的 / 设计 327 | 328 | |词条(Terms)| 329 | |------| 330 | |秒杀| 331 | |系统| 332 | |的| 333 | |设计| 334 | 335 | 分词过滤器: 336 | 337 | 此处以停用词分词过滤器为例讲解分词过滤器的过程,本文使用的停用词库示例:https://github.com/goto456/stopwords/blob/master/cn_stopwords.txt 338 | 339 | 得到去除了停用词`的`之后的词条(Terms)集合: 340 | 341 | |词条(Terms)| 342 | |------| 343 | |秒杀| 344 | |系统| 345 | |设计| 346 | 347 | #### 召回(Recall) 348 | 349 | ##### 什么是召回(Recall) 350 | 351 | 使用上文的文档内容以及文档分词结果: 352 | 353 | 文档ID|文档内容(Doc)|中文分词结果(Terms) 354 | ------|------|------ 355 | 1|电商设计手册SkrShop|电商 / 设计 / 手册 / SkrShop 356 | 2|秒杀是电商的一种营销手段|秒杀 / 是 / 电商 / 的 / 一种 / 营销 / 手段 357 | 3|购物车是电商购买流程最重要的一步|购物车 / 是 / 电商 / 购买 / 流程 / 最 / 重要 / 的 / 一步 358 | 359 | 进一步使用分词过滤器过滤分词结果,以相同的停用词分词过滤器为例。本文使用的停用词库示例:https://github.com/goto456/stopwords/blob/master/cn_stopwords.txt 360 | 361 | 比如命中了停用词`是`: 362 | 363 |

364 | 365 | 366 | 367 |

368 | 369 | 经过停用词分词过滤器之后的结果: 370 | 371 | 文档ID|文档内容(Doc)|中文分词结果(Terms) 372 | ------|------|------ 373 | 1|电商设计手册SkrShop|电商 / 设计 / 手册 / SkrShop 374 | 2|秒杀是电商的一种营销手段|秒杀 / 电商 / 一种 / 营销 / 手段 375 | 3|购物车是电商购买流程最重要的一步|购物车 / 电商 / 购买 / 流程 / 重要 / 一步 376 | 377 | 进一步得到倒排索引结构: 378 | 379 | 词条|文档IDs 380 | ------|------ 381 | 电商|1、2、3 382 | 设计|1 383 | 手册|1 384 | SkrShop|1 385 | 秒杀|2 386 | 一种|2 387 | 营销|2 388 | 手段|2 389 | 购物车|3 390 | 购买|3 391 | 流程|3 392 | 重要|3 393 | 一步|3 394 | 395 | 接着模拟搜索过程,假设用户搜索`秒杀系统的设计`: 396 | 397 | 关键字(Query)|中文分词结果(Terms) 398 | ------|------ 399 | 秒杀系统的设计|秒杀 / 系统 / 的 / 设计 400 | 401 | |词条(Terms)| 402 | |------| 403 | |秒杀| 404 | |系统| 405 | |的| 406 | |设计| 407 | 408 | 分词过滤器,使用同上过程的`停用词分词过滤器`为例,得到去除了停用词`的`之后的词条(Terms)集合,称之为关键字(Query)的词条集合: 409 | 410 | |词条(Terms)| 411 | |------| 412 | |秒杀| 413 | |系统| 414 | |设计| 415 | 416 | - 关键字(Query)的词条`秒杀`,通过上述倒排索引可以很容易找到`文档2` 417 | - 关键字(Query)的词条`系统`,通过上述倒排索引没有找到任何文档 418 | - 关键字(Query)的词条`设计`,通过上述倒排索引可以很容易找到`文档1` 419 | 420 | 这样用户搜索`秒杀系统的设计`就找到了如下文档: 421 | 422 | - `文档2`:秒杀是电商的一种营销手段 423 | - `文档1`:电商设计手册SkrShop 424 | 425 | 以上过程就是`召回`。 426 | 427 | 搜索名词概念|描述 428 | ------|------ 429 | 召回(Recall)|搜索引擎利用倒排索引,通过词条获取相关文档的过程。 430 | 431 | 上述召回过程,用户通过搜索`秒杀系统的设计`找到了文档1、2。 432 | 433 | ``` 434 | 补充:以上基于倒排索引的文本召回方式。除此之外还有基于相同类目、其他相似属性的召回方式,以及基于深度学习的向量召回。 435 | ``` 436 | 437 | 接着问题来了: 438 | 439 | > 召回的文档1、2,谁在前,谁在后的顺序怎么决定呢? 440 | 441 | 接着下文来讲搜索引擎排序的实现。 442 | 443 | #### 排序 444 | 445 | 引入上面的问题: 446 | 447 | > 文档1、2,谁在前,谁在后的顺序怎么决定呢? 448 | 449 | 答:文档的相关性决定的,搜索引擎会给文档的相关性进行打分score。通常决定这个分数score主要是两个指标: 450 | 451 | - 文档率:tf(Term Frequency) 452 | - 逆文档率:idf(Inverse Document Frequency) 453 | 454 | 可以简单理解为相关性score = 文档率 * 逆文档率,相关性score的值越高排序越靠前,接着,我们分别看看相关概念的含义。 455 | 456 | ##### 什么是词频(tf:Term Frequency) 457 | 458 | 还是使用上面的文档: 459 | 460 | 文档ID|文档内容(Doc)|中文分词结果(Terms) 461 | ------|------|------ 462 | 1|电商设计手册SkrShop|电商 / 设计 / 手册 / SkrShop 463 | 2|秒杀是电商的一种营销手段|秒杀 / 电商 / 一种 / 营销 / 手段 464 | 3|购物车是电商购买流程最重要的一步|购物车 / 电商 / 购买 / 流程 / 重要 / 一步 465 | 466 | 这里我们以词条:`电商/秒杀`为例。 467 | 468 | 词频的简单算法:词频 = 词条在单个文档出现的次数/文档总词条数,词频的值越大越相关,反之越不相关。 469 | 470 | 比如,`秒杀`一词在文档1中出现的频率,以单个文档的全部词条为维度,我们简单的到了`秒杀`一词在各文档的词频: 471 | 472 | 文档ID|文档内容(Doc)|中文分词结果(Terms)|词条在单个文档出现的次数|词频(秒杀) 473 | ------|------|------|------|------ 474 | 1|电商设计手册SkrShop|电商 / 设计 / 手册 / SkrShop|0|0/4=0 475 | 2|秒杀是电商的一种营销手段|秒杀 / 电商 / 一种 / 营销 / 手段|1|1/5=0.2 476 | 3|购物车是电商购买流程最重要的一步|购物车 / 电商 / 购买 / 流程 / 重要 / 一步|0|0/6=0 477 | 478 | 同理,我们简单的到了`电商`一词在各文档的词频: 479 | 480 | 文档ID|文档内容(Doc)|中文分词结果(Terms)|词条在单个文档出现的次数|词频(电商) 481 | ------|------|------|------|------ 482 | 1|电商设计手册SkrShop|电商 / 设计 / 手册 / SkrShop|1|1/4=0.25 483 | 2|秒杀是电商的一种营销手段|秒杀 / 电商 / 一种 / 营销 / 手段|1|1/5=0.2 484 | 3|购物车是电商购买流程最重要的一步|购物车 / 电商 / 购买 / 流程 / 重要 / 一步|1|1/6=0.167 485 | 486 | 搜索名词概念|描述 487 | ------|------ 488 | 词频(tf:Term Frequency)|词条在单个文档出现的次数/文档总词条数 489 | 490 | ##### 什么是逆文档率(idf:Inverse Document Frequency) 491 | 492 | 对于单个文档而言,词频越的值越相关。 493 | 494 | > 思考个问题,如果某个词条在所有文档都出现,相关性越好还是越不好? 495 | 496 | ``` 497 | 答:不好,对吧。 498 | ``` 499 | 500 | 这个就是文档率:`文档率 = 包含某个词条的文档数 / 所有文档数`,文档率值越大越不相关,反之相关。 501 | 502 | 因为词频的值越大越相关,反之越不相关。为了保证和词频的逻辑一致,以及最终相关得分越高越相关,调整了文档率的算法,调换了分子分母:`所有文档数 / (包含某个词条的文档数 + 1)`加1保证分母不为零,这个就是`逆文档率`。 503 | 504 | 逆文档率 = `所有文档数 / (包含某个词条的文档数 + 1)`。 505 | 506 | 但是呢,因为文档数往往特别大,上面的到的`逆文档率`的值会巨大无比,所以调整下公式,引入对数,降低值的大小,且让值变得平滑: 507 | 508 | `逆文档率 = log(所有文档数 / (包含某个词条的文档数 + 1))` 509 | 510 | 511 | 词条(Term)|逆文档率 512 | ------|------ 513 | 电商|log(3/(3+1)) 514 | 秒杀|log(3/(1+1)) 515 | 516 | 最终就计算出每个文档分别对应每个Query词条的相关性score(tf/idf):相关性score = 文档率 * 逆文档率。 517 | 518 | ##### 粗排/精排 519 | 520 | 上面利用tf/idf分数(`相关性score = 文档率 * 逆文档率`)排序的结果只是对召回文档的初步排序,称之为`粗排`。 521 | 522 | 得到`粗排`的结果后,通常还会把文档按照实际业务的要求进行更精确的排序,比如通过`人工干预`增加一些文档的权重,使之排序更靠前,这个过程就是`精排`。 523 | 524 | 搜索名词概念|描述 525 | ------|------ 526 | 粗排|利用tf/idf分数排序召回文档的过程 527 | 精排|把粗排结果,按照实际业务的要求更加精确的排序等等 528 | 529 | ### 搜索过程总结 530 | 531 | 1. 索引过程:文档(Doc) -> 分析 -> 倒排索引。 532 | 533 |

534 | 535 | 536 | 537 |

538 | 539 | 2. 查询过程:关键字(Query) -> 分析 -> 召回 -> 粗排 -> 精排。 540 | 541 |

542 | 543 | 544 | 545 |

546 | 547 | ## 搜索引擎ES进阶 548 | 549 | ### 索引(名词)的基本结构 550 | 551 | - 索引index 552 | + 类型type:区分不同的文档数据结构类型 553 | * 映射mapping:管理索引的属性,比如使用的分析器等等 554 | * 文档doc:需要被搜索的具体文档 555 | 556 |

557 | 558 | 559 | 560 |

561 | 562 | 进一步完善搜索过程:加入更详细的索引(名词)结构 563 | 564 |

565 | 566 | 567 | 568 |

569 | 570 | ### 搜索引擎ES的逻辑结构 571 | 572 |

573 | 574 | 575 | 576 |

-------------------------------------------------------------------------------- /src/express/README.md: -------------------------------------------------------------------------------- 1 | # 物流系统 2 | 3 | 请您耐心等待... -------------------------------------------------------------------------------- /src/order/README.md: -------------------------------------------------------------------------------- 1 | # 订单中心 2 | 3 | 订单中心系列主要内容如下: 4 | 5 | |知识点| 6 | |-------| 7 | |订单结算页| 8 | |创建订单| 9 | |订单履约| 10 | |订单状态| 11 | |订单详情| 12 | |订单逆向操作| 13 | |...| -------------------------------------------------------------------------------- /src/order/checkout.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 截止目前为止SkrShop《电商设计手册》系列梳理的内容已经涵盖了如下几大块: 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 | - 订单相关金额 74 | - 等等 75 | 76 | ## 订单结算页面的组成 77 | 78 | > 我一直在思考前端可以模块化,后端接口数据不可以模块化吗? 79 | 80 | ``` 81 | 我的答案:是可以的。 82 | ``` 83 | 84 | 我们依据上面整理的内容,再通过以往的经验把**订单结算页面**进行模块化拆分和组合,得到如下订单结算页面的**模块化构成**: 85 | 86 |

87 | 88 | 89 | 90 |

91 | 92 | 关于这块代码如何设计,可以参考我的文章[《代码组件 | 我的代码没有else》](http://tigerb.cn/go-patterns/#/?id=%e7%bb%84%e5%90%88%e6%a8%a1%e5%bc%8f) 93 | 94 | ## 订单结算页面各模块分析 95 | 96 | 模块编号|模块名称|子模块编号|子模块名称|模块描述 97 | ------------|------------|------------|------------|------------ 98 | 1|地址模块|-|-|展示用户最优地址 99 | 2|支付方式模块|-|-|该订单支持的支付方式 100 | 3|店铺模块|-|-|包含店铺信息、商品信息、参与的优惠信息、可选的物流方式、商品售后信息等 101 | 3|-|3.1|商品模块|包含子模块:商品基础信息模块、商品优惠信息模块、售后模块 102 | 3|-|3.2.1|商品基础信息模块|商品的信息,名称、图片、价格、库存等 103 | 3|-|3.2.2|商品优惠信息模块|选择的销售活动优惠选项 104 | 3|-|3.2.3|售后模块|商品享有的售后权益信息 105 | 3|-|3.3|物流模块|可选择的配送方式 106 | 3|-|3.4|店铺商品金额信息模块|- 107 | 4|发票模块|-|-|选择开发票的类型、补充发票信息 108 | 5|优惠券模块|-|-|展示该订单可以使用的优惠券列表 109 | 6|礼品卡模块|-|-|展示可以选择使用礼品卡列表 110 | 7|平台积分模块|-|-|用户可以使用积分抵掉部分现金 111 | 8|订单金额信息模块|-|-|包含该订单的金额明细 112 | 113 | ## 地址模块 114 | 115 | > 展示用户的最优地址 116 | 117 | 最优地址逻辑: 118 | 119 | - 首先,用户设置的默认地址 120 | - 如果没有默认地址,则返回最近下单的地址 121 | 122 | 字段名称|类型|下级字段名称|类型|字段含义 123 | ------|------|------|------|------ 124 | consignee|string|-|-|收货人姓名 125 | email|string|-|-|收货人邮箱(返回值用户名部分打码) 126 | mobile|string|-|-|收货人手机号(返回值中间四位打码) 127 | country|object|id|int64|国家ID 128 | country|object|name|string|国家名称 129 | province|object|id|int64|省ID 130 | province|object|name|string|省名称 131 | city|object|id|int64|市ID 132 | city|object|name|string|市名称 133 | county|object|id|int64|区县ID 134 | county|object|name|string|区县名称 135 | street|object|id|int64|街道乡镇ID 136 | street|object|name|string|街道乡镇名称 137 | detailed_address|string|-|-|详细地址(用户手填) 138 | postal_code|string|-|-|邮编 139 | address_id|int64|-|-|地址ID 140 | is_default|bool|-|-|是否是默认地址 141 | label|string|-|-|地址类型标签,家、公司等 142 | longitude|string|-|-|经度 143 | latitude|string|-|-|纬度 144 | 145 |

146 | 147 | 148 | 149 |

150 | 151 | 模块数据demo: 152 | ```json 153 | { 154 | "address_module": { 155 | "consignee": "收货人姓名", 156 | "email": "收货人邮箱(返回值用户名部分打码)", 157 | "mobile": "收货人手机号(返回值中间四位打码)", 158 | "country": { 159 | "id": 666, 160 | "name": "国家名称" 161 | }, 162 | "province": { 163 | "id": 12123, 164 | "name": "省名称" 165 | }, 166 | "city": { 167 | "id": 212333, 168 | "name": "市名称" 169 | }, 170 | "county": { 171 | "id": 1233222, 172 | "name": "区县名称" 173 | }, 174 | "street": { 175 | "id": 9989999, 176 | "name": "街道乡镇名称" 177 | }, 178 | "detailed_address": "详细地址(用户手填)", 179 | "postal_code": "邮编", 180 | "address_id": 212399999393, 181 | "is_default": false, 182 | "label": "地址类型标签,家、公司等", 183 | "longitude": "经度", 184 | "latitude": "纬度" 185 | } 186 | } 187 | ``` 188 | 189 | ## 支付方式模块 190 | 191 | > 该订单支持的支付方式 192 | 193 | 支付方式选项: 194 | 195 | - 在线支付 196 | - 货到付款 197 | 198 | 字段名称|类型|下级字段名称|类型|字段含义 199 | ------|------|------|------|------ 200 | pay_method_list|array|id|int|支付方式ID 201 | pay_method_list|array|name|string|支付方式名称 202 | pay_method_list|array|desc|string|支付方式描述 203 | 204 | 205 |

206 | 207 | 208 | 209 |

210 | 211 | 模块数据demo: 212 | ```json 213 | { 214 | "pay_method_module": { 215 | "pay_method_list": [ 216 | { 217 | "id": 1, 218 | "name": "在线支付", 219 | "desc": "在线支付的描述" 220 | }, 221 | { 222 | "id": 2, 223 | "name": "货到付款", 224 | "desc": "货到付款的描述" 225 | } 226 | ] 227 | } 228 | } 229 | ``` 230 | 231 | ## 店铺模块 232 | 233 | > 包含店铺信息、商品信息、参与的优惠信息、可选的物流方式、商品售后信息等 234 | 235 | 店铺模块由如下子模块组成: 236 | 237 | - 商品模块 238 | + 商品基础信息模块 239 | + 商品优惠信息模块 240 | + 售后模块 241 | - 商品物流模块 242 | - 店铺商品总金额信息模块 243 | 244 |

245 | 246 | 247 | 248 |

249 | 250 | 由于此处内容比较多我们之后再来单独分析。 251 | 252 | ## 发票模块 253 | 254 | > 用户选择开发票的类型以及补充发票信息 255 | 256 | 选择开发票的类型: 257 | 258 | - 个人 259 | - 单位 260 | 261 | 字段名称|类型|下级字段名称|类型|字段含义 262 | ------|------|------|------|------ 263 | type_id|int|-|-|发票类型:个人;单位 264 | type_name|string|-|-|发票类型名称 265 | type_desc|string|-|-|发票类型描述 266 | 267 | 268 |

269 | 270 | 271 | 272 |

273 | 274 | 模块数据demo: 275 | ```json 276 | { 277 | "invoice_module": { 278 | "type_list": [ 279 | { 280 | "type_id": 1, 281 | "type_name": "个人", 282 | "type_desc": "描述" 283 | }, 284 | { 285 | "type_id": 2, 286 | "type_name": "公司", 287 | "type_desc": "描述" 288 | } 289 | ] 290 | } 291 | } 292 | ``` 293 | 294 | ## 优惠券模块 295 | 296 | > 返回该订单可以使用的优惠券列表,以及默认选择对于当前订单而言的最优优惠券 297 | 298 | - 展示用户的优惠券列表:当前订单可用的排最前面其他放最后面 299 | - 默认选中最优优惠券:对于当前订单优惠力度最大的一张优惠券 300 | 301 | 关于优惠券的其他内容可以阅读优惠券章节内容。 302 | 303 | ## 礼品卡模块 304 | 305 | > 展示可以选择使用礼品卡列表 306 | 307 | 字段名称|类型|下级字段名称|类型|字段含义 308 | ------|------|------|------|------ 309 | giftcard_list|array|id|int64|礼品卡id 310 | giftcard_list|array|name|string|礼品卡名称 311 | giftcard_list|array|desc|string|礼品卡描述 312 | giftcard_list|array|pic_url|string|礼品卡图片 313 | giftcard_list|array|total_amount|float64|礼品卡初始总金额 314 | giftcard_list|array|total_amount_txt|string|礼品卡初始总金额-格式化后 315 | giftcard_list|array|remaining_amount|float64|礼品卡剩余金额 316 | giftcard_list|array|remaining_amount_txt|string|礼品卡剩余金额-格式化后 317 | 318 | 319 |

320 | 321 | 322 | 323 |

324 | 325 | 模块数据demo: 326 | ```json 327 | { 328 | "giftcard_module": { 329 | "giftcard_list": [ 330 | { 331 | "id": 341313121, 332 | "name": "礼品卡名称", 333 | "desc": "礼品卡描述", 334 | "pic_url": "礼品卡图片", 335 | "total_amount": 100.00, 336 | "total_amount_txt": "100.00", 337 | "remaining_amount": 21.00, 338 | "remaining_amount_txt": "21.00" 339 | } 340 | ] 341 | } 342 | } 343 | ``` 344 | 345 | ## 平台积分模块 346 | 347 | > 用户可以使用积分抵现 348 | 349 | 比如上线某东订单结算页面中的京豆。 350 | 351 | 字段名称|类型|下级字段名称|类型|字段含义 352 | ------|------|------|------|------ 353 | order_amount_min|float64|-|-|可使用积分抵现功能的订单金额下限 354 | total_points|int64|-|-|用户总积分 355 | can_use_points|int64|-|-|可使用的积分(可能存在冻结的积分) 356 | points2money_rate|int|-|-|积分转换为现金比率,比如每100积分抵1元,最低1积分抵0.01元 357 | points2money_min|int|-|-|用户最少满多少积分才可使用积分抵现 358 | points2money_max|int|-|-|单笔订单 最多可以使用积分的上限 359 | points_amount|float64|-|-|该订单积分可抵扣金额 360 | points_amount_txt|string|-|-|该订单积分可抵扣金额-格式化后 361 | 362 | 363 |

364 | 365 | 366 | 367 |

368 | 369 | 模块数据demo: 370 | ```json 371 | { 372 | "points_module": { 373 | "order_amount_min": 100.00, 374 | "total_points": 9999, 375 | "can_use_points": 9999, 376 | "points2money_rate": 100, 377 | "points2money_min": 1000, 378 | "points2money_max": 9999, 379 | "points_amount": 99.99, 380 | "points_amount_txt": "99.99" 381 | } 382 | } 383 | ``` 384 | 385 | ## 订单金额信息模块 386 | 387 | > 包含该订单的金额明细 388 | 389 | 字段名称|类型|下级字段名称|类型|字段含义 390 | ------|------|------|------|------ 391 | skus_amount|float64|-|-|商品的总金额 392 | promotion_amount|float64|-|-|优惠的总金额 393 | freight|float64|-|-|运费 394 | final_amount|float64|-|-|支付金额 395 | promotion_detail|object|coupon_amount|float64|优惠券优惠金额 396 | promotion_detail|object|sales_activity_amount|float64|销售活动优惠金额 397 | promotion_detail|object|giftcard_amount|float64|礼品卡使用金额 398 | promotion_detail|object|points_amount|float64|该订单积分抵扣金额 399 | 400 | ``` 401 | _txt字段略 402 | ``` 403 | 404 |

405 | 406 | 407 | 408 |

409 | 410 | 模块数据demo: 411 | ```json 412 | { 413 | "order_amount_module": { 414 | "skus_amount": 99.99, 415 | "skus_amount_txt": "99.99", 416 | "promotion_amount_total": 10.00, 417 | "promotion_amount_total_txt": "10.00", 418 | "freight_total": 8.00, 419 | "freight_total_txt": "8.00", 420 | "final_amount": 97.99, 421 | "final_amount_txt": "97.99", 422 | "promotion_detail": { 423 | "coupon_amount": 5.00, 424 | "coupon_amount_txt": "5.00", 425 | "sales_activity_amount": 5.00, 426 | "sales_activity_amount_txt": "5.00", 427 | "giftcard_amount": 0, 428 | "giftcard_activity_amount_txt": "0", 429 | "points_amount": 0, 430 | "points_amount_txt": "0" 431 | } 432 | } 433 | } 434 | ``` 435 | 436 | ## 结语 437 | 438 | 如上,订单结算页面的内容基本介绍完毕了,有任何问题随时到我们的github项目下留言 。 439 | -------------------------------------------------------------------------------- /src/promotion/README.md: -------------------------------------------------------------------------------- 1 | # 营销体系 2 | 3 | 营销体系在一个电商系统中承担着尖刀般的作用,为电商团队拼下无数订单,就如同现实生活中的一线销售人员,引流、促销、提高成单率和客单价。 4 | 5 | 一个营销体系拥有各种提升GMV的手段,并且仍然在不断的推陈出新,着实令人敬佩。 6 | 7 | ## 营销体系拆分 8 | 9 | 按照不同的维度,我们把营销体系划分为两大系统: 10 | 11 | 系统|描述|维度 12 | ---|---|--- 13 | 活动营销系统|拥有各种手段**提升PV\UV**,并引导消费|PV、UV维度 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 | 满减|单笔订单满多少减多少,例如,“满500减20” 55 | 满赠|单笔订单满多少增送其他商品,例如,“满999送秋裤” 56 | 买送|买就送,例如,“全场下单送秋裤” 57 | 限时购|限定时间区间,降价或折扣 58 | 秒杀|白送价格,数量极少的销售场景,例如,“一元秒杀” 59 | 加价购|再加多少钱,可以低价购买其他推荐商品 60 | 多买|单笔订单购买多个sku,享受折扣 61 | 全款预售|预售库存一次付清 62 | 订金膨胀预售|包含订金阶段、尾款阶段,订金翻倍抵现尾款,订金可退 63 | 定金膨胀预售|包含定金阶段、尾款阶段,定金翻倍抵现尾款,定金不退 64 | 盲售|包含订金(定金)阶段,尾款阶段;不知道尾款价格,不知道sku的具体信息,只知道spu的信息(可选) 65 | 拼团|限定时间凑够多人成功支付订单(享优惠价格) 66 | 砍价(点赞降价)|限定时间邀请好友砍价到指定金额 67 | 众筹|限定时间购买人数达到指定人数及以上即可成功享受优惠价格 68 | 组合套装|多个sku捆绑销售 69 | ---|--- 70 | 满减券|单笔订单满多少才能使用的优惠券 71 | 折扣券|单笔订单折扣 72 | 现金券|单笔订单抵现金s -------------------------------------------------------------------------------- /src/promotion/coupon.md: -------------------------------------------------------------------------------- 1 | # 优惠券服务 2 | 3 | ## 前言 4 | 5 | 进入正题,营销体系的基础服务「优惠券服务」。通过如下问题来介绍优惠券: 6 | 7 | - 优惠券有**哪些类型**? 8 | - 优惠券有**哪些适用范围**? 9 | - 优惠券有**哪些常见的场景**? 10 | - 优惠券本身应该有**哪些状态**? 11 | - 优惠券服务要有**哪些服务能力**? 12 | - 优惠券服务的**风控**怎么做? 13 | 14 | ## 优惠券有哪些类型? 15 | 16 | 对于获取优惠券的用户而言,关注的是优惠券的优惠能力,所以按优惠能力维度优惠券主要分为下面三类: 17 | 18 | 优惠能力维度|描述 19 | ------------|------------ 20 | 满减券|满多少金额(不含邮费)可以减多少金额 21 | 现金券|抵扣多少现金(无门槛) 22 | 抵扣券|抵扣某Sku全部金额(一个数量) 23 | 折扣券|打折 24 | 25 | 对于发放优惠券的运营人员而言: 26 | 27 | 一种是「**固定有效期**」,优惠券的生效时间戳和过期时间戳,在创建优惠券的时候已经确定。用户在任意时间领取该券,该券的有效时间都是之前设置的有效时间的开始结束时间。 28 | 29 | 另一种是「**动态有效期**」,创建优惠券设置的是有效时间段,比如7天有效时间、12小时有效时间等。这类优惠券以用户领取优惠券的时间为优惠券的有效时间的开始时间,以以用户领取优惠券的时间+有效时间为有效时间的结束时间。 30 | 31 | 有效期维度|优惠券类型|优惠券生效时间|优惠券失效时间|描述 32 | ------------|------------|------------|------------|------------ 33 | 固定|固定有效期|优惠券类型被创建时已确定|优惠券类型被创建时已确定|无论用户什么时间领取该优惠券,优惠券生效的时间都是设置好的统一时间 34 | 动态|动态有效期|用户领取优惠券时,当前时间戳|用户领取优惠券时,当前时间戳 + N\*24\*60\*60|优惠券类型被创建时,只确定了该优惠券的有效,例如6小时、7天、一个月 35 | 36 | 小结如下: 37 | 38 |

39 | 40 | 41 | 42 |

43 | 44 | 45 | ## 优惠券有哪些适用范围? 46 | 47 | ### 运营策略 48 | 49 | 限制纬度|限制细节|描述 50 | ------------|------------|------------ 51 | 商品纬度|| 52 | |(非)指定Sku|Sku券 53 | |(非)指定Spu|Spu券 54 | |(非)指定类别|类别券 55 | 平台纬度|| 56 | |指定店铺|店铺券 57 | |全场通用|平台券 58 | 59 | ### 适用终端 60 | 61 | 适用终端(复选框)|描述 62 | ------------|------------ 63 | Android|安卓端 64 | iOS|iOS端 65 | PC|网页电脑端 66 | Mobile|网页手机端 67 | Wechat|微信端 68 | 微信小程序|微信小程序 69 | All|以上所有 70 | 71 | ### 支付方式 72 | 73 | 支付类型|描述 74 | ------------|------------ 75 | 货到付款| 76 | 在线支付|支付宝/微信/银联 77 | All| 78 | 79 | ### 适用人群 80 | 81 | 适用人群|描述 82 | ------------|------------ 83 | 白名单|测试用户 84 | 会员|会员专属 85 | 86 | 小结如下: 87 | 88 |

89 | 90 | 91 | 92 |

93 | 94 | 95 | 96 | 97 | ## 优惠券有哪些常见的场景? 98 | 99 | ### 领取优惠券场景 100 | 101 | 领取优惠券场景|描述 102 | ------------|------------ 103 | 活动页面|大促、节假日活动页面展示获取优惠券的按钮 104 | 游戏页面|通过游戏获取优惠券 105 | 店铺首页|店铺首页展示领券入口 106 | 商品详情|商品详情页面展示领券入口 107 | 积分中心|积分兑换优惠券 108 | 109 | ### 展示优惠券场景 110 | 111 | 展示优惠券场景|描述 112 | ------------|------------ 113 | 活动页面|大促、节假日活动页面展示可以领取的优惠券 114 | 商品详情|商品详情页面展示可以领取、可以使用的优惠券列表 115 | 个人中心-我的优惠券|我的优惠券列表 116 | 订单结算页面|结算页面,适用该订单的优惠券列表以及推荐 117 | 积分中心|展示可以兑换的优惠券详情 118 | 119 | 120 | ### 选择优惠券场景 121 | 122 | 选择优惠券场景|描述 123 | ------------|------------ 124 | 商品详情|商品详情页面展示该用户已有的,且适用于该商品的优惠券 125 | 订单结算页面-优惠券列表|选择可用优惠券结算 126 | 订单结算页面-输入优惠码|输入优惠码结算 127 | 128 | ### 返还优惠券场景 129 | 130 | 返还优惠券场景|描述 131 | ------------|------------ 132 | 未支付订单取消|未支付的订单,用户主动取消返还优惠券,或超时关单返还优惠券 133 | 已支付订单全款取消|已支付(未收货)的订单,订单部分退款不返还,当整个订单全部退款返还优惠券 134 | 135 | ### 优惠券获取场景示例 136 | 137 | 场景示例|描述 138 | ------------|------------ 139 | 活动页领券|大促、节假日活动页面展示获取优惠券的按钮 140 | 游戏发券|游戏奖励 141 | 商品页领券|- 142 | 店铺页领券|- 143 | 购物返券|购买某个Sku,订单妥投后发放优惠券 144 | 新用户发券|新用户注册发放优惠券 145 | 积分兑券|积分换取优惠券 146 | 评论返券|用户订单收货之后,添加评论(审核后),给用户返回一张指定优惠券 147 | 148 | 小结如下: 149 | 150 |

151 | 152 | 153 | 154 |

155 | 156 | 157 | ## 优惠券本身应该有哪些状态? 158 | 159 |

160 | 161 | 162 | 163 |

164 | 165 | 166 | ## 优惠券服务要有哪些服务能力? 167 | 168 | ### 服务能力1: 发放优惠券 169 | 170 | 发放方式|描述 171 | ------------|------------ 172 | 同步发放|适用于用户点击领券等实时性要求较高的获取券场景 173 | 异步发放|适用于实时性要求不高的发放券场景,比如新用户注册发券等场景 174 | 175 | 发放能力|描述 176 | ------------|------------ 177 | 单张发放|指定一个优惠券类型ID,且指定一个UID只发一张该券 178 | 批量发放|指定一个优惠券类型ID,且指定一批UID,每个UID只发一张该券 179 | 180 | 发放类型|描述 181 | ------------|------------ 182 | 优惠券类型标识|通过该优惠券类型的身份标识发放,比如创建一个优惠券类型时会生成一个16位标识码,用户通过`16位标识码`领取优惠券;这里不使用自增ID(避免对外泄露历史创建了的优惠券数量) 183 | 优惠码code|创建一个优惠券类型时,运营人员会给该券填写一个6位左右的Ascall码,比如`SKR6a6`,用户通过该码领取优惠券 184 | 185 | ### 服务能力2: 后台撤销优惠券 186 | 187 | 撤销能力|描述 188 | ------------|------------ 189 | 单张撤销|指定一个优惠券ID,且指定一个UID,校验归属是否合法,之后撤销一张券 190 | 批量撤销|指定一个优惠券类型ID,且指定一批UID,每个UID撤销一张该券 191 | 192 | ### 服务能力3: 伴随订单系统进行状态变更 193 | 194 | | 变更类型 | 描述 | 195 | | -------- | ------------------------------------ | 196 | | 冻结 | 成单时对优惠券进行订单绑定占用 | 197 | | 解冻 | 用户未支付前取消订单,解冻归还优惠券 | 198 | | 核销 | 将已经冻结的优惠券标记为已使用 | 199 | 200 | | 变更能力 | 描述 | 201 | | ------------------ | ---------------------------------------------------- | 202 | | 单张冻结/解冻/核销 | 指定一个优惠券ID,一个订单 ID,更改优惠券状态 | 203 | | 批量冻结/解冻/核销 | 指定一个优惠券ID与订单 ID 的映射,进行优惠券状态变更 | 204 | 205 | ### 服务能力4: 列表查询优惠券 206 | 207 | 用户优惠券列表|子类|描述 208 | ------------|------------|------------ 209 | 全部|-|查询该用户所有的优惠券 210 | 可以使用|全部|查询该用户所有可以使用的优惠券 211 | -|适用于某个spu或sku|查询该用户适用于某个spu或sku可以使用的优惠券 212 | -|适用于某个类别|查询该用户适用于某个类别可以使用的优惠券 213 | -|适用于某个店铺|查询该用户适用于某个店铺可以使用的优惠券 214 | 已用|全部|查询该用户所有已经使用的优惠券 215 | -|正常使用|查询该用户所有履约完成的订单中使用的优惠券 216 | -|订单冻结|查询该用户所有未关闭订单中被冻结的优惠券 217 | 无效|全部|查询该用户拥有,但是不可用的优惠券 218 | -|过期|查询该用户所有过期的优惠券 219 | -|失效|查询该用户所有后台冻结的优惠券 220 | 221 | ### 服务能力5: 条件查询优惠券 222 | 223 | 有两种场景的条件查询 224 | 225 | 1. 商品/店铺展示页面可以展示此商品/店铺 已经领取/可以领取的券的集合 226 | 2. 下单时根据商品/店铺/支付方式过滤出当前可用的优惠券集合 227 | 228 | 注:活动页券集合一般会搭配相应的运营系统自定义,不在此列 229 | 230 | ### 服务能力6: 结算页优惠券推荐 231 | 232 | 订单结算页面推荐一张最适合(金额)该订单的优惠券 233 | 234 | 小结如下: 235 | 236 |

237 | 238 | 239 | 240 |

241 | 242 | 243 | ## 优惠券服务的风控怎么做? 244 | 245 | 一旦有发生风险的可能则触发风控: 246 | 247 | - 对用户,提示稍后再试或联系客服 248 | - 对内部,报警提示,核查校验报警是否存在问题 249 | 250 | ### 频率限制 251 | 252 | 领取|描述 253 | ------------|------------ 254 | 设备ID|每天领取某优惠券的个数限制 255 | UID|每天领取某优惠券的个数限制 256 | IP|每天领取某优惠券的个数限制 257 | 258 | 使用|描述 259 | ------------|------------ 260 | 设备ID|每天使用某优惠券的个数限制 261 | UID|每天使用某优惠券的个数限制 262 | IP|每天使用某优惠券的个数限制 263 | 手机号|每天使用某优惠券的个数限制 264 | 邮编|比如注重邮编的海外地区,每天使用某优惠券的个数限制 265 | 266 | ### 用户风险等级 267 | 268 | 依托用户历史订单数据,得到用户成功完成交易(比如成功妥投15天+)的比率,根据此比率对用户进行等级划分,高等级进入通行Unblock名单,低等级进入Block名单,根据不同用户级别设置限制策略。等其他大数据分析手段。 269 | 270 | ### 阈值 271 | 272 | - 发券预算 273 | - 实际使用券预算 274 | 275 | 根据预算值设置发券总数阈值,当触发阈值时阻断并报警。 276 | 277 | ### 优惠券不要支持虚拟商品 278 | 279 | 优惠券尽量不要支持虚拟商品以防止可能被利用的不法活动。 280 | 281 |

282 | 283 | 284 | 285 |

286 | 287 | -------------------------------------------------------------------------------- /src/promotion/glue.md: -------------------------------------------------------------------------------- 1 | # 通用抽奖工具(Glue万能胶) 2 | 3 | ## 抽奖需求分析 4 | 5 | 首先我们先来回顾下**营销体系**的组成: 6 | 7 | |营销体系| 8 | |---| 9 | |活动营销系统| 10 | |销售营销系统| 11 | 12 | 今天带来的是**活动营销系统**下的第一个独立子系统**通用抽奖工具**的介绍,本篇文章主要分为如下4部分: 13 | 14 | - 常见抽奖场景与归类 15 | - 抽奖需求配置 16 | - 常见奖品类型 17 | - 抽奖五要素 18 | 19 | ## 常见抽奖场景与归类 20 | 21 | 下面是我列出来的一些常见的抽奖场景,红包雨、糖果雨、打地鼠、大转盘(九宫格)、考眼力、答题闯关、游戏闯关、支付刮刮乐、积分刮刮乐等等活动营销场景。 22 | 23 | |活动名称|描述| 24 | |------|------| 25 | |红包雨|每日整点抢红包🧧抽奖,每个整点一般可参与一次| 26 | |糖果雨|每日整点抢糖果🍬抽奖,每个整点一般可参与一次| 27 | |打地鼠|每日整点打地鼠抽奖,每个整点一般可参与一次| 28 | |大转盘(九宫格)|某个时间段,转盘抽奖,每个场一般可参N次| 29 | |考眼力|某个时间段,旋转杯子猜小球在哪个被子里,猜对可抽奖,一般每日可参与N次| 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 | |红包雨|是|1|N| 62 | |糖果雨|是|1|N| 63 | |打地鼠|是|N|N| 64 | |幸运大转盘(九宫格)|否|N|N| 65 | |考眼力|否|N|N| 66 | 67 | 通过上面的分析我们得到了**活动**和**场次**的概念: 一个活动需要支持多场次的配置。 68 | 69 | - 活动activity:配置活动的日期范围 70 | - 场次session:配置每场的具体时间范围 71 | 72 | **红包雨的需求配置示例:** 73 | 74 | > 活动特征:红包雨需要支持多场次。 75 | 76 | 比如双十二期间三天、每天三场整点红包雨配置如下: 77 | 78 | 活动、场次配置: 79 | 80 | |双十二红包雨| 81 | |------| 82 | |活动配置:| 83 | |2019-12-10 至 2019-12-12| 84 | |场次配置:| 85 | |10:00:00 至 10:01:00| 86 | |12:00:00 至 12:01:00| 87 | |18:00:00 至 18:01:00| 88 | 89 | 奖品配置: 90 | 91 | |场次|奖品1|奖品2|---|奖品N| 92 | |------|------|------|---|------| 93 | |场次10:00:00 至 10:01:00|优惠券2元|空奖|---|无| 94 | |场次12:00:00 至 12:01:00|优惠券5元|空奖|---|无| 95 | |场次18:00:00 至 18:01:00|优惠券10元|优惠券20元|---|空奖| 96 | 97 | ```md 98 | 上面配置的结果如下: 99 | 100 | 2019-12-10日三场整点红包雨: 101 | 2019-12-10 10:00:00 ~ 10:01:00 102 | 2019-12-10 12:00:00 ~ 12:01:00 103 | 2019-12-10 18:00:00 ~ 18:01:00 104 | 105 | 2019-12-11日三场整点红包雨: 106 | 2019-12-11 10:00:00 ~ 10:01:00 107 | 2019-12-11 12:00:00 ~ 12:01:00 108 | 2019-12-11 18:00:00 ~ 18:01:00 109 | 110 | 2019-12-12日三场整点红包雨: 111 | 2019-12-12 10:00:00 ~ 10:01:00 112 | 2019-12-12 12:00:00 ~ 12:01:00 113 | 2019-12-12 18:00:00 ~ 18:01:00 114 | ``` 115 | 116 | **幸运大转盘的需求配置示例:** 117 | 118 | > 活动特征:幸运大转盘不需要多场次。 119 | 120 | 比如年货节2020-01-20 至 2020-02-10期间幸运大转盘配置如下: 121 | 122 | 活动、场次配置: 123 | 124 | |双十二幸运大转盘| 125 | |------| 126 | |活动配置:| 127 | |2019-12-10 至 2019-12-12| 128 | |场次配置:| 129 | |00:00:00 至 23:59:59| 130 | 131 | 奖品配置: 132 | 133 | |场次|奖品1|奖品2|---|奖品N| 134 | |------|------|------|---|------| 135 | |场次00:00:00 至 23:59:59|优惠券2元|空奖|---|无| 136 | 137 | ```md 138 | 上面配置的结果如下: 139 | 140 | 幸运大转盘抽奖活动将于 2019-12-10 00:00:00 ~ 2019-12-12 23:59:59 进行 141 | ``` 142 | 143 | 注意与思考:双十二幸运大转盘不需要多个场次,只配置一个场次即可,完全复用活动场次模型。 144 | 145 | ### 接着,第二类: `按抽奖次数抽奖`的需求配置 146 | 147 | |类型|活动名称|特点| 148 | |-|-|-| 149 | |按抽奖次数抽奖|答题闯关、游戏闯关|(成功参与)当前活动次数维度| 150 | 151 | **答题闯关的需求配置示例:** 152 | 153 | > 活动特征:每一关的奖品不同,一般越到后面中大奖的几率越大。 154 | 155 | 活动、场次配置: 156 | 157 | |双十二答题闯关| 158 | |------| 159 | |活动配置:| 160 | |2019-12-10 至 2019-12-12| 161 | |场次配置:| 162 | |00:00:00 至 23:59:59| 163 | 164 | 奖品配置: 165 | 166 | |双十二答题闯关|奖品| 167 | |------|------| 168 | |第一关|优惠券2元| 169 | |第二关|优惠券5元| 170 | |第三关|优惠券10元| 171 | |第四关|优惠券20元| 172 | |第五关|优惠券50元| 173 | |第六关|优惠券100元| 174 | 175 | 注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。 176 | 177 | ### 最后,第三类: `按数额范围区间抽奖`的需求配置: 178 | 179 | |类型|活动名称|特点| 180 | |-|-|-| 181 | |按数额范围区间抽奖|支付刮刮乐、积分刮刮乐|数额区间维度| 182 | 183 | **支付刮刮乐的需求配置示例:** 184 | 185 | > 活动特征:不同的订单金额,一般金额越大中大奖的几率越大。 186 | 187 | 活动、场次配置: 188 | 189 | |双十二答题闯关| 190 | |------| 191 | |活动配置:| 192 | |2019-12-10 至 2019-12-12| 193 | |场次配置:| 194 | |00:00:00 至 23:59:59| 195 | 196 | 奖品配置: 197 | 198 | |订单金额|奖品1|奖品2|---|奖品N| 199 | |------|------|------|---|------| 200 | |0~100|优惠券2元|空奖|---|无| 201 | |100~200|优惠券5元|空奖|---|无| 202 | |200~1000|优惠券10元|优惠券20元|---|空奖| 203 | |1000以上|优惠券50元|笔记本电脑|---|空奖| 204 | 205 | 注意与思考:同理活动&场次配置完全复用,同幸运大转盘配置(不需要支持多场次)。 206 | 207 | > 总结: 通过上面的分析我们得到了抽奖工具的两个要素**活动**和**场次**。 208 | 209 | 210 | ## 常见奖品类型 211 | 212 | > 抽奖抽什么? 213 | 214 | |常见奖品类型| 215 | |-| 216 | |优惠券| 217 | |积分| 218 | |实物| 219 | |空奖| 220 | 221 | > 总结: 我们得到了抽奖工具的另一个要素**奖品**。 222 | 223 | ## 抽奖五要素 224 | 225 | 通过上面的分析我们已经得到了抽奖的**三要素** 226 | 227 | - 活动 228 | - 场次 229 | - 奖品 230 | 231 | > 那还有什么要素我们还没聊到呢?接下来来看。 232 | 233 | ### 第四要素:中奖概率 234 | 235 | 抽奖自然离不开奖品的中奖概率的设置。关于中奖概率我们支持如下灵活的配置: 236 | 237 | 1. 手动设置奖品中奖概率 238 | 2. 自动概率,根据当前奖品的数量、奖品的权重得到中奖概率 239 | 240 | 比如我们某次大促活动红包雨的配置如下: 241 | 242 | 活动配置|描述 243 | ------|------ 244 | 活动时间|2019-12-10至2019-12-12 245 | 活动名称|2019双十二大促整点红包雨 246 | 活动描述|2019双十二大促全端整点红包雨活动 247 | 手动设置奖品概率|是 248 | 249 | |场次|奖品类型|具体奖品|奖品数量|中奖概率 250 | |-|-|-|-|-| 251 | |10:00:00 ~ 10:01:00|优惠券|2元优惠券|2000|50%| 252 | |-|优惠券|5元优惠券|1000|20%| 253 | |-|空奖|-|5000|30%| 254 | |12:00:00 ~ 12:01:00|优惠券|2元优惠券|2000|50%| 255 | |-|优惠券|5元优惠券|1000|20%| 256 | |-|空奖|-|5000|30%| 257 | |18:00:00 ~ 18:01:00|优惠券|2元优惠券|2000|50%| 258 | |-|优惠券|5元优惠券|1000|20%| 259 | |-|空奖|-|5000|30%| 260 | 261 | 备注:每轮场次中奖概率之和必须为100%,否则剩余部分默认添加为空奖的中奖概率。 262 | 263 | ### 第五要素:均匀投奖 264 | 265 | > 如何均匀的抽走奖品? 266 | 267 | 答案: 均匀投奖。 268 | 269 | 具体方式为拆分总奖品数量,到各个细致具体的时间段。以双十二幸运大转盘为例: 270 | 271 | |场次|奖品类型|具体奖品|奖品数量|中奖概率|投奖时间(默认提前5分钟投奖)|投奖数量 272 | |-|-|-|-|-|-|-| 273 | |00:00:00 至 23:59:59|优惠券|2元优惠券|2000|50%|-|-| 274 | |-|-|-|-|-|00:00:00|2000| 275 | |-|-|-|-|-|06:00:00|2000| 276 | |-|-|-|-|-|12:00:00|2000| 277 | |-|-|-|-|-|18:00:00|2000| 278 | 279 | 这里我们就得到了抽奖的**第五个要素:均匀投奖**。 280 | 281 | ## 需求总结 282 | 283 | 通过上面的分析,我们得到抽奖五要素如下: 284 | 285 | 抽奖五要素|要素名称 286 | ------|------ 287 | 第一要素|活动 288 | 第二要素|场次 289 | 第三要素|奖品 290 | 第四要素|中奖概率 291 | 第五要素|均匀投奖 292 | 293 | 同时我们通过**抽奖五要素**也得到了**通用抽奖工具**配置一场抽奖活动的5个基本步骤: 294 | 295 | 1. 活动配置 296 | 2. 场次配置 297 | 3. 奖品配置 298 | 4. 奖品中奖概率配置 299 | 5. 奖品投奖配置 300 | 301 | ## 通用抽奖工具系统设计 302 | 303 | 需求已经分析完了,今天我们就来看看这通用抽奖工具具体的设计,分为如下三个部分: 304 | 305 | - DB设计 306 | - 配置后台设计 307 | - 接口设计 308 | 309 | ## DB设计 310 | 311 | 第一要素`活动配置`的`抽奖活动表`: 312 | 313 | ```sql 314 | -- 通用抽奖工具(万能胶Glue) glue_activity 抽奖活动表 315 | CREATE TABLE `glue_activity` ( 316 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '活动ID', 317 | `serial_no` char(16) unsigned NOT NULL DEFAULT '' COMMENT '活动编号(md5值中间16位)', 318 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '活动名称', 319 | `description` varchar(255) NOT NULL DEFAULT '' COMMENT '活动描述', 320 | `activity_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖', 321 | `probability_type` tinyint(1) unsigned NOT NULL DEFAULT '1' COMMENT '中奖概率类型1: static 2: dynamic', 322 | `times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制', 323 | `start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动开始时间', 324 | `end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动结束时间', 325 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 326 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 327 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 328 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 329 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable', 330 | PRIMARY KEY (`id`) 331 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖活动表'; 332 | ``` 333 | 334 | 第二要素`场次配置`的`抽奖场次表`: 335 | 336 | ```sql 337 | -- 通用抽奖工具(万能胶Glue) glue_session 抽奖场次表 338 | CREATE TABLE `glue_session` ( 339 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '场次ID', 340 | `activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID', 341 | `times_limit` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖次数限制,0默认不限制', 342 | `start_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次开始时间', 343 | `end_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次结束时间', 344 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 345 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 346 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 347 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 348 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable', 349 | PRIMARY KEY (`id`) 350 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次表'; 351 | ``` 352 | 353 | 第三、四要素`奖品配置`的`抽奖场次奖品表`: 354 | 355 | ```sql 356 | -- 通用抽奖工具(万能胶Glue) glue_session_prizes 抽奖场次奖品表 357 | CREATE TABLE `glue_session_prizes` ( 358 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 359 | `session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID', 360 | `node` varchar(255) NOT NULL DEFAULT '' COMMENT '节点标识 按时间抽奖: 空值, 按抽奖次数抽奖: 第几次参与值, 按数额范围区间抽奖: 数额区间上限值', 361 | `prize_type` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型 1:优惠券, 2:积分, 3:实物, 4:空奖 ...', 362 | `name` varchar(255) NOT NULL DEFAULT '' COMMENT '奖品名称', 363 | `pic_url` varchar(255) NOT NULL DEFAULT '' COMMENT '奖品图片', 364 | `value` varchar(255) NOT NULL DEFAULT '' COMMENT '奖品抽象值 优惠券:优惠券ID, 积分:积分值, 实物: sku ID', 365 | `probability` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '中奖概率1~100', 366 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 367 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 368 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 369 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 370 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:disable, 1:enable', 371 | PRIMARY KEY (`id`) 372 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品表'; 373 | 374 | ``` 375 | 376 | 第五要素`均匀投奖`的`抽奖场次奖品定时投放器表`: 377 | 378 | ```sql 379 | -- 通用抽奖工具(万能胶Glue) glue_session_prizes_timer 抽奖场次奖品定时投放器表 380 | CREATE TABLE `glue_session_prizes_timer` ( 381 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 382 | `session_prizes_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '抽奖场次奖品ID', 383 | `delivery_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '定时投放奖品数量的时间', 384 | `prize_quantity` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '奖品数量', 385 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 386 | `create_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人staff_id', 387 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 388 | `update_by` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '修改人staff_id', 389 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:deleted, 0:wait, 1:success', 390 | PRIMARY KEY (`id`) 391 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='抽奖场次奖品定时投放器表'; 392 | 393 | ``` 394 | 395 | 其他表,抽奖记录&奖品发放记录表: 396 | 397 | ```sql 398 | -- 通用抽奖工具(万能胶Glue) glue_user_draw_record 用户抽奖记录表 399 | CREATE TABLE `glue_user_draw_record` ( 400 | `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '自增ID', 401 | `activity_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '活动ID', 402 | `session_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '场次ID', 403 | `prize_type_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '奖品类型ID', 404 | `user_id` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建人user_id', 405 | `create_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', 406 | `update_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', 407 | `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '状态 -1:未中奖, 1:已中奖 , 2: 发奖失败 , 3: 已发奖', 408 | `log` text COMMENT '操作信息等记录', 409 | PRIMARY KEY (`id`) 410 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户抽奖记录表'; 411 | ``` 412 | 413 | ## 配置后台设计 414 | 415 | ### 创建活动 416 | 417 |

418 | 419 | 420 | 421 |

422 | 423 | ### 创建活动场次 424 | 425 |

426 | 427 | 428 | 429 |

430 | 431 |

432 | 433 | 434 | 435 |

436 | 437 |

438 | 439 | 440 | 441 |

442 | 443 | ### 活动列表 444 | 445 |

446 | 447 | 448 | 449 |

450 | 451 | 452 | ## 接口设计 453 | 454 | 1. 获取活动信息 GET {version}/glue/activity 455 | 456 | 请求参数: 457 | 458 | 字段|类型|是否必传|描述 459 | ------------|------------|------------|------------ 460 | serial_no|string|Y|活动编号 461 | 462 | 响应内容: 463 | ```json 464 | { 465 | "code": "200", 466 | "msg": "OK", 467 | "result": { 468 | "serial_no": "string, 活动编号", 469 | "type": "number, 活动抽奖类型1: 按时间抽奖 2: 按抽奖次数抽奖 3:按数额范围区间抽奖", 470 | "name": "string, 活动名称", 471 | "description": "string, 活动描述", 472 | "start_time": "number, 活动开始时间", 473 | "end_time": "number, 活动开始时间", 474 | "remaining_times": "number, 活动抽奖次数限制,0不限制", 475 | "sessions_list":[ 476 | { 477 | "start_time": "number, 场次开始时间", 478 | "end_time": "number, 场次开始时间", 479 | "remaining_times": "number, 场次抽奖次数限制,0不限制", 480 | "prizes_list": [ 481 | { 482 | "name": "string, 奖品名称", 483 | "pic_url": "string, 奖品图片" 484 | } 485 | ] 486 | } 487 | ] 488 | } 489 | } 490 | ``` 491 | 492 | 2. 抽奖 POST {version}/glue/activity/draw 493 | 494 | 请求参数: 495 | 496 | 字段|类型|是否必传|描述 497 | ------------|------------|------------|------------ 498 | serial_no|string|Y|活动编号 499 | uid|number|Y|用户ID 500 | 501 | 响应内容: 502 | ```json 503 | // 中奖 504 | { 505 | "code": "200", 506 | "msg": "OK", 507 | "result": { 508 | "serial_no": "string, spu id", 509 | "act_remaining_times": "number, 本活动抽奖剩余次数,0不限制", 510 | "session_remaining_times": "number, 本场次抽奖剩余次数,0不限制", 511 | "prizes_info": 512 | { 513 | "name": "string, 奖品名称", 514 | "pic_url": "string, 奖品图片" 515 | } 516 | } 517 | } 518 | 519 | // 未中奖 520 | { 521 | "code": "401", 522 | "msg": "", 523 | "result": { 524 | 525 | } 526 | } 527 | ``` 528 | 529 | ## 结语 530 | 531 | 活动营销系统中的第一个字系统**通用抽奖工具**今天讲完了,希望对大家有一定的帮助或启示。 -------------------------------------------------------------------------------- /src/promotion/seckill.md: -------------------------------------------------------------------------------- 1 | # 秒杀服务 2 | 3 | ## 前言 4 | 5 | 做业务的都知道: 6 | 7 | > 做系统概念很重要 8 | 9 | 因为在同事和同事间沟通中,这些概念可以精准的告诉别人你想表达的。尤其是电商系统,众所周知电商里有很多的概念,比如Sku、Spu等。 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 | |概念1|活动| 35 | |概念2|场次的概念,场次是活动的子集| 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 | |限时| 74 | 75 | 把上面的维度按照运营需求组合就得到了不同的秒杀活动类型,如下: 76 | 77 | ### 首先,一元秒杀之类:白菜价+极少+(爆品或者非爆品)+限时 78 | 79 |

80 | 81 | 82 | 83 |

84 | 85 | ### 其次,限时购(又称常规秒杀):非白菜价+(极少或非极少)+(爆品或者非爆品)+限时 86 | 87 |

88 | 89 | 90 | 91 |

92 | 93 | ### 接着,爆品抢购:非白菜价+(极少或非极少)+爆品+限时 94 | 95 |

96 | 97 | 98 | 99 |

100 | 101 | |秒杀活动类型|营销维度| 102 | |-------|-------| 103 | |一元秒杀之类|白菜价+极少+(爆品或者非爆品)+限时| 104 | |限时购(又称常规秒杀) |非白菜价+(极少或非极少)+(爆品或者非爆品)+限时 -> | 105 | |爆品抢购|非白菜价+(极少或非极少)+爆品+限时| 106 | 107 | ## 一个简单的秒杀系统 108 | 109 | **实现原理:** 通过redis原子操作减库存 110 | 111 | **图一** 112 |

113 | 114 | 115 | 116 |

117 | 118 | 优点|缺点 119 | ------------|------------ 120 | 简单好用|考验redis服务能力 121 | 122 | |是否公平| 123 | |-------| 124 | |公平| 125 | |先到先得| 126 | 127 | 我们称这类秒杀系统为: 128 | 129 | > 简单秒杀系统 130 | 131 | 如果刚开始QPS并不高,redis完全抗的下来的情况,完全可以依赖这个「简单秒杀系统」。 132 | 133 | ## 一个够用的秒杀系统 134 | 135 | **实现原理:** 服务内存限流算法 + redis原子操作减库存 136 | 137 | **图二** 138 |

139 | 140 | 141 | 142 |

143 | 144 | 优点|缺点 145 | ------------|------------ 146 | 简单好用|- 147 | 148 | |是否公平| 149 | |-------| 150 | |不是很公平| 151 | |相对的先到先得| 152 | 153 | 我们称这类秒杀系统为: 154 | 155 | > 够用秒杀系统 156 | 157 | ## 性能再好点的秒杀系统 158 | 159 | **实现原理:** 服务本地内存原子操作减库存 160 | 161 | > 服务本地内存的库存怎么来的? 162 | 163 | 活动开始前分配好每台机器的库存,推送到机器上。 164 | 165 | **图三** 166 |

167 | 168 | 169 | 170 |

171 | 172 | 优点|缺点 173 | ------------|------------ 174 | 高性能|不支持动态伸缩容(活动进行期间),因为库存是活动开始前分配好的 175 | 释放redis压力|- 176 | 177 | |是否公平| 178 | |-------| 179 | |不是很公平| 180 | |不是绝对的先到先得| 181 | 182 | 183 | 我们称这类秒杀系统为: 184 | 185 | > 预备库存秒杀系统 186 | 187 | ## 支持动态伸缩容的秒杀系统 188 | 189 | **实现原理:** 服务本地协程Coroutine**定时redis原子操作减部分库存**到本地内存 + 服务本地内存原子操作减库存 190 | 191 | **图四** 192 |

193 | 194 | 195 | 196 |

197 | 198 | 优点|缺点 199 | ------------|------------ 200 | 高性能|支持动态伸缩容(活动进行期间) 201 | 释放redis压力|- 202 | **具备通用性**|- 203 | 204 | |是否公平| 205 | |-------| 206 | |不是很公平,但是好了点| 207 | |几乎先到先得| 208 | 209 | 我们称这类秒杀系统为: 210 | 211 | > 实时预备库存秒杀系统 212 | 213 | ## 公平的秒杀系统 214 | 215 | **实现原理:** 服务本地Goroutine**定时同步是否售罄**到本地内存 + 队列 + 排队成功轮训(或主动Push)结果 216 | 217 | **图五** 218 |

219 | 220 | 221 | 222 |

223 | 224 | 优点|缺点 225 | ------------|------------ 226 | 高性能|开发成本高(需主动通知或轮训排队结果) 227 | 真公平|- 228 | **具备通用性**|- 229 | 230 | |是否公平| 231 | |-------| 232 | |很公平| 233 | |绝对的先到先得| 234 | 235 | 我们称这类秒杀系统为: 236 | 237 | > 公平排队秒杀系统 238 | 239 | ## 骚操作 240 | 241 | > 上面的秒杀系统还不够完美吗? 242 | 243 | 答案:是的。 244 | 245 | > 还有什么优化的空间? 246 | 247 | 答案:静态化获取秒杀活动信息的接口。 248 | 249 | > 静态化是什么意思? 250 | 251 | 答案:比如获取秒杀活动信息是通过接口 `https://seckill.skrshop.tech/v1/acticity/get` 获取的。现在呢,我们需要通过`https://static-api.skrshop.tech/seckill/v1/acticity/get` 这个接口获取。有什么区别呢?看下面: 252 | 253 | 服务名|接口|数据存储位置 254 | ------|------|------ 255 | 秒杀服务|https://seckill.skrshop.tech/v1/acticity/get|秒杀服务内存或redis等 256 | 接口静态化服务|https://static-api.skrshop.tech/seckill/v1/acticity/get|CDN、本地文件 257 | 258 | **以前是这样** 259 |

260 | 261 | 262 | 263 |

264 | 265 | **变成了这样** 266 |

267 | 268 | 269 | 270 |

271 | 272 | 结果:可以通过接口`https://static-api.skrshop.tech/seckill/v1/acticity/get`就获取到了秒杀活动信息,流量都分摊到了cdn,秒杀服务自身没了这部分的负载。 273 | 274 | ## 完整流程方案① 275 | 276 | 完整流程主要涉及三个接口: 277 | 278 |

279 | 280 | 281 | 282 |

283 | 284 | |秒杀服务接口|对内还是对外|描述| 285 | |-------|-------|-------| 286 | |秒杀信息获取接口|对外|QPS要求高、所以可以直接对外 287 | |获取秒杀资格|对外|用户获取加入此商品加入购物车的资格| 288 | |校验并获取秒杀价格接口|对内|购物车接口校验资格,并返回该商品当前活动的秒杀资格| 289 | 290 | ## 完整流程方案② 291 | 292 |

293 | 294 | 295 | 296 |

297 | 298 | 这个方案和上面的有什么区别呢?答:把获取秒杀活动信息的接口统一收敛到了「营销中心」,目的: 299 | 300 | > 把所有的营销活动都抽象到一个「商品活动信息」的接口 301 | 302 | 这样,我们的商品详情页面的读的逻辑就很清晰,如下: 303 | 304 | - 第1类:商品基础信息接口,获取商品的基础信息(图片、名称、描述、价格、库存等等) 305 | - 第2类:商品活动信息接口,获取该商品参加的所有营销活动信息(满减、满赠、买送、秒杀等等) 306 | 307 | 图示: 308 | 309 |

310 | 311 | 312 | 313 |

314 | 315 | ## 总结 316 | 317 | 上面我们得到了如下几类`秒杀系统` 318 | 319 | |秒杀系统| 320 | ------------| 321 | |简单秒杀系统| 322 | |够用秒杀系统| 323 | |预备库存秒杀系统| 324 | |实时预备库存秒杀系统| 325 | |公平排队秒杀系统| 326 | 327 | 我想说的是里面没有最好的方案,也没有最坏的方案,只有**适合你**的。 328 | 329 | 拿`先到先得`来说,一定要看你们的产品对外宣传,切勿上来就追逐绝对的先到先得。其实你看所有的方案,相对而言都是“先到先得”,比如,活动开始一个小时了你再来抢,那相对于准时的用户自然抢不过,对吧。 330 | 331 | 又如`预备库存秒杀系统`,虽然不支持动态伸缩容。但是如果你的环境满足如下任意条件,就完全够用了。 332 | 333 | - 秒杀场景结束时间之快,通常几秒就结束了,真实活动可能会发生如下情况: 334 | + 服务压力大还没挂:根本就来不及动态伸缩容 335 | + 服务压力大已经挂了:可以先暂停活动,服务起来&扩容结束,用剩余库存重新推送 336 | - 运维自身不具备动态伸缩容的能力 337 | 338 | 所以: 339 | 340 | > 合适好用就行,切勿过度设计。 -------------------------------------------------------------------------------- /src/shopping/cart.md: -------------------------------------------------------------------------------- 1 | # 购物体系 2 | 3 | ## 购物车服务 4 | 5 | 对于一个电商来讲,购物车是整个购买流程最重要的一步。因为电商发展到今天购物车不仅仅只是为了完成打包下单的功能;也是收藏、对比、促销提醒、相关推荐的重要展示窗口。如此多的能力我们该如何设计保证购物车的高性能、以及良好的扩展能力来满足未来的发展呢? 6 | 7 | 今天开始我们就以一个假定的场景来输出一个购物车设计:某某电商平台,是一个多租户模式(我们前面的诸多设计都是多租户模式),用户可以把商品加入到购物车,并切按照商户纬度来展示、排序。当然购物车也支持常规的各种操作:选择、删除、清空、商品失效等。并且有相关的促销能够提醒用户。同时为了监控、运营,要支撑购物车数据同步到监控、数仓等能力。 8 | 9 | 本文会从用户使用的角度以及服务端两个角度来讲解系统的能力。本篇我们的主要目的是说清楚购物车的能力以及一些逻辑。下一篇会进行购物车模型设计以及接口定义。 10 | 11 | ### 用户视角 12 | 13 | 我们先来定义一下在用户侧用户操作购物车的功能有哪些? 14 | 15 | ![用户则需求](https://dayutalk.cn/img/user-cart-c.png) 16 | 17 | 一个购物车基本的能力基本上都在上图中,下面我们一一来分解。 18 | 19 | #### 操作 20 | 21 | 我们从用户的角度来看,购物车对于用户来说可以添加商品到购物车(加购物车、立即购买都属于一种添加方式);加入进购物车后,不想要了可以删除该商品(删一个、删多个、清空);想多买可以修改购买数量,发现钱不够可以减少购买数量;或者发现红色的比白色更漂亮,可以在购物车方便的进行更换规格;对于一些价格很贵的商品,能够在购物车添加一些保障服务(其实是绑定的虚拟商品);在要去结算的时候,还会提供选择能力让用户决定哪些商品真的本次要购买。 22 | 23 | 通过上面的描述我们可以看到这个过程是有其内在联系的。这里说一下关于选中功能,业界有两种做法,各有优劣,我们来看一下。淘宝的产品选中状态是保存在客户端的,并且默认不选中,刷新、重新打开APP状态会消失;京东、苏宁这一类是保存在服务端,会记录用户选中状态。针对这两种情况各有优劣。 24 | 25 | **客户端:** 26 | 27 | 1. 性能,选中/不选中的逻辑直接放在本地做,减少网络请求 28 | 2. 体验,多端不能同步,但是购物车相对来说更像是一个收藏夹,每次用户自己选择也无可厚非 29 | 3. 计算,价格计算时需要上传本地选中商品(也可以本地计算) 30 | 4. 实现,主要靠客户端实现,与服务端无关,研发解耦合 31 | 32 | **服务端:** 33 | 34 | 1. 性能,每次操作选中都需要调用服务端,而该操作可能很频繁,除了网络损耗,服务端也需要考虑该如何快速找到修改的商品 35 | 2. 体验,多端同步状态,记录历史状态 36 | 3. 计算,服务端可获取数据,请求时无须上传额外数据 37 | 4. 实现,服务端与客户端需要商定如何交互,以及返回数据(每次选中会导致价格变化),耦合在一起 38 | 39 | 个人认为这两种方式并无谁具备明显优势,完全是一种基于业务模式以及团队情况来做选择。我们这里后续的设计会基于在服务端保存商品选中状态。 40 | 41 | 在整个操作逻辑中,有个两个比较重要的地方单独说明一下:购买方式与购物车内修改购买属性 42 | 43 | ##### 购买方式 44 | 45 | 主要的购买方式有立即购买、加入购物车、拼团购三种方式。 46 | 47 | 首先普通的加入购物车没什么太多要说的。重点来看下立即购买与拼团。 48 | 49 | 立即购买在于操作上来说就是选择商品后直接到了订单确认页面,没有购物车中去结算这一步。但是它的实现却可以依赖购物车的逻辑来做,我们来看一下使用购物车与不使用购物车实现这个逻辑有什么差别? 50 | 51 | 如果使用购物车来实现,也就是用户点击立即购买时,商品本质上还是加入到购物车中,但这个购物车却与原型的购物车不同,因为该购物车只能加一个商品,并且每次操作都会被覆盖。在视角效果上也是直接从商品详情页面跳转到订单确认页面。来看看这种方式的好处 52 | 53 | 1. 与购物车在订单确认、下单逻辑上一致,内部可以直接通过购物车获取数据 54 | 2. 需要一个独立的专门用于一键购买的购物车来实现,内存有消耗 55 | 56 | 另外一种实现方式使用一个新的数据结构,因为一般来说一键购买更简单,它只需要商品信息、价格信息即可。每次交互均可以根据sku_id来获取。 57 | 58 | 1. 订单确认、下单逻辑上需要进行改造,每次请求之间要传递约定参数 59 | 2. 节省内存,上下交互通过sku_id来保证 60 | 61 | 我们会采用使用在服务端一键购买以独立的购物车形式来实现。购物车的数据模型一致,保证了后续处理流程上的一致。 62 | 63 | 对于拼团,他其实分为两部分,首先是开团这个动作,当团成立后。我们可以选择将成团的商品加入普通购物车,同时可以加购其它商品。也可以选择将成团商品加入一键购买的购物车,保证成团商品只能买一个。拼团模式更像是加入购物车的一个前置条件。本质上它对于购物车的设计没有影响。 64 | 65 | ##### 购物车内修改购买属性 66 | 67 | 这里主要是指可以在购物车便捷的操作一些需要在spu纬度操作的事情,比如:变更规格(也就是更换sku),以及选择绑定到spu纬度的服务(保险、延保等)。 68 | 69 | 我们重点说一下选择绑定的服务。例如:我们买一个手机,厂家提供了延保、各种其它附加服务,一般情况这种服务都是虚拟商品。但是这有个特殊情况。这些保障服务首先不能单独购买,其次他是跟主商品的数量息息相关。比如买两个手机,如果选择了加购服务,那么这些服务的数量必须是2,这会是一个联动关系。 70 | 71 | 这些保障服务是不能进行单独购买的,它一定要跟特定的商品捆绑销售。 72 | 73 | 服务端在存储这部分数据时一定需要考虑如何保存这种层级关系,这部分我们后面模型设计的时候大家会看到。 74 | 75 | ![绑定商品关系](https://dayutalk.cn/img/product-relations.png) 76 | 77 | #### 提醒 78 | 79 | 促销提醒很简单,返回的购物车数据,每一个商品应该携带当前的促销信息。这部分重点在于怎么获取促销信息,会在服务端看到。 80 | 81 | 然后说下购物车数量的提醒,也就是显示当前购物车商品的数量。一般来说进入到APP就会调用一个接口,获取用户的未读消息数、购物车商品数等。这里是需要非常高的读取速度。那么这种需求该如何满足呢? 82 | 83 | **方案一:** 我们可以设计一个结构保存了用户相关的这种提醒信息数量,每次直接读取这个数据即可。不需要去跟消息服务、购物车服务打交道拿这些数据。 84 | 85 | **方案二:** 在消息、购物车的模型中均设计一个保存总数量的字段,在读取数据的接口中,通过并发的方式调用这些服务拿到数据后进行聚合,这样在速度上只取决于最慢的服务。 86 | 87 | 这里我们的设计会采用 **方案二**,因为这样在某种程度上效率可以得到保证,同时整个系统的结构数据的一致性更容易得到保障。当然这里有个细节一定要注意,并发读取一定要设计超时,不要因为某个服务读数问题而导致拖累整个接口的性能。 88 | 89 | 接下来再来看看促销,这部分除了提醒,还需要提供对应的入口,让用户完成促销的操作。比如说某个商品有券,那么可以直接提供入口去领取;可凑单,有入口进入凑单列表并选择商品等。这部分需要解决的问题是服务端该如何及时从商品纬度拿到这些促销活动。 90 | 91 | 从用户的视角看完了,我们再来站在研发的角度看看服务端有哪些事情要做 92 | 93 | ### 研发视角 94 | 95 | 还是先来看看需求的汇总图: 96 | 97 | ![服务端则需求](https://dayutalk.cn/img/user-cart-s.png) 98 | 99 | #### 存储 100 | 101 | 对于存储,首选肯定是内存存储,至于要不要落库,我觉得没有必要。说下我的理由: 102 | 103 | 1. 购物车的数据相对变化非常频繁,落库成本比较高,如果异步方式落库,很难保障一致性 104 | 2. 极端情况,cache奔溃了,仅仅需要用户重新加入购物车,并且我们可以通过cache的持久化机制来保证数据的恢复 105 | 106 | 所以对于购物车,我们只会把数据完全保存在内存中。 107 | 108 | #### 商品销售类型发生变化 109 | 110 | 现在我们来讨论 **商品销售类型发生变化** 这个问题。这是什么意思呢?大家想一下:比如我把A商品加入到购物车,但是一直没有结算。这时运营说针对A商品搞一个活动,拿出10个库存5折购。那么问题来了,对于之前购物车中就有该商品的用户该如何处理?**这里解决的主要问题是:购物车有该商品的用户不能直接以5折买**。几种方案,我们来看一下: 111 | 112 | **方案一:** 促销配置后,所有购物车中有该商品的用户失效或删除,这个方案首先被pass,操作成本太高,并且用户体验差 113 | 114 | **方案二:** 购物车中要区分同一个SKU,不同销售类型。也就是说在我们的购物车中不是按照SKU的纬度来加商品,而是通过 **SKU+售卖类型** 来生成一个唯一识别码。 115 | 116 | 可以看到 **方案二** 解决了同一个sku在购物车并存的问题,并且库存之前互相不影响。不过这里又有一个问题?商品的售卖类型(或者说这个标记),该怎么什么地方设置?好像商品系统可以设计、促销系统也可以设置。我们的逻辑中会在促销系统中进行配置。因为商品属于基础逻辑,如果一改就是全局库存受到影响。活动结束后很难做到自动正常售卖。因此这个标记应该落到活动中进行设置(活动设置时会通过促销系统获取该商品之前的活动是否互斥,以确保配置的活动不会互相矛盾)。 117 | 118 | #### 依赖系统 119 | 120 | 购物车系统依赖了非常多的其它系统。 121 | 122 | - 商品系统 123 | - 库存系统 124 | - 促销系统 125 | - 结算系统 126 | 127 | 这些依赖的系统,有的是为了传输数据,有的是为了获取数据。我们按照这两个纬度来看一下。 128 | 129 | ##### 促销提醒与计算 130 | 131 | 服务端要解决的是促销的提醒与价格计算问题。 132 | 133 | 现来说计算,针对这部分最佳的方式是,调用结算中心的价格计算。我们来看一下购物车中的价格计算与订单结算时的价格计算的差异。 134 | 135 | 首先购物车中计算价格时不知道用户的地址,这会影响运费的计算;再是不知道用券的情况。那么其实如果解决了这两个问题,我们就可以让价格计算出自同一个逻辑,仅仅是部分入参不同罢了。因此我们这里计算时可以按照最高运费来计算,同时用券默认在购物车都不使用券。对于促销问题这里是可以通过促销系统确认选中的商品可以享受哪些价格的。因此促销的价格应该计算在内。 136 | 137 | 接下来在再来说说如何为用户高效的提供促销的信息。先从我们的配置视野出发。 138 | 139 | 我们在配置一个促销活动或者发一张券时,都是将多个商品归到一个促销活动或者券的下面。如果按照活动、券的纬度来获取商品效率相对比较高。 140 | 141 | ![活动-商品](https://dayutalk.cn/img/activity-product.png) 142 | 143 | 但是在购物车的场景中发生了一个变化。我们是需要从商品纬度获取到该商品的所有活动信息(全平台活动、店铺活动); 144 | 那么购物车中为了展示这些信息该怎么做?很常规的一个做法(也确实不少公司是这样):把所有活动信息取出来,遍历出所有跟该商品相关的信息。这种做法效率很低,并且无法满足大规模的应用场景,比如双十一期间。 145 | 146 | 因此这里为了满足该需求,促销系统需要提供一个能力按照商品获取对应促销(活动、券)。因此一般来讲促销系统配置的活动不能仅仅是按照活动纬度存储,同时还需要生成一份商品纬度的促销信息。 147 | 148 | ![商品-活动](https://dayutalk.cn/img/product-activity.png) 149 | 150 | #### 购物车数据分析 151 | 152 | 对于购物车数据来说,前端会通过埋点记录加入购物车数据的情况,但是前端埋点一般是记录触发了某个前端操作,但是并不知道该操作是否成功与否。以及无法及时了解当前整体购物车的数据情况。 153 | 154 | 为了让运营团队更完整的了解购物车当前情况,我们通过后端打本地日志,然后通过日志收集的方式将日志同步给数据、监控等服务。 155 | 156 | #### 失效与排序 157 | 158 | 还有两个小部分没有讲到,一是商品该如何失效,比如:库存没有了、下架了;二是购物车中的商品是多个店铺的,排序的策略是什么? 159 | 160 | 由于本文我们还只是讨论需求,不涉及具体的模型设计,因此只是介绍方案。首先是商品失效,这很像一个软删除操作,一旦设置,用户侧看到的商品将是无法进行结算的,只能进行删除操作。 161 | 162 | 对于排序我们会采用的设计是:根据某个店铺在购物车中最后发生操作的时间,最新的操作肯定在最上面。 163 | 164 | ### 结尾 165 | 166 | 通过上面我们基本上搞清楚了购物车设计中我们要做什么,依赖的系统要提供什么能力。下篇开始进入数据模型的设计、前后端接口设计。 167 | 168 | 如果你对购物车上面的需求还有哪些补充,欢迎留言。我们一起来完善。 169 | 170 | 171 | ## 购物车架构 172 | 173 | 在上一篇文章 [购物车设计之需求分析](https://dayutalk.cn/2019/12/09/%E8%B4%AD%E7%89%A9%E8%BD%A6%E8%AE%BE%E8%AE%A1%E4%B9%8B%E9%9C%80%E6%B1%82%E5%88%86%E6%9E%90/) 描述了购物车的通用需求。本文重点则在如何实现上进行架构上的设计(业务+系统架构)。 174 | 175 | ### 说明 176 | 177 | 架构设计可以分为三个层面: 178 | - 业务架构 179 | - 系统架构 180 | - 技术架构 181 | 182 | 快速简单的说明下三个架构的意思;当我们拿到购物车需求时,我们说用Golang来实现,存储用Redis;这描述的是技术架构;我们对购物车代码项目进行代码分层,设计规范,以及依赖系统的规划这叫系统架构; 183 | 184 | 那业务架构是什么呢?业务架构本质上是对系统架构的文字语言描述;什么意思?我们拿到一个需求首先要跟需求方进行沟通,建立统一的认知。比如:规范名词(购物车中说的商品与商品系统中商品的含义是不同的);建立大家都能明白的模型,购物车、用户、商品、订单这些实体之间的互动,以及各自具备什么功能。 185 | 186 | 在业务架构分析上有很多方法论,比如:领域驱动设计,但是它并不是唯一的业务架构分析方法,也并不是说最好的。适合你的就是最好的。我们常用的实体关系图、UML图也属于业务架构领域; 187 | 188 | 这里需要强点一点的是,不管你用什么方式来建模设计,有设计总比没设计强,其次一定要将建模的内容体现到你的代码中去。 189 | 190 | 本文在业务架构上的分析借助了 `DDD` (领域驱动设计)思想;还是那句话`适合的就是最好的`。 191 | 192 | ### 业务架构 193 | 194 | 通过前面的需求分析,我们已经明确我们的购物车要干什么了。先来看一下一个典型的用户操作购物车过程。 195 | 196 | ![用户旅程](https://dayutalk.cn/img/cart-sys-00.png) 197 | 198 | 在这个过程中,用户使用购物车这个载体完成了商品的购买流程;不断流动的数据是商品,购物车这个载体是稳定的。这是我们系统中的稳定点与变化点。 199 | 200 | 商品的流动方式可能多种多样,比如从不同地方加入购物车,不同方式加入购物车,生命周期在购物车中也不一样;但是这个流程是稳定的,一定是先让购物车中存在商品,然后才能去结算产生订单。 201 | 202 | 商品在购物车中的生命周期如下: 203 | 204 | ![过程](https://dayutalk.cn/img/cart-sys-01.jpg) 205 | 206 | 按照这个过程,我们来看一下每个阶段对应的操作。 207 | 208 | ![过程对应的操作](https://dayutalk.cn/img/cart-sys-02.jpg) 209 | 210 | 这里注意一点,加车前这个操作其实我们可以放到购物车的添加操作中,但是由于这部分是非常不稳定且多变的。我们将其独立出来,方便后续进行扩展而不影响相对比较稳定的购物车阶段。 211 | 212 | > 上面这三个阶段,按照DDD中的概念,应该叫做实体,他们整体构成了购物车这个域;今天我们先不讲这些概念,就先略过,后面有机会单独发文讲解。 213 | 214 | #### 加车前 215 | 216 | 通过流程分析,我们总结出了系统需要具备的操作接口,以及这些接口对应的实体,现在我们先来看加车前主要要做些什么; 217 | 218 | 加车前其实主要就是对准备加入的购物车商品进行各个纬度的校验,检查是否满足要求。 219 | 220 | 在让用户加车前,我们首先解决的是用户从哪里卖,然后进行验证?因为同一个商品从不同渠道购买是存在不同情况的,比如:小米手机,我们是通过秒杀买,还是通过好友众筹买,或者商城直接购买,价格存在差异,但是实际上他是同一个商品; 221 | 222 | 第二个问题是是否具备购买资格,还是上面说的,秒杀、众筹这个加车操作,不是谁都可以添加的,得现有资格。那么资格的检查也是放到这里; 223 | 224 | 第三个问题是对这个购买的商品进行商品属性上的验证,如是否上下架,有库存,限购数量等等。 225 | 226 | 而且大家会发现,这里的验证条件可能是非常多变的。如何构建一个方便扩展的代码呢? 227 | 228 | ![加车的验证](https://dayutalk.cn/img/cart-sys-03.jpg) 229 | 230 | 整个加车过程,重要的就是根据来源来区分不同的验证。我们有两种选择方式。 231 | 232 | 方式一:通过策略模式+门面模式的方式来搞定。策略就是根据不同的加车来源进行不同的验证,门面就是根据不同的来源封装一个个策略; 233 | 234 | 方式二:通过责任链模式,但是这里需要有一个变化,这个链在执行过程中,可以选择跳过某些节点,比如:秒杀不需要库存、也不需要众筹的验证; 235 | 236 | 通过综合的分析我选择了责任链的模式。贴一下核心代码 237 | 238 | ``` 239 | // 每个验证逻辑要实现的接口 240 | type Handler interface { 241 | Skipped(in interface{}) bool // 这里判断是否跳过 242 | HandleRequest(in interface{}) error // 这里进行各种验证 243 | } 244 | 245 | // 责任链的节点 246 | type RequestChain struct { 247 | Handler 248 | Next *RequestChain 249 | } 250 | 251 | // 设置handler 252 | func (h *RequestChain) SetNextHandler(in *RequestChain) *RequestChain { 253 | h.Next = in 254 | return in 255 | } 256 | ``` 257 | 258 | 关于设计模式,大家可以看我小伙伴的github:https://github.com/TIGERB/easy-tips/tree/master/go/src/patterns 259 | 260 | #### 购物车 261 | 262 | 说完了加车前,现在来看购物车这一部分。我们在之前曾讨论过,购物车可能会有多种形态的,比如:存储多个商品一起结算,某个商品立即结算等。因此购物车一定会根据渠道来进行购物车类型的选择。 263 | 264 | 这部分的操作相对是比较稳定的。我们挑几个比较重要的操作来讲一下思路即可。 265 | 266 | ##### 加入购物车 267 | 268 | 通过把条件验证的前置,会发现在进行加车操作时,这部分逻辑已经变得非常的轻量了。要做的主要是下面几个部分的逻辑。 269 | 270 | ![加入购物车](https://dayutalk.cn/img/cart-sys-04.jpg) 271 | 272 | 这里有几个取巧的地方,首先是获取商品的逻辑,由于在前面验证的时候也会用到,因此这里前面获取后会通过参数的方式继续往后传递,因此这里不需要在读库或者调用服务来获取; 273 | 274 | 其次这里需要把当前用户现有购物车数据获取到,然后将添加的这个商品添加进来。这是一个类似合并操作,原来这个商品是存在,相当于数量加一;需要注意这个商品跟现存的商品有没有父子关系,有没有可能加入后改变了某个活动规则,比如:原来买了2个送1个赠品,现在再添加了一个变成3个,送2个赠品; 275 | 276 | > 注意:这里的添加并不是在购物车直接改数量,可能就是在列表、详情页直接添加添加。 277 | 278 | 通过将合并后的购物车数据,通过营销活动检查确认ok后,直接回写到存储中。 279 | 280 | ##### 合并购物车 281 | 282 | 为什么会有合并购物车这个操作?因为一般电商都是准许游客身份进行操作的,因此当用户登录后需要将二者进行合并。 283 | 284 | 这里的合并很多部分的逻辑是可以与加入购物车复用的逻辑。比如:合并后的数据都需要检查是否合法,然后覆写回存储中。因此大家可以看到这里的关联性。设计的方法在某种程度上要通用。 285 | 286 | ##### 购物车列表 287 | 288 | 购物车列表这是一个非常重要的接口,原则上购物车接口会提供两种类型,一种简版,一种完全版本; 289 | 290 | 简版的列表接口主要是用在类似PC首页右上角之类获取简单信息;完全版本就是在购物车列表中会用到。 291 | 292 | 在实际实现中,购物车绝不仅仅是一个读取接口那么简单。因为我们都知道不管是商品信息、活动信息都是在不断的发生变化。因此每次的读取接口必然需要检查当前购物车中数据的合法性,然后发现不一致后需要覆写原存储的数据。 293 | 294 | ![购物车列表](https://dayutalk.cn/img/cart-sys-05.jpg) 295 | 296 | 也有一些做法会在每个接口都去检查数据的合法性,我建议为了性能考虑,部分接口可以适当放宽检查,在获取列表时再进行完整的检查。比如添加接口,我只会检测我添加的商品的合法性,绝不会对整个购物车进行检查。因为该操作之后一般都会调用列表操作,那么此时还会进行校验,二者重复操作,因此只取后者。 297 | 298 | #### 结算 299 | 300 | 结算包括两部分,结算页的详情信息与提交订单。结算页可以说是在购物车列表上的一个包装,因为结算页与列表页最大的不同是需要用户选择配送地址(虚拟商品另说),此时会产生更明确的价格信息,其他基本一致。因此在设计购物车列表接口的时候,一定要考虑充分的通用性。 301 | 302 | 这里另外一个需要注意的是:立即购买,我们也会通过结算页接口来实现,但是内部其实还是会调用添加接口,将商品添加到购物车中;有三个需要注意的地方,首先是这个添加操作是服务内部完成的,对于服务调用方是不需要感知这个加入操作的存在;其次是这个购物车在Redis中的Key是独立于普通购物车的,否则二者的商品耦合在一起非常难于操作处理;最后立即购买的购物车要考虑账号多终端登录的时候,彼此数据不能互相影响,这里可以用每个端的uuid来作为购物车的标记避免这种情况。 303 | 304 | 购物车的最后一步是生成订单,这一步最要紧的是需要给购物车加锁,避免提交过程中数据被篡改,多说一句,很多人写的Redis分布式锁代码都存在缺陷,大家一定要注意原子性的问题,这类文章网络上很多不再赘述。 305 | 306 | 加锁成功之后,我们这里有多种做法,一种是按照DB涉及组织数据开始写表,这适用于业务量要求不大,比如订单每秒下单量不超过2000K的;那如果你的系统并发要求非常高怎么办? 307 | 308 | 其实也很简单,高性能的三大法宝之一:异步;我们提交的时候直接将数据快照写入MQ中,然后通过异步的方式进行消费处理,可以通过通过控制消费者的数量来提升处理能力。这种方法虽然性能提升,但是复杂度也会上升,大家需要根据自己的实际情况来选择。 309 | 310 | 关于业务架构的设计,到此告一段落,接下来我们来看系统架构。 311 | 312 | ### 系统架构 313 | 314 | 系统结构主要包含,如何将业务架构映射过来,以及输出对应输入参数、输出参数的说明。由于输入、输出针对各自业务来确定的,而且没有什么难度,我们这里就只说如何将业务架构映射到系统架构,以及系统架构中最核心的Redis数据结构选择以及存储的数据结构设计。 315 | 316 | #### 代码结构 317 | 318 | 下面的代码目录是按照 `Golang` 来进行设计的。我们来看看如何将上面的业务架构映射到代码层面来。 319 | 320 | ```golang 321 | ├── addproducts.go 322 | ├── cartlist.go 323 | ├── mergecart.go 324 | ├── entity 325 | │   ├── cart 326 | │   │   ├── add.go 327 | │   │   ├── cart.go 328 | │   │   └── list.go 329 | │   ├── order 330 | │   │   ├── checkout.go 331 | │   │   ├── order.go 332 | │   │   └── submit.go 333 | │   └── precart 334 | ├── event 335 | │   └── sendorder.go 336 | ├── facade 337 | │   ├── activity.go 338 | │   └── product.go 339 | └── repo 340 | ``` 341 | 342 | 外层有 `entity`、`event`、`facade`、`repo`这四个目录,职责如下: 343 | 344 | **entity**: 存放的是我们前面分析的购物领域的三个实体;所有主要的操作都在这三个实体上; 345 | 346 | **event**: 这是用来处理产生的事件,比如刚刚说的如果我们提交订单采用异步的方式,那么该目录就该完成的是如何把数据发送到MQ中去; 347 | 348 | **facade**: 这儿目录是干嘛的呢?这主要是因为我们的服务还需要依赖像商品、营销活动这些服务,那么我们不应该在实体中直接调用它,因为第三方可能存在变动,或者有增加、减少,我们在这里进行以下简单的封装(设计模式中的门面模式); 349 | 350 | **repo**: 这个目录从某种程度上可以理解为 `Model`层,在整个领域服务中,如果与持久化打交道,都通过它来完成。 351 | 352 | 最后外层的几个文件,就是我们所提供的领域服务,供应用层来进行调用的。 353 | 354 | > 为了保证内容的紧凑,我这里放弃了对整个微服务的目录介绍,只单独介绍了领域服务,后续会单独成文介绍下微服务的整个系统架构。 355 | 356 | 通过上面的划分,我们完成了两件事情: 357 | 358 | 1. 业务架构分析的结构在系统代码中都有映射,他们彼此体现。这样最大的好处是,保证设计与代码的一致性,看了文档你就知道对应的代码在哪里; 359 | 360 | 2. 每个目录各自的关注点都进行了分离,更内聚,更容易开发与维护。 361 | 362 | #### Redis存储 363 | 364 | 现在来看,我们选择Redis作为购物商品数据的存储,我们要解决两个问题,一是我们需要存哪些数据?二是我们用什么结构来存? 365 | 366 | 网络上很多写购物车的都是只保存一个商品id,真实场景是很难满足需求的。你想想,一个商品id如何记住用户选择的赠品?用户上次选择的活动?以及购买的商品渠道? 367 | 368 | 综合比较通用的场景,我给出一个参考结构: 369 | 370 | ```golang 371 | // 购物车数据 372 | type ShoppingData struct { 373 | Item []*Item `json:"item"` 374 | UpdateTime int64 `json:"update_time"` 375 | Version int32 `json:"version"` 376 | } 377 | 378 | // 单个商品item元素 379 | type Item struct { 380 | ItemId string `json:"item_id"` 381 | ParentItemId string `json:"parent_item_id,omitempty"` // 绑定的父item id 382 | OrderId string `json:"order_id,omitempty"` // 绑定的订单号 383 | Sku int64 `json:"sku"` 384 | Spu int64 `json:"spu"` 385 | Channel string `json:"channel"` 386 | Num int32 `json:"num"` 387 | Status int32 `json:"status"` 388 | TTL int32 `json:"ttl"` // 有效时间 389 | SalePrice float64 `json:"sale_price"` // 记录加车时候的销售价格 390 | SpecialPrice float64 `json:"special_price,omitempty"` // 指定价格加购物车 391 | PostFree bool `json:"post_free,omitempty"` // 是否免邮 392 | Activities []*ItemActivity `json:"activities,omitempty"` // 参加的活动记录 393 | AddTime int64 `json:"add_time"` 394 | UpdateTime int64 `json:"update_time"` 395 | } 396 | 397 | // 活动 398 | type ItemActivity struct { 399 | ActID string `json:"act_id"` 400 | ActType string `json:"act_type"` 401 | ActTitle string `json:"act_title"` 402 | } 403 | ``` 404 | 405 | 重点说一下 `Item` 这个结构,`item_id` 这个字段是标记购物车中某个商品的唯一标记,因为我们之前说过,同一个sku由于渠道不同,那么在购物车中会是两个不同的item;接下来的 `parent_item_id` 字段是用来标记父子关系的,这里将可能存在的树结构转成了顺序结构,我们不管是父商品还是子商品,都采用顺序存储,然后通过这个字段来进行关联;有些同学可能会奇怪,为什么会存order id这个字段呢?大家关注下自己的日常业务,比如:再来一单、定金预售等,这种一定是与某个订单相关联的,不管是为了资格验证还是数据统计。剩下的字段都是一些非常常规的字段,就不在一一介绍了; 406 | 407 | > 字段的类型,大家根据自己的需要进行修改。 408 | 409 | 接下来该说怎么选择Redis的存储结构了,Redis常用的 `Hash Table、集合、有序集合、链表、字符串` 五种,我们一个个来分析。 410 | 411 | 首先购车一定有一个key来标记这个购物车属于哪个用户的,为了简化,我们的key假设是:`uid:cart_type`。 412 | 413 | 我们先来看如果用 `Hash Table`;我们添加时,需要用到如下命令:`HSET uid:cart_type sku ShoppingData`;看起来没问题,我们可以根据sku快速定位某个商品然后进行相关的修改等,但是注意,ShoppingData是一个json串,如果用户购物车中有非常多的商品,我们用 `HGETALL uid:cart_type` 获取到的时间复杂度是O(n),然后代码中还需要一一反序列化,又是O(n)的复杂度。 414 | 415 | 如果用`集合`,也会遇到类似的问题,每个购物车看做一个集合,集合中的每个元素是 ShoppingData ,取到代码中依然需要逐一反序列化(反序列化是成本),关于有序集合与链表就不在分析,大家可以按照上面的思路去尝试下问题所在。 416 | 417 | 看起来我们没得选,只有使用`String`,那我们来看一下`String`的契合度是什么样子。首先`SET uid:cart_type ShoppingDataArr`;我们把购物车所有的数据序列化成一个字符串存储,每次取出来的时间复杂度是O(1),序列化、反序列化都只需要一次。看来是非常不错的选择。但是在使用中大家还是有几点需要注意。 418 | 419 | 1. 单个Value不能太大,要不然就会出现大key问题,所以一般购物车有上限限制,比如item不能超过多少个; 420 | 2. 对redis的操作性能提升上来了,但是代码的就是修改单个item时的不便,必须每次读取全部然后找到对应的item进行修改;这里我们可以把从redis中的数据读取出来后,在内存中构建一个HashTable,来减少每次遍历的复杂度; 421 | 422 | 网上也看到很多Redis数据结构组合使用来保存购物车数据的,但是无疑增加了网络开销,相比起来还是String最经济划算。 423 | 424 | ### 总结 425 | 426 | 至此对于购物车的实现设计算是完结了,其中关于订单表的设计会单独放到订单模块去讲。 427 | 428 | 对于整个购物车服务,虽然没有写的详细到某个具体的接口,但是分析到这一步,我相信大家心中都是有沟壑的,能够结合自己的业务去实现它。 429 | 430 | 文中有些很有意思的地方,建议大家动手去做做看,有任何问题,我们随时交流。 431 | 432 | - 改编版的责任链模式 433 | - Redis的分布式事务锁实现 -------------------------------------------------------------------------------- /src/warehouse/README.md: -------------------------------------------------------------------------------- 1 | # 仓储系统 2 | 3 | 请您耐心等待... --------------------------------------------------------------------------------