更新记录
1.2.2(2023-01-19)
增加余额支付的支付密码页面。
1.2.1(2023-01-18)
云函数去掉金额单位转换。放到前端。
1.2.0(2023-01-14)
1 增加了店铺类型。加盟店、自营店。加盟店流水转入平台,自营店采用二维码收款。场景面向餐饮店或线下服务店铺。
2 店铺编辑页面增加了收款码上传的表单。
查看更多平台兼容性
云端兼容性
| 阿里云 | 腾讯云 | 支付宝云 |
|---|---|---|
| × | √ | × |
云函数类插件通用教程
使用云函数类插件的前提是:使用HBuilderX 2.9+
《短视频+商城+分佣 1.0 版本说明文档》
定位
短视频+电商+分佣系统;多商家商城;赚钱推广工具。
用户测试账号:微信授权即可体验。
商家测试账号和密码 zhang zhang1234
小程序二维码

H5 二维码

(H5不支持支付)
移动端部分页面截图

(1)

(2)

(3)

(4)

(5)
PC端后台

持续更新中...
一 游客功能(小程序端)
1.1 商家/用户使用微信免注册登录系统
1.2 查看推荐图文内容
1.3 查看图文的评论
1.4 查看店铺主页
1.5 查看商品详情(页)
1.6 分享商品到群/朋友圈
分享时会携带当前分享用户的身份标识,以便其它用户从分享链接进入系统下单时,系统能“记住”推广用户,从而给推广用户分佣。
1.7 查看商品评价
这里把 4-5 分归为好评,2-3 归为中评。1 分归为差评。
商品评价页面的图片用缩略图渲染,点击时可以放大查看原图。
1.8 播放/暂停视频
1.9 滑动切换上/下一个视频
1.10 查看视频评论
视频评论和推广图文评论规则一样。
1.11 分享视频到群/朋友圈
1.12 我要赚钱(查看所有商家的推广商品列表)
1.13 用户协议和使用条款
1.14 联系我们
1.15 问题与反馈
二 登录用户功能(小程序端)
2.1 扫码分销
扫码绑定推广关系
① 用户不能互相推荐:例如 A 用户推荐了 B 用户,B 用户又扫了 A 用户的码。
② 已经注册的用户,不能再绑定推广员了。
推广分佣【预留,未实现】
当前系统已经实现了扫码绑定上下级关系。但分佣时并未依赖扫码的绑定关系。
当前系统只有 1 种分佣规则,谁发布推广内容时挂在了商品,谁就是推广员。消费者从推广员的推广连接中下单,就会获得分佣。
2.2 账号资料设置(uni-id-pages 实现)
编辑头像
编辑昵称
绑定微信手机号
修改密码
注销账号
未完全实现,预留。
2.3 设置
退出登录
我的二维码
因小程序提供的二维码有数量和一些规则显示,这里使用的是普通二维码跳转小程序。如果是餐饮店商家,可以实现店铺码、桌码等功能。
2.4 下单购买商品
1 下单步骤:
① 根据选择的商品创建订单
② 创建订单时选择地址。
③ 计算邮费。
④ 使用/不使用优惠券。
⑤ 使用微信支付订单费用。
2 优惠券规则
优惠券为满减券,满 X 元减 Y 元。其中 X 元指的是本次订单的商品总额。
3 邮费
邮费规则由商家设定。详情见下文。邮费规则有 1 种方式是根据订单是否满 X 元包邮,不满 X 元则收取 Y 元邮费。其中的 X 元也是指本次订单的商品总额。
2.5 地址管理
新增/修改地址
微信小程序的精准定位需要申请 wx.getLocation 的 API 权限,审核通过后方可使用。目前比较难申请通过。因此这里的地址不是精准地址,详细地址需要手动填写。
查看创建地址列表
删除地址
2.6 申请开通店铺
用户填写店铺名称、上传证件即可申请。证件分为身份证正反面、营业执照。
2.7 订单管理
查看订单列表
展示不同订单状态下的订单。
用户订单中心展示的是用户关注的状态。待付款、待收货、待评价等。其它状态的订单可以在全部订单里查看。
取消订单
对未完成的订单进行取消,分两种情况:对于已支付订单需要先申请,等待商家同意退款后,才能取消;对于未支付订单,用户直接取消即可。
这里的取消订单是指,后一种情况。
不论那种情况,取消订单都会做一些回退操作。例如商品 SKU 库存返还、优惠券使用状态回退为未使用等。
申请退款
这里的申请退款,就是上述取消订单中的第一种情况。
取消退款
当用户对已支付、但未评价的订单申请退款时,也许某一时刻觉得不再需要退款了。那么可以撤销之前的退款申请。也就是这里的取消退款。取消退款后,商家看到的订单将不会是申请退款状态。
对未支付订单进行支付
确认签收订单
查看订单详情
删除订单
已完成的订单和取消状态的订单可以删除。
2.8 订单消息
总消息
详细状态消息
2.9 评价管理
包含以下功能。
1 对订单中的商品进行评价
2 查看我对所有商品的评价列表
3 对我评价的订单进行回复
2.10 收益管理
包含以下功能。
1 查看我的推广分佣收益
2 查看我的消费总金额
3 查看我的余额
4 查看我的资金流水记录
5 申请提现
2.11 内容管理
包含以下功能。
1 查看我发布的内容
2 查看我发布的内容相关的评论/回复消息
2.12 查看某个店铺的优惠券列表
已登录用户进入店铺主页时,可以查看该店铺对外设置的所有优惠券。包括普通优惠券、特定商品优惠券、特定用户优惠券。
2.13 发布推广内容
1 选择推广商品
如果当前角色为用户,则从商家推广的商品列表中选择推广商品。此时用户相当于推广员;如果当前角色为商家,则从自家发布的商品列表中选择商品作为推广商品。此时用户相当于为自己店铺的商品推广。
2 上传图片/视频
2.14 发表评论/回复
用户可以对图文/视频进行评论或回复。
2.15 对视频进行点赞/取消点赞
用户可以对视频进行点赞或取消点赞操作。
2.16 推广商品获得分佣
详情见佣金设计。
三 店铺功能(小程序端)
3.1 店铺编辑
店铺名称、门头图、营业时间、店铺显示风格、接单规则、邮费设计。
3.2 分类管理
包含以下功能。
1 添加商品分类
2 删除商品分类
3 查看商品分类列表
3.3 商品管理
包含以下功能。
1 查看商品列表
2 添加/编辑商品
3 删除商品
4 上架/下架产品
3.4 优惠券管理
1 新增优惠券
2 删除优惠券
3 修改优惠券
4 查询优惠券列表
3.5 订单管理
包含以下功能。
1 查询订单列表
2 手动接单模式下拒绝接单
3 同意用户退款申请
4 打印订单
5 对订单确认发货
6 对订单确认送达
7 删除订单
8 用户下单支付成功打印机提醒
3.6 收益账单
包含以下功能。
1 查看店铺的账户总收益
主要是 3 个指标:累计收益、即将到账、账户余额。
账户余额是,用户支付的订单金额在订单完成后到达商家账户的钱。
即将到账是冻结金额,用户支付后订单未完成前,钱都会在这里。起到一个锁定的作用。万一用户退款,则冻结金额返还。不会影响到商家账户余额。
累计收益,就是账户额的所有历史累加之和,不会递减。
2 查看我的资金流水记录
3 申请提现
提现需要扣手续费。
3.7 评价模板管理
包含以下功能。
1 创建评价模板
2 删除评价模板
3 查询商家模板列表
3.8 商品评价管理管理
包含以下功能。
1 查看评价列表
2 回复评价
3.9 打印机设置
1 查询我的打印机
四 PC 端管理员功能
4.1 店铺管理
包含以下功能。
1 查看申请的店铺列表
2 审核/拒绝店铺审核
4.2 内容管理
包含以下功能。
1 查看发布的内容列表
2 对已发布内容进行审核
4.3 打印机管理
包含以下功能。
1 为商家添加打印机
2 查看打印机里列表
3 修改打印机配置
4.4 意见反馈
1 查看意见反馈列表
4.5 文章管理
包含以下功能。
1 添加文章
2 查询文章列表
3 修改文章内容
4.6 首页显示
平台累计到账和平台收益余额
五 系统设计
5.1 数据库设计
| 表 | 说明 | 核心字段 |
|---|---|---|
| 核心表 | ||
| 用户表 | 使用系统提供的 uni-id-users 表 | 增加了几个字段 balance 余额 frozen_balance 待结算佣金 pay_total 累计支付 msg_count 推广内容未读消息 login_times 非严格统计的登录次数。 |
| 商家表 | 记录店铺信息。包含店铺信息(名称、图片、地址、营业时间)、邮费规则等。 | |
| 商品表 | 记录商品信息。商品不存价格和库存。价格、库存数量只存在 SKU 中。 商品中存了 sku 列表。 | name 商品名称 sku ---数组类型 category--数组(为了方便查询),存上下级, |
| SKU 表 | 每个 SKU 就是一个单品。记录每个商品中的所有单品列表。 | |
| 商品分类表 | 记录商品分类。只保存二级分类。 | |
| 订单表 | 记录商家和用户的订单信息。 | |
| 商家优惠券表 | 记录每个商家发放的优惠券。 | |
| 用户优惠券表 | 记录用户领取和使用优惠券的记录。 | |
| 推广内容表 | 记录用户发布过的图文、视频。 | |
| 推广内容评论表 | 记录每个推广内容的所有评论、回复。 | |
| 评价表 | 记录每个商品的评价。 | |
| 资金记录表 | ||
| 平台账户表 | ||
| 其它表 | ||
| 用户地址表 | ||
| 评价模板表 | ||
| 视频点赞记录表 | ||
| 系统配置表 | ||
| 文章内容表 | ||
| 商户打印机表 | ||
| 上传记录表 | ||
| 意见反馈表 | ||
| 其它系统表 | ||
| ... |
5.2 核心业务设计
5.2.1 SKU 设计
有些电商系统的设计里,会把 SKU 表做如下拆分。
商品表上有一个基础价格字段,具体的规格放在 SKU 里,如下结构:
| 颜色 | 温度 | 容量 | |
|---|---|---|---|
| 值 | 黄色:价格增量 0 红色:价格增量 0 | 常温:价格增量 0 冰冻:价格增量 0 加热:价格增量 0 | 大杯:价格增量+3 小杯:价格增量 0 |
用户的选择所有选择哪个 SKU,商品价格就累加上对应 SKU 的价格增量。这种方式的 SKU 表不算是一个纯粹的“商品”,更像是一个配置。用户选择后,订单表中存的是如下结构。
| 组合值 | ① 黄色常温大杯 ② 黄色常温大杯。③ 黄色冰冻大杯 ④ 黄色冰冻小杯。 ⑤ 黄色加热大杯 ⑥ 黄色加热小杯。⑦ 红色常温大杯 ⑧ 红色常温大杯。 ⑨ 红色冰冻大杯 ⑩ 红色冰冻小杯。11 红色加热大杯 12 红色加热小杯。 |
|---|
在实践中,发现此种方式的管理界面比较复杂,导致培训商家的学习成本高。而且现在很多电商专家都在呼吁做减法,把 SKU 减掉。
综合总总原因,为了简化操作,也为了让 SKU 表存储的是 1 个真正的 SKU。本系统在设计 SKU 时决定不再遵循常用做法。
本系统 SKU 的设计规则是:一个 SKU 设计为 1 个单品。商品表和 SKU 表的关系就是简单的 1 对多。商品表中的 sku 字段就是一个简单的数组,每一项都对应 sku 中的一条记录。
下面给出 1 个“奶茶”商品的存储案例。商品表 1 条记录,sku 表为 2 条记录。
| 商品表 | 商品字段 | SKU 表 | SKU 字段 |
|---|---|---|---|
| 奶茶 | _id:xxxx name、 skulist 数组存右边 images:[] 商品轮播图 没有价格和库存字段 | 大杯常温 | _id: sku 主键, name:”大杯常温” price:198 单位为分 num:999 库存, img: 单张图片对象。 |
| 小杯常温 | _id: sku 主键, name:”大杯常温” price:98 单位为分 num:999 库存, img: 单张图片对象。 |
这样,界面在展示的时候,就不需要分多组展示(颜色组、容量组、温度组),而是一组就够。价格计算时就不用再拿着商品的基础价格和 sku 的价格增量进行累加或者累减。而是把 sku 上的价格拿过来直接累加或者累减,就能让计算出订单中的商品总额。
总的来说,SKU 就是“降维设计”,把二维转化为一维。“降维”之后,在数据库设计、页面设计、编码实现、用户操作、商家培训、这五个方面,都得到了简化。
5.2.2 订单设计
1 什么叫做 1 个订单?
本系统是一个多商家商城系统,消费者可以在任意一个商家的商品列表下单。用户下单时,系统有多种选择:把不同商家的商品全部放在 1 个订单里;不同商家的商品分别生成 1 个订单。
如果把不同的商品全部生成在 1 个订单里,问题会变得复杂很多。例如,某一个商家退款时,平台就需要部分退款,其中涉及复杂的资金流向计算;用户取消订单或者订单完成时,系统为了区分不同商家的状态,需要在订单表中为每个商家维护 1 个状态。再加上,如果有优惠券,分佣时,还要单独计算出每个用户在不同商家的商品总额....这样要考虑的东西实在太复杂了。
因此,本系统定义的订单为:消费者在某个店铺中选择若干个商品的若干个 SKU 后,在支付前,系统为用户生成的订单信息。
上述描述包含以下隐形规则(也是设计初衷):
- 1 个订单,既属于用户,也属于商家。所以商家订单和用户订单是同一个表。
- 订单表和商品表、订单表和 SKU 表的数据结构。那就是 1 个订单中,包含多个商品(第一层),每个商品中包含了多个 SKU(第二层)。如此,下列结构就呼之欲出了。
{ product_list:[ { name:”商品 1”,sku:[{ },{ } ] } , { name:”商品 2”,sku:[{ },{ }] ] }
- 1 个订单只会包含 1 个商家的商品。
- 用户和商家都可以删除订单信息。现在的删除操作一般是逻辑删除。所以需要使用 2 个字段分别保存商家删除状态、用户删除状态。
- 订单支付前,如果有优惠券可以使用,通常可以使用优惠券进行抵扣。因此需要把优惠券信息也存到订单表中。
2 订单的状态改如何设计?
电商系统一个订单从开始到结束的通用流程为:
- 用户支付前,系统为其生成订单。
- 用户对订单进行支付。
- 商家接单。
- 商家开始发货/配送。
- 货物送达。
- 用户确认签收。
- 用户进行评价。
- 若用户对商品不满意,可以申请退单(退款)。
假设 status 为订单状态,我们用 1 个表格来梳理每个状态之间的转换关系。
| status 值 | 含义 | 场景 | 转换关系 |
|---|---|---|---|
| 0 | 订单取消 | 交易取消,无法转换。 | |
| 1 | 下单未支付 | 用户在支付时,放弃支付 | 用户可以继续支付,也可以取消订单。支付 status=2,取消 status = 0 |
| 2 | 已支付 | 用户支付成功后,支付回调里修改状态。 | 商家可以选择接单,也可以选择拒绝接单。接单 status=3,退单后 status 在退款成功的回调里把 status 设置为 0 用户可以申请退款,但需要商家同意后才能退款,因此申请退款不能修改 status,只能用其它字段来保存申请退款状态。只有商家同意退款后,在退款成功的回调里把 status 设置为 0。 |
| 3 | 商家已接单 | 商家手动接单后,状态变为已接单。 | 商家可以选择发货,status=4 用户可以申请退款,...(同上) |
| 4 | 商家已发货 | 商家发货后,状态变为已发货。 | 商家可以确认货物送达,状态变为已送达。status =5 |
| 5 | 货物已送达 | 商家确认货物送达,状态为已送达。 | 用户可以申请退款,......(同上) |
| 6 | 已签收 | 用户签收订单后,状态变为已签收。 | 用户可以申请退款,......(同上) |
| 7 | 已评价 | 用户对订单进行评价后,状态变为已评价。 | 交易完成,无法转换。 |
总结如下:
① 1\~6,用户可以进行退款。其中 1 不需要申请,因为未支付。2\~6 需要申请,且商家同意。
② 0 表示用户取消未支订单,商家同意退款(如果有支付的话)、商家拒单。这三种场景。
7 表示对用户而言表示评价完成,对订单而言表示订单完成。
③ 0 和 7 相当于交易易关闭的状态,不能再进行任何操作。
到这里订单状态已经可以满足正常使用了,但是用户申请退款、商家同意退款、用户撤销申请退款这 3 个操作,仍然需要记录下来。不可能再用 status 来记录,原因是用 status 会增加系统的复杂度。所以需要多用 1 个字段:apply_cancel_status
| apply_cancel_status 字段值 | 字段含义 | 说明 |
|---|---|---|
| -1 | 商家拒绝/不同意退款 | 该字段未使用。做预留。页面上也没有为商家提供拒绝退款的按钮。 |
| 0 | 默认,创建订单时默认为 0。 | 为了方便查询过滤,所以给了个默认值。 |
| 1 | 用户申请退款。 | 用户在 status > 1 && status \< 7 的情形下,都可以申请退款。 页面上已经做好了按钮的展示权限控制,只要用户看得到申请按钮就可以点击。只要一点击 apply_cancel_status = 1 |
| 2 | 商家同意退款。 | 商家看到 apply_cancel_status = 1 的订单后,可以点击同意退款。点击了之后,仅仅表示商家同意退款。不代表退款成功。要等到微信的退款回调执行后,才算是退款成功。而且就算商家一点机退款,能确保微信回调退款一定能成功,但在商家点击同意按钮的那一刻到退款成功回调那一刻,这期间是有一定的时间差的。因此需要一个中间状态,记录商家同意退款。 |
| 3 | 退款完成/成功/到账。 | 商家同意退款,商家拒绝接单,都会触发退款操作。本系统的退款操作是在微信退款回调种执行的。里面会进行一系列的校验,最终把 apply_cancel_status 设置为 3,status = 0 |
总结:
1 apply_cancel_status = 3 时,status 一定为 0。
2 用户在 status > 1 && status \< 7 的情形下,都可以申请退款。status = 1 不用申请退款,直接由用户操作取消。但是为了符合设计规则,对于 status=1 的订单用户取消后把 status=0,
apply_cancel_status 也被设置为 3。相当于退款成功。
3 apply_cancel_status=1 的情况下,用户可以撤销退款申请。撤销后 apply_cancel_status = 0
回到标题:订单如何设计?设计好以下字段,剩下的按需求填充即可。
| 字段名称 | 字段含义 | 值 |
|---|---|---|
| status | 订单状态。 | [0-7] |
| apply_cancel_status | 订单退款状态。默认 0 | -1、0、1、2、3。 |
| shop_del | true 商家已删除,false 未删除。默认 false。 | |
| user_del | true 用户已删除,false 未删除,默认 false。 | |
| product_list | 订单中包含的购买商品列表。 | |
| coupon_id | 使用的优惠券 id。默认没有这个字段。 |
5.2.3 评价设计
正常理解,订单支付完成后。对订单进行评价。但仔细思考,订单属于用户和商家,其它用户又看不到不属于自己的订单,评价主要是为了给其它用户提供购买参考。对订单进行评价有什么意义吗?
让我们来换个说法。对商家进行评价,或者对订单中的商品进行评价。这两个选择都是可以的,取决于需求。但一般而言,下单对象是商品,那么评价也应该针对商品。既然针对商品,那就面临 1 个问题:1 个订单中如果包含了 10 个商品,到底要评价几次?没错,是 10 次。如果不是 10 次,那么评价的意义也就不大了。
根据订单设计,我们知道。1 个订单只包含 1 个商家的多个商品,每个商品包含多个 SKU。本系统的评价设计就是:
- 评价只针对商品,不针对 SKU、也不针对商家、也不针对订单。
- 每个订单提供 1 个评价按钮,点击 1 次评价就跳转到评价页面。该评价页面里包含了多少个商品,就需要评价多少次,就生成多少条评价记录。只是页面上统一提交一次就行。
- 评价中使用到图片均上传到腾讯云 OSS 中(非 DCloud 后台的腾讯云空间),以便在列表展示时,对评价图片进行缩略图提取和压缩图片显示等操作。
因为评价标志着订单的完成,所以评价动作对系统来说,是一个必须进行的动作。如果用户不进行评价,系统定时器会进行默认评价。详情看定时器章节。定时器的自动评价和用户手动评价的逻辑是一样的。具体逻辑如下:
- 订单完成。
- 将推广用户的冻结佣金释放到可用余额。
- 将商家的冻结余额释放到可用余额。
- 将平台账户的冻结余额释放到可用余额。
- 商品评价数量+1。
- 计算商品的综合评分。
5.2.4 消息设计
本系统产生消息的场景有 3 类:推广内容消息、用户订单消息、商家订单消息。本系统并没有专用的消息界面,而是根据场景把需要显示的消息展示以数字角标的方式展示在对应菜单。
1 推广内容消息
当用户发布的推广内容被评论了,或者发布的评论被回复了,或者回复被回复了。都会产生 1 条消息。具体的实现方式是,在评论回复表中添加 2 个字段。
| 字段 | 说明 |
|---|---|
| read | false 表示消息未读,true 表示消息已读。 |
| notify_uid | 表示需要接收消息的用户 id。 并不是只要一有人评论/回复,就产生消息。例如,自己评论/回复自己,就是没必要的消息。 |
2 用户订单的某些重要状态改变了
用户关注的订单状态包含“待付款、待收货、待评价”三个。
3 商家订单的某些重要状态改变了
商家关注的重要订单状态包含“待接单、待发货、待送达”这 3 个,再加上“退款申请中”的状态。因为统计涉及 2 个字段,有重复消息,后台统计时需要对消息数量进行去重。
5.2.5 佣金设计(用户如何赚钱)
1 赚钱和商品的关系。
所有商家在编辑商品时,只要开通“加入推广”的功能,设置好分佣比例。该商品就会加入推广商品列表中,成为推广商品,对所有用户可见。
用户赚钱的意思类似抖音中的推广达人一样,把选择一款商品挂在视频左下角,然后通过发布视频进行推广,有人下单购买就产生佣金。只不过因为本系统的推广内容包含图文(类似朋友圈)、短视频。因此商品可以挂在段视频左下角,也可以挂在图文下方。此时发布推广内容的用户相当于一个推广员。
2 赚钱步骤
① 选择推广商品。
② 发布图文/短视频。(发布后也可以分享朋友圈或群)
③ 有用户从你发布的图文/短视频挂载的商品中进入商品详情,进行下单购买。
④ 用户付款成功,只是获得冻结佣金(预结算佣金)。
⑤ 如果用户退款,佣金退还。
⑥ 若订单完成,即用户主动评价或系统自动评价(详情见自动评价说明),则真正的佣 金到账。
⑦ 提现。
3 获得分佣的所有情况。
①A 用户发布的推广图文中包含了 S1 商家的 P1 商品,所有的消费者从 A 发布的图文点击进入到商品详情页下单购买时,A 用户都会得到佣金。分佣金额从 S1 的销售收入中分走。
②B 用户发布的推广视频包含了 S2 商家的 P2 商品,所有的消费者从 B 的视频左下角点击商品链接进入商品详情页下单购买时,B 用户都会得到佣金。佣金从 S2 的销售收入中分走。
③ C 用户从商品详情页分享了 S3 商家的 P3 商品到微信群或朋友圈,所有的消费者从 C 分享的商品链接进入商品详情页下单购买时,C 用户都会得到佣金。佣金从 S3 的销售收入中分走。
上述所有情形,均要求 A、B、C 用户在登录情况下操作。
4 不能分佣的情形
① 商家自己发布的图文、视频包含了自家的商品,此时商家虽然身份相当于推广员,但是其它用户下单时,商家自己不会获得分佣。原因很简单,商家也是用户。如果把钱再流入商家的用户表字段里既是多次一举,还增加了一些操作。还不如就让他留在商家账户里。
② 商家自己分享出去的链接,商家作为用户身份也不会产生分佣。
5 佣金的计算方式
推广员所得的分佣 = 券后商品总额 * 含有佣金的商品金额比例 * 分佣比例。
详细的案例见下文的(5.2.7 账户余额和收益设计)。
6 实际的下单用户从何而来?
由 2 个渠道来:本系统的运营方的用户群,推广用户。系统只是提供了灵活的推广方式而已,并没有把商业模式固化。具体如何运营完全取决于运营方。
5.2.6 餐饮系统的桌码设计
为了保证商城的通用性,本系统并未实现此功能。
若需要实现,则按照参考以下步骤即可:
① 在微信小程序中设置跳转规则。
② 在商家菜单中,增加添加桌码的页面。自定义桌码,把商家 id 和桌码 id 拼接在自定义域名上,然后用普通的二维码插件生成二维码。保存,打印即可。
③ 跳转规则设置。扫码后,可以设置跳转路径。直接指向商家主页即可。
④ 商家主页中解析参数。根据商品 id 加载商家信息。
⑤ 下单时,把桌码 id 保存到订单表中。方便后续关联小票打印机或者在订单详情页显示。
5.2.7 账户余额和收益设计
1 用户账户
主要在用户表(uni-id-users)中设计以下 3 个字段
| 字段名 | 说明 |
|---|---|
| balance | 可提现的账户余额。来自自推广佣金。提现需要收取手续费。手续费的费率由系统配置决定。 |
| frozen_balance | 冻结佣金。订单完成时,佣金释放到余额中。 |
| pay_total | 累计支付总金额。只会递增,不会减少。 |
2 商家账户
主要在商家表(shop)中设计以下 3 个字段
| 字段名 | 说明 |
|---|---|
| balance | 可提现的账户余额。来源于用户支付的订单金额的一部分或全部。提现需要收取手续费。手续费的费率由系统配置决定。 |
| frozen_balance | 冻结余额。订单完成时,冻结余额释放到账户余额中。 |
| receive_total | 累计收款金额。只会递增,不会减少。 |
3 平台账户
主要在平台账户表(platform-account)中设计以下 3 个字段
| 字段名 | 说明 |
|---|---|
| balance | 可提现的账户余额。来源于用户支付的订单金额的一部分或全部。提现需要收取手续费。手续费的费率由系统配置决定。 |
| frozen_balance | 冻结余额。订单完成时,冻结余额释放到账户余额中。 |
| receive_total | 累计收款金额。只会递增,不会减少。 |
4 费率问题
*店铺的账户余额 = 用户支付的钱 - 分佣的钱 - 平台手续费。
分佣的钱:如果某个订单不存在分佣,则分佣的金额为 0。
平台手续费:主要取决于运营方是否需要扣除手续费。本系统由费率决定,费率大于 0 就需要收取。
举个例子:平台 A、商家 B、用户 C 三个角色。用户 C 向商家 B 支付了 5 元,并不是真的 5 元到了平台 A 的商户号。而是 4.95 元,因为平台 A 存在费率(这里假设是 0.9%)。那么平台 A 实际的手续费为: 0.009*100*5*100 = 0.045 元,四舍五入并保留 2 位小数 = 0.05 元。因此平台 A 实际到账为 5 - 0.05 = 4.95 元。那么商家 B 的订单金额究竟是 4.95 还是 5 元。这个需要根据平台运营要求来定。
如果需要手续费,显示的有视 5 元,那么意味着提现的时候必须把费率转移到商家身上或者扣除相应的手续费。不然平台的费率就没有人来填补。
如果显示的是 4.95 元,也就意味着所有的费率均有商家出。那么请在运营阶段就告知商家这一规定。以免日后造成经济纠纷。
如果不需要商家出,那么可以直接显示 5 元。提现时,无需手续费即可。*
5 金额相关的定义
| 概念 | 说明 |
|---|---|
| 商品总额 | 订单商品中包含的所有 SKU 价格之和 |
| 优惠金额 | 优惠券的抵扣金额。详情见优惠券设计。 |
| 邮费 | 商家邮费规则中经过计算得到的费用。详情见邮费规则。 |
| 佣金 | 商家需要分给用户的推广佣金。详情看佣金设计。 |
| 实际支付 | 用户实际需要支付的钱,计算方式 实付 = 商品总额 + 邮费 - 佣金 - 优惠金额 |
| 手续费 | 提现需要的手续费。详情见上述 4。 |
还是举例,
A 消费者根据 B 推广员的链接在 C 店铺购买了 2 款商品,1 个单价 30 元的 P1 和 1 个单价为 50 元的 P2。P1 款佣金为 0,P1 佣金比例为 5%。B 店铺的邮费规则为不满 100,收取 10 元邮费。A 用户使用了 1 张优惠券(满 80 抵扣 5 元),最终所有相关的金额字段的值为多少?
商品总额 = 30 + 50 = 80 元。
优惠金额 = 5 元。
券后商品总额 = 75
邮费 = 10 元。
A 用户支付 = 75+10 = 85 元。
P2 推广佣金比例 = 5%,B 得到的预结算佣金 = 50/80 * 75 = 2.43 元。
C 店铺到账 = 85 - 2.43 = 82.57 元。
平台账户增加 85 元。
5.2.8 提现设计
1 提现规则。
用户、商家均可提现。并且都需要承担相应的手续费。否则手续费就要由系统运营方承担了。提现时如果调用 API 成功,则账户余额字段都会相应减少。具体的到账时间,以微信规则为准。
2 计算方式
到账金额 = 提现金额 - 手续费
手续费 = 提现金额 * 费率。
费率由管理员在 PC 端后台进行配置。
3 其它操作
提现会产生一条提现记录。
5.2.9 邮费规则
邮费必须在下单前提前知道。后知道邮费,补邮费的方式不仅对用户体验不好。而且计算金额时,也非常麻烦。下表总结了一些常用的设计规则。
| 策略 | 方案 | 规则 |
|---|---|---|
| 简单 策略 | 固定邮费 | 所有商品都一样。要么包邮要么固定邮费。 |
| 外卖 策略 | 定位计算 | 起步 x 元,超过 y 公里收 m 元/公里。结合特殊时段,区域,特殊天气等进行动态调整。 |
| 电商 策略 | 系统全局规则 | 指定订单金额满多少钱包邮,不满足就收取固定邮费。甚至是设置一些梯度规则。 |
| 分类级限定 | 全局的基础上,加上某些分类限定。 | |
| 产品级限定 | 上述基础,加上产品级规则。 | |
| SKU 级限定 | 上述基础,加上 SKU 级规则。 |
本系统并未设计的太复杂,而是选择了一种灵活配置的方式支持以下三种模式。并把邮费规则放入商品表中让商家进行选择。
| 支持的模式 | 说明 | 配置案例 |
|---|---|---|
| 固定邮费 | 固定 y 元,其中 y = 0 表示包邮。 | { mode:1, price:5, } |
| 外卖模式 | 按公里计算。x 公里内包邮,超过 x 公里按 y 元/公里。 | { mode:2, km:3.5, km_price:5, max_km:5 配送半径 } 3.5 公里内包邮,超过 3.5 公里按每公里 5 元收起。最大配送半径 5 公里。 |
| 最低邮费 | 订单消费满 x 元,邮费为 0。否则为 y 元。其中 y = 0 表示包邮。 | { mode:3, free_level:50, price:15, } 上述为满 50 元免费,否则收 15 元/单的邮费配置。 |
外卖模式下,若需要依赖 wx.getLocation 这个 API 来获取用户和商家的精准位置。该 API 目前被微信严格管控,审核难以通过。请考虑或放弃此模式。本系统涉及到模式 2 的代码也全部屏蔽。
5.2.10 优惠券设计
- 本系统实现的优惠券为满减券。满 x 元减 y 元。例如满 100 减 20 元,满 100 元的计算方法为:商品总额大于等于 100。
- 优惠券可以限定给某些商品使用。
- 优惠券可以限定给某些用户使用。
- 优惠券有生效日期和截止日期。
- 优惠券删除后,用户领取的优惠将会失效。
- 为了防止商家误操作,系统前端限定优惠券的抵扣金额最多为 30%。实际业务中,几乎也没有 30%如此高的优惠力度。
5.2.11 支付方式
目前仅支持微信支付,如需余额支付。需要先增加余额充值功能。
5.2.12 资金流水
本系统涉及到的所有资金流水,全部存入资金记录表中。
| 字段 | 说明 |
|---|---|
| change_money | 变动金额。可正可负。入账为+,支出为-。 |
| type | 用户相关的类型 1 用户充值。 2 用户下单支出。 3 用户得到佣金。 4 用户提现。 5 用户退款成功。 6 因订单取消而返还佣金。 [7-20]预留未使用。 商家相关的类型 21 商家收到商品支付款。 22 商家提现。 23 商家选择给用户退款。 ... |
| owner | 拥有者对象。type \<20 时是 user 对象。type >20 时是 shop 对象。 |
| status | 预留。当前未使用。 |
| comment | 流水说明。 |
| create_time | |
| order_id | 关联的订单 id。当前未使用。 |
5.2.13 没有购物车
购物车作用不大,因此没加。
若要添加需严格遵守订单设计规则。不能把不同商家的商品都放在 1 个订单里。因此展示时,建议以商家为单位进行归类展示。
5.2.14 没有商品分类和商品搜索
商品分类就静静躺在商品表里,但未使用分类搜索。商城泛滥的时代,分类和搜索作用不大。重点应当是如何让用户以最短的时间,最大的可能,最多的购物。除非再出一个大的平台。这仅仅是笔者的观点。
如需添加,可以单独添加 1 个某店铺的商品搜索页面。把分类页放进去检索。
5.2.15 点餐风格和电商风格
本系统设计之初,就是希望既能作为点餐系统,也能作为电商购物系统。因此做了两种 UI 风格。供参加选择,具体在店铺配置中选择。
5.2.16 接单模式
订单设计中提到了订单的 8 种状态:status = 0\~7 。这是比较通用的流程,但如果某些情况下希望简化流程。例如,不是所有的商家都有时间盯着手机去看有没有订单到来,有就接单,再点击一下接单按钮;也不是所有的商家都需要配送或者有时间去配送。有些商家希望,订单一来就马上到用户签收或者评价那一步。这都是可以的。
为了兼容传统的通用流程,也为了支持某些商家的特殊要求。本系统定义了一个概念,叫做接单模式。接单模式有两种:手动接单、自动接单。详细说明如下表。
| 模式 | 说明 |
|---|---|
| 手动接单 | 默认模式。即通用流程。 |
| 自动接单 | 自动接单,用户支付成功后,订单状态不是变为 2,而是变为 3。商家自动接单。如果希望变为 4、5、6,请参考 5.4.13 |
接单模式会作为店铺的基础配置,放在店铺编辑页面中。
5.3 UI 设计
5.3.1 UI 更新步骤
1.0 版本,界面再丑也要先自己做原型。此时你想当一个产品经理的角色,必须要多角度考虑交互设计、合理安排页面功能、控件。
全部功能完成后,让 UI 设计师理解产品功能,在原型基础上进行设计。
最后再把 UI 设计师好的部分吸收过来,不合适的部分舍弃掉。
5.3.2 页面规划
原则 1:
按照功能的重要程度,把最能提现自己的商业模式、项目差异化、系统的核心竞争力的功能放在用户最容易看到、最高频看到的地方。
优先级如下:
一级页面-->一级菜单-->二级页面-->二级菜单...
一级页面:无需跳转,核心功能能够在该页面全部体现出来。
一般认为,有底部 tab 的页面全是一级页面。
一级菜单:功能在二级页面,但是把跳转入口作为菜单按钮的方式放在一级页面。
二级页面:核心功能能够在该页面全部体现出来。
二级菜单:同上依次类推。
根据这个规则,本系统部分的页面安排如下。
一级页面:首页(tab)、视频(tab)、个人中心(tab)
一级菜单:我要赚钱(菜单按钮)、商家管理。
二级页面:订单详情、订单中心。
二级菜单:商品管理(商家角色)、商家订单中心(商家角色)、
...
原则 2:
迎合用户使用习惯。移动互联网发展已有十多年,成百上千万的移动端软件系统,已经教育也教会了用户的使用习惯。如果没有项目没有特殊的要求,预算也有限。尽可能以用户已经熟悉的交互方式来安排页面组件。创新过度的后果可能是用户也不知道如何使用系统,学习成本太高。增加推广压力。当然,内部使用的系统除外。
例如,下单购买按钮,不要一点击就立即下单,因为涉及到钱,最起码要让用户有考虑和选择的余地;取消订单、拒绝接单、接受订单、删除 xxxx 等重要功能的按钮,要让用户有后悔的余地,所以要设计一个弹窗提示,只有当用户点击确认按钮时才执行相应的逻辑。
有如,商城系统最重要的功能是下单。它的宗旨就是尽可能多的让用户下单。那么简化用户的下单流程,也能为实现这一伟大目标作出贡献。
...
总之,页面规划不应当是 UI 设计师的工作,应当是产品经理的职责。在项目初期就应当完成。
5.4 开发相关
5.4.1 云函数和 ClientDB 之争
笔者是从 JSP/Servlet,再到 SpringBoot、SpringCloud、再到 Serverless 一路走来的。如同不看好被淘汰的 JSP 一样,也不看好 ClientDB 的未来。尽管 ClientDB 被 DCloud 官方用了大量推广。
作为 java 全栈开发者,选择 Serverless 开发方式最重要的原因是,只需要专注写逻辑,无需担心并发、架构设计、部署等一系列麻烦的问题。
既然是专注写逻辑,那只是代表逻辑放在云函数了。并不代表前端代码和逻辑耦合在一起。但凡 Web 层、Dao 层、Service 层耦合在一起的项目,基本上规模都没办法做得很大。做的太大,很难维护。
因此笔者是一个保守派,只选择了官方认为的“传统”云函数作为后台进行开发。
5.4.2 云函数路由器
Java 的 springmvc 框架中有一个类叫做 dispatcherServlet,作用是 http 请求分发到每个 controller 实例中。我们把 dispatcherServlet 称作为路由器。
类似的,笔者既然选择了传统的云函数,同样也需要一个路由器来进行请求分发。和官方框架不一样的是,笔者喜欢大道至简的实现,并没有实现什么中间件,IOC 之类的功能。笔者的路由器仅仅实现最重要的 2 个功能:请求分发、角色粒度级的权限验证。
请求分发原理:开发时把逻辑封装到 1 个 js 文件中;前端统一请求后台的 1 个云函数(路由器),云函数把请求 url 映射为上述 js 的文件路径。再利用 node.js 的 require 动态加载机制加载这个 js 文件,并且执行。这样请求不同 url 就是执行不同的 js 文件。
我们暂且称呼这个 js 文件为 Servlet 吧。这个 js 文件有一个 doService 方法,作为请求 url 匹配时的执行入口。为了配合 node.js 的 require 机制,所以这个 js 文件结构应该如下:
module.exports = {
async doService( param ){
}
}
详情请看云函数 controller/router.js 的实现。
5.4.3 工程依赖关系
本系统有2个工程,1个是小程序工程。1个是PC端工程。
小程序工程---依赖腾讯云云空间。
PC 端工程---依赖“小程序工程”项目。
5.4.4 使用图片缩略图显示
为了提升加载速度,小程序中首页、商品列表、评价列表等需要展示多个图片的列表页面,均使用了缩略图技术。
具体是使用腾讯云的数据万象(Cloud Infinite,CI)中的图片处理功能。像图片转码、裁剪、质量压缩等接口。数据万象是付费服务,请酌情使用。
此外,此部分的图片需要依赖图片的分辨率(width、height)、大小(size)时,是上传时通过 image 的 load 事件来获取的。然后存入数据库中。这样渲染图片时,可以直接拿出来使用。
5.4.5 拖拽
因为需要对编辑的列表进行排序,所以小程序端增加了拖拽功能。在小程序工程的根目录的 component 目录中有一个 draggle-list 就是一个通用的实现。
拖拽不可滥用,因为负面影响较大:拖拽视野区的尺寸不能太接近屏幕尺寸,否则页面出现滚动时,难以滑动页面到其它部分。
5.4.6 放弃微信头像和微信昵称
从微信废除的 API 速度来看,通过微信授权来获取微信头像和微信昵称,一种不靠谱的行为。就算当下通过某种曲折的方式实现获取了,也难免以后会被废弃。既如此,那就不要再依赖微信头像和微信昵称了。这块本系统使用了 uni-id-pages 提供的功能。界面虽丑,基本功能完善。
5.4.7 Token 问题
官方在 uni-id-users 表的设计里,token 字段是一个数组,也就是允许多端、多地、多人登录。在某些情况下,用户并不会主动点击退出登录的按钮。如果客户端缓存被清除后,客户端的 token 也会被清除。此时数据库里的 token 依然在。因为官方不可能帮开发者实现一个后端的定时清除 token 的操作。因为官方也不知道开发者把 token 放哪里。资金充足的项目,开发者可能放在 redis 中。不充足的项目,可能就放在数据库中。如果这种情况比较频繁的发展下去,会出现一个糟糕的情况:用户表的 token 数组越来越长,几十、几百、甚至上千个。这在查询、传参时都会带来性能和流量的开销。
为了解决这种情况,本系统做了如下规则限制。最多允许 2 个 token 存在,超过 2 个 token 时,之前的 token 会被清除。
5.4.8 分包设计
小程序对分包的限制一定程度上影响了开发者的项目布局。为了彻底解决这个问题。建议所有开发者在开发项目时就考虑这个问题。
本系统在设计之初,笔者同样忘记了考虑分包的问题。后来不得已重构了项目目录。当前的系统分包如下:
| 包目录 | |
|---|---|
| /pages_admin 子包 | 放管理员操作页面 |
| /pages_shop 子包 | 放商家操作页面 |
| /pages_user 子包 | 放登录用户的操作页面。 |
| /uni_modules/uni-id-pages 子包 | 登录、注册、个人账户操作。原来就有的 |
| /pages 主包 | 放访客页面、用户和商家公共页面。 |
| 主包的其它目录 | 存放公共组件、库、资源。 |
5.4.9 字体使用
本系统使用平方体。1 个字体库大约 10MB 空间。为了节省首次加载时间,系统只是用了“PingFang SC Regular.ttf”这 1 个字体库。App.vue 中配置如下:
<style>
@font-face {
font-family: pf-r;
src: url('https://xxxxx/PingFang SC Regular.ttf');
}
.pf_r {
font-family: pf-r;
}
view,
text,
button,
input,
textarea {
font-family: "pf-r";
}
</style>
5.4.10 金额字段
本系统中但凡使用到金额的单位均使用分作为单位,参与计算。遵循以下规则:
- 页面展示,通过工具类转换为元。
- 数据库存储中使用分存储。
- 云函数计算使用分计算。
- 所有金额均保留 2 位小数。
5.4.11 打印机使用
本系统的在用户支付成功的回调里,会调用打印机打印小票。当前使用的是飞鹅 4G 版的小票云打印机。如果是奶茶店等场景,一般需要配置 1 个标签打印机+1 个小票机。
打印机是否需要,以及打印机的类型可以灵活选配。
5.4.12 定时器
本系统使用了 1 个定时器,详细信息如下。
| 云函数名称 | Timer |
|---|---|
| 云函数厂商 | 腾讯云 |
| 执行频率 | 5 分钟。 |
| 定时器配置 | [{"name":"timer","type":"timer","config":"0 */5 * * * * *"}] |
| 定时器功能 | 1 取消 5 分钟内支付的订单。 2 取消已经支付但 5 分钟,商家未接受的订单。 |
5.4.13 接单模式
5.2.15 提到了接单模式,如果自动接单模式下希望订单状态不是变为 3(商家已结单),而是变为 4(商家已发货)、5(商家确认已送达)、6(用户已签收)。可以在 order-utils 公共模块的 updateOrder 函数中进行配置。
5.4.14 获取视频第一帧画面
发布推广内容时,如果选择内容为视频。表明用户希望发布短视频来推广商品。此时为了让用户在滑动短视频时,较快地预览到内容。希望是先把封面给用户,再加载视频。这就需要获取视频的第一帧。或者其中某几帧。(短视频设计见视频滑动组件设计)
有 2 种方式获取:客户端获取和服务端获取。
客户端获取就是在上传之前获取。
对于 H5 获取视频第一帧,可以使用 canvas 标签结合相应的画布的 API 来获取。
小程序获取视频第一帧方式比较曲折。
对于安卓 APP 可以通过开发原生安卓插件来实现,对于 IOS 同样如此。
服务端获取就是在上传之后获取。要么自建服务器,要么使用第三方服务。
既然都是为了 Serverless 而使用 uniCloud,为了稳定,效率。本系统使用最简单的方式,腾讯云 OSS 的数据万象中的视频截帧接口(付费服务)。
腾讯云的数据万象的视频截帧接口使用流程如下:
- 小程序使用正常的 OSS 上传流程把视频上传到指定的 bucket 中。这一步和图片上传一样。
- 开通数据万象服务。
- 创建工作流。
- 配置视频截帧节点。截帧策略为从开始第 1 秒截取。总帧数 1。
- 填写回调模式为 http 回调。表示截取成功之后,通知 http 接口。
- 新建一个云函数,进行 url 化。把地址填写到 ⑤ 中的回调接口。
- 云函数中,根据回调参数获取截帧图片的实际地址。把地址写入数据库临时表 upload-temp 中。
- 小程序上传完成后,根据文件名定时去数据库查询 temp-upload 表。如果查得到视频封面则使用。
腾讯云的视频截帧并不是同步返回的,因为处理时间可能较长。因此使用工作流的方式来处理,处理完毕再以回调方式通知开发者。开发者需要临时保存,再做定时查询。所以视频上传界面要考虑到交互问题。
5.4.15 前端通用列表页实现
宇宙再大,皆有阴阳两面组成。系统再复杂,抽丝剥茧后,不过是增删改查而已。把页面分类,也就是详情页、编辑页、列表页三种页面。
详情页,通常根据主键 id 加载某一条记录的字段渲染在页面上。例如商品详情、店铺详情。
编辑页,通常是新增和修改页。例如发布推广内容、商品编辑。
列表页,通常是从数据库中加载出多条记录,以某种风格渲染出来。例如优惠券列表、订单列表。其中在管理类的功能菜单中,列表页占了绝大部分。例如本系统的首页图文、订单管理、内容管理、地址管理、评价列表、账单流水和商家中的几乎所有管理功能都是列表页。
我们这里重点讨论列表页,列表页不管显示风格如何,在数据层面,它都是一个一维数组。都需要刷新重新加载、都需要加载下一页。
如此高度重复的功能,应当抽离出 1 个组件或者 mixin 复用。
本系统的解决方案如下:
| 解决方案 | ||
|---|---|---|
| 前端 | list-mixin.js | 通用逻辑逻辑和数据结构封装。详情见/common/mixin/list-mixin.js |
| 前端 | scroll-view | 解决页面滑动、出现下拉条的问题。 |
| 后端 | 云函数分页 | 每个云函数都依赖 create_time 字段进行分页。 |
在实际实现过程中,由于小程序对插槽的支持比较受限制。因为在小程序端未能实现最简化的代码。依然保留着一部分的重复代码。
5.4.16 后端分页设计
为了在分页时能获得较快的查询效率,需要依赖递增、且唯一的字段。由于腾讯云的_id 字段不是自增的,因此被迫选择了 create_time 字段,create_time 是除了_id 之外最合适的选择。分页逻辑
let {param,page} = data;
if(param.create_time){
param.create_time = cmd.lt(param.create_time);
}
await table.where(param).orderBy("create_time","desc").limit(page.size).get();
影响分页的唯一因素:某些表中出现了 2 个或者多个 create_time 值相同的记录。而某一时刻,刚好相同的 create_time 值作为分页的临界记录时,create_time 相同的其它记录就会被过滤掉。
解决办法:
① 尽可能保证 create_time 的唯一性。尤其是云函数中批量插入时。要给每条记录的 create_time 要加上一定的偏移量。
② 不使用 create_time 字段,使用_id 字段分页,但用自定义的、确保自增且唯一的主键策略来赋值给_id。
5.4.17 前端请求封装
本系统封装了一个 request 函数,除了配合云函数路由器之外,还可以实现一些统一的处理。例如统一 loading,统一错误提示等。
详情见/common/api.js 的 request 函数。
5.4.18 多线程工具函数
不管是前端还是云函数,都有多线程的需求。本身如果不用 async 和 await,只用 promise 的 then 和 catch 写法,也能实现多线程的效果。但问题在于,既要同时执行多个线城,又要等待多个线程全部执行结束。也就是实现类似于 java 中主线程等待子线程执行完毕再返回后的功能。
这时只需要对 promise 进行封装即可。使用到的技巧是计数器。
函数定义:
runThread(func,success_times){
return new Promise((exe,reject)=\>{
let times = 0;
let checkTimes = async function(res){
times ++;
if( times == success_times){
exe( res );
}
}
func(checkTimes);
});
}
函数使用举例:
await runThread((check)=\>{
//1 添加评价。
ETable.add( rows ).then(check).catch(check);
//2商品的评价量+1
productTable.where({_id:cmd.in( product_ids )})
.update({comment_count:cmd.inc(1)}).then(check).catch(check);
//3发放分佣给推广员。
BalanceUtils.releaseUserFrozenPrize(order).then(check).catch(check);
//4释放商户冻结余额。
BalanceUtils.releaseShopFrozenBalance( order ).then(check).catch(check);
//5增加平台余额。
BalanceUtils.addPlatformBalance( order ).then(check).catch(check);
}, 5 );
以上实例代码见 Timer 云函数。本系统云函数中大量使用了此工具函数。
当然,这个工具函数不能滥用。某些情况下并不适合。
- 执行有前后依赖关系逻辑。多线程的特点就是随机性,不确定哪个线程先结束。有依赖的逻辑处理,请使用同步调用方式。
- 事务中。笔者亲自测试过,使用 runThread 函数执行 2-3 个操作,会导致事务繁忙。因此在事务操作中,还是老老实实地按照同步执行地步骤进行调用。
5.4.19 评论回复设计
评论回复是一种常用的功能,比较完善的系统都会支持以下三种基本操作:
① 对主体内容(文章、视频或者其它内容主体)进行评论。
② 对评论进行回复。
③ 对回复进行回复。
上述三点,其实就构成了一棵多叉树(别急,这里并非要用树结构)。想到树,就会有 parent_id 来关联父节点。所以下列一个这样的表结构就会被设计出来了。
| 表字段 | 字段说明 |
|---|---|
| _id | 主键 |
| article_id | 主题内容,假设这里是文章。那就是文章 id。 |
| content | 评论或回复内容 |
| parent_id | 上级 id。指向被回复的对象。 为 null 表明当前记录时一条评论。 不为 null 时表示针对评论或者针对回复进行恢复。被回复的具体内容,可以通过 parent_id 关联去找。 |
| 其它字段... | ... |
上述结构的确可以满足存储要求。但是假设某条评论的回复内容非常多,或者某条回复的回复比较多时。应当如何去加载这些回复呢?并且对查询还有一定的时间要求。
很明显,上述结构只是方便了存储,并不方便查询。我们不可能把主体内容的所有评论回复全部一次性加载到内存中,再生成树。再返回给客户端做假的评论展开渲染。
因此我们需要反向思考,能不能优先考虑先满足方便查询和快速查询这两个要求。再满足方便存储甚至是牺牲方便存储的特性。算法的问题,大不了用空间换时间,用冗余换取便利。
唯一方便查询的,那就是数组结构。
本系统的做法是:所有的评论是平级的,每一条评论下的所有回复都是 1 条单链表。新增回复总是插在目标内容的后面。
假设有一篇文章,7 个人分别在不同的时间评论如下:
| 模拟 | 时 间 | 排列 |
|---|---|---|
| 张三评论:“文章非常棒” | 第 1s | 张三:“文章非常棒” |
| 李四回复张三:“一般般吧” | 第 2s | 张三:“文章非常棒” 李四 ▶ 张三 “一般般吧” |
| 王五回复张三:“是很棒” | 第 3s | 张三:“文章非常棒” 王五 ▶ 张三:“是很棒” 李四 ▶ 张三:“一般般吧” |
| 赵六回复李四:“你行你上” | 第 4s | 张三:“文章非常棒” 王五 ▶ 张三:“是很棒” 李四 ▶ 张三:“一般般吧” 赵六 ▶ 李四:“你行你上” |
| 周七回复王五:“我也可以” | 第 5s | 张三:“文章非常棒” 王五 ▶ 张三:“是很棒” 周七 ▶ 王五:“我也可以” 李四 ▶ 张三:“一般般吧” 赵六 ▶ 李四:“你行你上” |
| 吴八回复赵六:“我上” | 第 6s | 张三:“文章非常棒” 王五 ▶ 张三:“是很棒” 周七 ▶ 王五:“我也可以” 李四 ▶ 张三:“一般般吧” 赵六 ▶ 李四:“你行你上” 吴八 ▶ 赵六:“我上” |
| 孙九回复张三:“什么眼神” | 第 7s | 张三:“文章非常棒” 孙九 ▶ 张三:“什么眼神” 王五 ▶ 张三:“是很棒” 周七 ▶ 王五:“我也可以” 李四 ▶ 张三:“一般般吧” 赵六 ▶ 李四:“你行你上” 吴八 ▶ 赵六:“我上” |
本系统的表设计如下
| 字段 | 说明 |
|---|---|
| content | 评论或回复的内容 |
| article_id | 文章 id |
| is_comment | 是否是评论。true 是,false 不是。 |
| comment_id | 评论 id。记录根评论 id。 |
| to_id | 回复对象 id。发评论时为 null。 |
| prev_id | 链表结构的上一个节点 id。添加评论时,会动态改变。 |
| next_id | 链表结构的下一个节点的 id。添加评论时,会动态改变。 |
| sort_index | 该内容在链表结构中的序号。0、1、2...相当于数组下标。在添加评论时会维护这个序号。以便分页查询。 |
| user | 用户对象 |
| ...其它字段 | ... |
查询评论写法。
前端传参
param = {
is_comment:true,
article_id:文章id
}
云函数
const db = uniCloud.database();
const cmd = db.command;
const table = db.collection("article-comment")
module.exports = {
async doService( data ){
let {param,page} = data;
let where = { ... param };
if(param.create_time){
where.create_time = cmd.lt( param.create_time );
}
let res = await table.where( where ).orderBy("create_time","desc")
.limit( page.size ).get();
return this.ok( res.data );
}
}
查询评论的所有下级回复时,就可以按照普通表一样分页查询。
前端传参
param = {
is_comment:false,
comment_id:评论id,
article_id:文章id
}
云函数
const db = uniCloud.database();
const cmd = db.command;
const table = db.collection("article-comment")
module.exports = {
async doService( data ){
let {param,page} = data;
let where = { ... param };
if(param.sort_index){
where.sort_index = cmd.gt( param.sort_index );
}
let res = await table.where( where )
.orderBy("sort_index","asc")
.orderBy("create_time","asc")
.limit( page.size )
.get();
return this.ok( res.data );
}
}
5.4.20 普通二维码跳转小程序
微信提供了普通二维码跳转到小程序的能力。调用微信接口生成的二维码有诸多限制。而普通二维码,只需要找一个 js 库就能随意生成。本质上是 1 个普通字符串转换为二维码。
因此生成普通二维码的方式来跳转小程序会方便很多。
详情在小程序后台的“开发设置/普通链接二维码地址详情”页面进行配置。
扫描该二维码,可以跳转到指定页面。然后在该页面解析参数。做相应动作。本系统当前实现的跳转规则只有 1 个。下表详细说明。
| 名称 | 状态 | 二维码 所在页面 | 携带参数 | 跳转路径 | 实现功能 |
|---|---|---|---|---|---|
| 用户 二维码 | 已实现 | /page_user/ mycode.vue | uid 即用户表 的主键 | /pages_user/home/ index | 用于绑定推广关系。仅仅是绑定推广关系,未通过推广关系来分佣。 |
| 商家码 | 预留 未实现 | 预留 未实现 | shop_id 即店铺表的 主键 | /pages_user/ucenter/shop/shop-detail | 用于扫码跳转到商家主页。创建订单时无需额外存储,因为本来订单表已经存了一个 shop 字段。 |
| 含有商家码的桌面二维码 | 预留 未实现 | 预留 未实现 | shop_id table_id 商家自定义的桌子编号字符串。 | 扫桌码的目的也是为了去到商家主页点餐,所以跳到主页。 /pages_user/ucenter/shop/shop-detail | 用于下单时,记录是那一桌的客人下的单。创建订单时需要把该 table_id 也放入订单记录中。订单记录表有一个 ext_param 作为扩展参数预留,可以放在里面。 |
微信的“普通链接二维码地址详情”配置中,二维码规则这一项有一个容易犯的错。
假设域名 abc.com,我们希望为每个用户生成 1 个二维码,用户 id 是可变参数。
| 配置方式 | 二维码规则 | 说明 |
|---|---|---|
| 错误配置 | https://abc.com/user?uid=xxx | uid 是可变的,但是微信不支持这种方式。并不会把“https://abc.com/user?”当作是前缀去匹配 “https://abc.com/user?uid=123”所以用这种方式生成的二维码,微信是不会跳转到小程序的。 |
| 正确配置 | https://abc.com/user?type=go&uid=xxxx | 这里把...?type=go 作为前缀。不一定是 type=go,只是要配一个参数。a=b 也可以。 \&uid=xxx 中的 uid 值是变化的。生成的二维码才有用。微信才能匹配到。 |
5.4.21 推广码绑定用户规则
常用推广码绑定用户的方式
| 方式 | 实现原理 |
|---|---|
| 邀请码绑定 | 系统在用户注册成功后,为每个用户分配一个唯一的,通常是 4-8 位的字符串作为邀请码(invite_code),当然也可以用手机号、工号等作为邀请码。系统在进行推广时,待注册用户需要手动输入 invite_code 才有用。 这种方式,笔者称之为显性绑定。因为需要手动输入。常见于信用卡推广、地推促销等场景。 |
| 扫码绑定 | 系统为已注册用户生成一个二维码,包含了当前的用户 id。待注册用户扫描该二维码时,跳转到注册页面。注册成功后,逻辑同上。 该方式笔者称之为隐形绑定。 |
| 链接绑定 | 系统为用户生成一个带当前用户 id 的链接,用户把该链接分享出去。其它用户点击该链接则进入注册页面。其余逻辑同上。 该方式笔者称之为隐形绑定。此种方式不仅可以用于绑定推广员,也可以用于分销。本系统中,从分享链接中下单时推广员获得分佣就是采用这种方式。 |
5.4.20 中提到了用户二维码的实现。扫描用户二维码就能跳转到/pages_user/home/index 页面,在该页面的 onLoad 中解析出 uid,然后保存到小程序客户端缓存。
当用户进行注册时,把 uid 随注册参数一起写入。这样二维码中的 uid 就顺其自然地写入新注册用户表中的 inviter_uid 字段中。至此就完成了推广员和用户之间的绑定工作。
这是常规逻辑,但是本系统依赖了 uni-id-pages 来完成用户注册、退出等基本工作。为了不改动源代码,笔者选择了在登录后进行绑定。
所以本系统的推广码绑定用户的云函数代码逻辑在/controller/Service/login/getUserInfo.js 中。
5.4.22 视频滑动组件设计
本系统采用了 swiper + video +image 的方式来实现视频滑动组件。其结构如下:
<template>
<view>
<swiper>
<swiper-item v-for=”(item,index) in videolist”>
<image v-if="current>= index -1 && current <= current +1" ></image>
<video v-if=”current ==index”></video>
</swiper-item>
</swiper>
<CommentList></CommentList>
</view>
</template>
基本原理是另 swiper 高度满屏,image 放视频的封面,video 放视频内容。因为视频一般比封面加载时间更长。所以先展示封面。
CommentList 是该视频下的所有评论回复组件。包含了发布评论、评论展示列表、回复展示列表等功能。该组件以弹窗形式打开,在切换视频时,重新加载评论回复列表。
具体的控制还需要结合事件。详情见/pages/video/index.vue
5.4.23 上传图片组件封装
图片上传比较高频使用,笔者根据使用场景封装了简单的 upload-images 组件。
| 组件 | 差异 |
|---|---|
| upload-images | 存放位置是云空间直接关联的腾讯云存储中。通过 uniCloud.upload 直接上传。直接展示。 |
| sync-upload-images | 同上。不用于展示,只用于封装上传 API。 |
5.4.24 关于图片字段的设计
本系统中涉及到图片内容的字段,经常被设计为图片对象或对象数组类型。
| 类型 | 数据结构 |
|---|---|
| 图片对象 | { width: 图片宽度,上传时@load 事件获取。 height: 图片高度,上传时@load 事件获取。 size: 图片大小,选择图片时获取。 src: 图片最终路径,上传后获取。 } |
| 对象数组 | [{上述结构},{ },{ }] |
如果场景是多图片,则使用数组。
这样设计的目的是为了在需要时,可以根据图片的宽高、大小进行相应的显示或压缩、裁剪、格式转换等。
5.4.25 关于角色权限的设计
uni-admin 框架在 PC 菜单层面已经实现了角色-权限控制的功能。
而小程序工程中的云函数,并未实现权限粒度的控制。但可以实现角色和 url 之间的匹配。
具体配置在/controller/config.js 中配置
module.exports = {
name: "HelloWorld",
roles: {
guest: ["guest\**"],
admin: ["admin\**"], //需要拥有该角色才能访问的URL
login: ["user\**","login\**","pay\**"], //只需要登录用户就有权限访问的URL。
shop_owner: ["shop\**"] //只需要登录用户就有权限访问的URL。
}
}
roles 中的 url 规则如下:
1.左边的 key 为角色 key。
2.右边的数组为有权限访问的后端 js 文件(接口或者 Servlet)。
3.特别的,guest 表示未登录用户。该配置表示未登录用户可以访问哪些 url。login 表示登录用户。该配置表示登录用户可以访问哪些 url。
4.匹配规则实现了模糊匹配。但并未实现优先级。默认的优先级规则为:写在最前面的配置的先生效。
5.模糊匹配举例:
* 匹配全部
user/* 匹配 user/insert 、user/xxxx、
user/** 匹配 user/insert/xxxx 、user/xxx/xxxxx
user/*search 匹配 user/usersearch 、user/xxxsearch
user/do* 匹配 user/doGet 、user/doPost user/doxxx。
5.4.26 富文本编辑器
小程序端未使用富文本编辑器,小程序端在 2 个页面使用 rich-text 渲染富文本:
一个是联系我们;一个是配置/《用户协议和使用条款》。
PC 端使用了富文本编辑器进行编辑。这里使用的是 wangEditor。安装如下:
若未安装 cnpm 先安装:
npm install -g cnpm --registry=https://registry.npm.taobao.org
安装完 cnpm 后执行以下:
cnpm install @wangeditor/editor --save
cnpm install @wangeditor/editor-for-vue@next --save
详情见https://www.wangeditor.com/v5/for-frame.html#%E4%BD%BF%E7%94%A8-1
5.4.27 支付回调
本系统的支付回调为 paynotify 云函数。该云函数已经进行 URL 化。
支付回调中主要完成的逻辑如下:
- 根据商家的接单规则修改订单状态。若为手动接单,则状态改为已支付(status = 2);若为自动接单,则状态改为商家已接单(status = 3 )。
- 增加商家的冻结余额。
- 增加推广用户的冻结佣金,如果该订单产生分佣的话。
- 增加平台的冻结余额。
- 增加资金流水记录。
- 更新用户的支付费用字段。
- 增加商品的销量。
- 调用小票/标签云打印机,打印订单信息。
支付回调的逻辑放在非事务中执行。
5.4.28 退款回调
本系统的退款回调为 refundnotify 云函数。该云函数已经 URL 化。
退款回调中完成的逻辑如下:
- 更新订单状态。上文也说过,退款成功后,status=0,apply_cancel_status=3,所以主要是修改这 2 个状态。顺带更新一下订单的流程信息。详情见订单表的 process_text 字段。
- 库存返还。
- 优惠券返还。
- 取消商家的冻结余额。
- 取消平台的冻结余额。
- 取消用户的推广佣金。
- 增加相应的资金流水记录。
退款逻辑放在事务中进行。
5.4.29 关于库存
1 库存管理?
本系统并未实现进销存管理的功能。因此不存在库存管理。
只是简单的修改 SKU 的库存而已。
2 什么时候减库存?
电商系统有多种方案:一种是下单就减库存;一种是付款成功再减库存;一种下单先扣除,超时取消订单再返还或者手动取消时返还。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 1 下单减库存 | 用户体验好。 | 如果有人恶意下单。那就是灾难。 |
| 2 付款减库存 | 能过滤一部分恶意下单用户。 | 用户体验较差。因为未支付前,看不到真实的剩余库存。 |
| 3 先扣除 超时返还 | 用户体验好,一定程度上过滤恶意下单的用户。 | 需要额外的系统开销来检测订单。系统定时器就是一种额外的开销。 |
本系统使用方案三。
5.4.30 事务 VS 非事务
事务的原子性、一致性,能够解决很多复杂的问题。但凡一次性操作很多个表,同时存在更新数据,又要确保要么同时成功、要么同时失败的场景,就必须使用事务。否则系统访问量多了之后一定会出现 bug。
但是事务也有缺点,尤其是云厂商提供的事务机制。对很多开发者来说几乎是暗箱操作。这里笔者给出自己使用事务的一点经验。
- 某些校验能放在事务之前,就放在事务之前。
- 事务中只使用 doc(),不使用 where。如果没有_id,那就在进入事务前把_id 查出来,或者把_id 在需要的字段中。
- 事务中使用同步去操作。不要用异步执行(多线程的方式)操作多个表,很容易导致事务繁忙。笔者曾经使用 transaction 并发操作几个表,有时候成功有时候失败。
- 把尽可能少的逻辑放在事务中。
- 尽可能操作少的表。
- 事务中减少耗时操作。
有一些场景虽然执行复杂的操作,但是不一定需要使用事务。这里介绍 2 种不使用事务的场景。
1 对某些字段进行 inc 自增/自减时。inc 操作本身是原子性的。
2 需要同时 update 多个表。借助乐观锁的思路。只有更新成功了再执行下一步。
let res = await table.where({
status:old_status
}).update({
status:3
})
if(res.updated){
//再执行其它update。
}
建议不要超过 2 步嵌套依赖。也就是下面的操作不推荐。
let res = await table.where({
status:old_status,
}).update({
status:3
})
if(res.updated){
let res2 = await table.where({ version:xxx }).update({ version:cmd.inc(1) });
if(res2.updated){
//再执行其它update。
}
}
5.4.31 调试日记
在调试线上功能时,例如支付回调、退款回调、公共模块等。经常需要 console.info 打印各种测试消息。Console 打印的消息会被云厂商记录下来。不过腾讯云的日记已经开始收费了。开发者可以把信息写入一个临时表。可以用下面的工具类:
const Console = {
async info(key,content = ""){
let create_time = “yyyy-mm-dd HH:mm:ss”;
await uniCloud.database().collection("temp-log").add({key,create_time,content});
},
async log(key,content){
await this.info( key,content );
}
}
调用时,await Csonole.info();即可。

收藏人数:
购买普通授权版(
导入插件并试用
赞赏(0)
下载 6
赞赏 0
下载 34566
赞赏 155
赞赏
京公网安备:11010802035340号