对于后端这一块,耽误了好久,还特地去补习了 SQL。

这是 2018 年学习 Udacity 微信小程序课程时的笔记,使用的是当时的 Wafer / qcloud 方案。原稿里的 12 张截图没有恢复,商品、路由和请求的关系按文字保留,SQL 与订单分组则补成了可以单独阅读的示例。它记录课程架构,没有重新搭建当年的整套服务。

早期编程学习目录

先把三个部分连起来

原来把后端分成了业务逻辑、路由、数据下载三部分。更准确地说,前两个主要在服务端,最后一个是小程序发起请求并读取结果:

  1. 业务逻辑决定要查什么数据、验证什么条件,以及怎样组织返回结果。
  2. 路由把 URL 和 HTTP 方法对应到处理函数。
  3. 客户端请求访问路由,检查返回结果,再更新页面。

课程中,控制器把数据放进 ctx.state.data,交给后面的响应处理逻辑包装。把值写进 state 本身,不等于已经向客户端发送了 HTTP 响应。

商品列表与详情

列表是从 product 表读取多条商品记录。详情则按商品 ID 查询一条记录。原稿提到“确保 ID 是数字”,这里还需要区分合法的整数 ID 和仅仅能够转成数字的输入。

整理时补充的校验例子:

1
2
3
4
5
function parseProductId(raw) {
if (typeof raw !== 'string' || !/^[1-9]\d*$/.test(raw)) return null;
var id = Number(raw);
return Number.isSafeInteger(id) ? id : null;
}

如果数据库约定商品 ID 为正整数,这样可以拒绝负数、小数、空字符串和超出安全整数范围的值。具体规则应与实际表结构一致。

查询时绑定参数,例如:

1
2
3
SELECT id, name, image, price
FROM product
WHERE id = ?;

不要把客户端传来的字符串直接拼进 SQL。数据库驱动如果返回数组,取得第一项之前,也应先判断有没有查到记录。商品不存在和数据库执行失败,是两种不同情况。

小程序怎样拿到结果

当年的课程用 qcloud.request 发起请求。路由、响应中间件和客户端约定共同决定返回格式;不能只凭 HTTP 状态码判断业务是否完成。

例如,当响应正文约定为下面这样的结构时:

1
2
3
4
{
"code": 0,
"data": []
}

HTTP 请求成功后,还要检查正文中的 code。使用原生 wx.request 时,响应正文在回调参数的 res.data 中;若使用 SDK 封装,则以该 SDK 回调拿到的对象为准。原稿直接写 res.code,缺少这层区分。

商品查询通常使用 GET。课程中的“立即购买”使用 POST,因为它会改变服务端状态。

购买与身份

原稿记录了购买操作依赖用户身份,因此在处理函数前使用验证中间件。服务端应当从已经验证的身份上下文取得用户标识,而不是相信请求体里自报的用户 ID。

这里的“立即购买”是课程业务动作的名称。原稿没有完整记录支付流程、库存和事务处理,不能因为接口返回成功,就把它描述成真实支付已经完成。当前保留下来的重点是路由、身份上下文和订单数据之间的关系。

三张表怎样连成订单列表

原来的订单查询使用三张表:

这里使用的字段含义
order_useridusercreate_time订单及所属用户
order_productorder_idproduct_idcount订单中的商品明细
productidnameimageprice商品资料

整理后的查询如下,问号绑定经过验证的用户标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT
ou.id AS order_id,
ou.create_time,
op.product_id,
op.count,
p.name,
p.image,
p.price
FROM order_user AS ou
LEFT JOIN order_product AS op ON ou.id = op.order_id
LEFT JOIN product AS p ON op.product_id = p.id
WHERE ou.user = ?
ORDER BY ou.id, op.product_id;

原稿简写 SQL 时出现了 order.product_id,这个别名并不存在;应当使用订单明细表中的 product_id。字段说明里也把 order_product.id 与商品 ID 混在了一起,这里改为明确的 order_product.product_id

LEFT JOIN 会保留左表中的订单,即使没有匹配到商品明细。因此后面的代码要处理商品字段为空的情况。

另外,此查询取得的是 product 表里的当前价格。实际成交价通常需要在订单中保留快照,不能在商品改价之后,仍然用当前价格当作历史成交价格。原课程片段没有完整展示这一部分表结构。

把多行查询结果分组

原来的循环给订单重新编了从 1 开始的序号,还依赖同一订单的行始终连续。下面是本次整理补充的分组函数,保留真实订单 ID,并允许输入行不连续:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function groupOrders(rows) {
const groups = new Map();
for (const row of rows) {
if (!groups.has(row.order_id)) {
groups.set(row.order_id, {
id: row.order_id,
create_time: row.create_time,
list: []
});
}
if (row.product_id !== null && row.product_id !== undefined) {
groups.get(row.order_id).list.push({
product_id: row.product_id,
count: row.count,
name: row.name,
image: row.image,
price: row.price
});
}
}
return Array.from(groups.values());
}

查询结果可以先交给 groupOrders,再由控制器放到课程约定的数据位置。这个函数只负责整理已经查到的数据,身份验证、数据库查询和响应包装仍由各自的部分完成。

本次检查了 ID 校验、SQL 连接关系与分组结果,没有重新接通微信登录和购买接口。

客户端响应结构可对照 微信小程序 wx.request 文档;订单表与课程调用方式保留的是原笔记中的背景。