实现:websocket 在项目中的使用-订单锁定

从前在订单模块用到了 websocket,记录一下。

场景

admin 订单详情模块存在一个页面,叫做 fraud order list。也就是退款订单页面。

大概长这样:
在这里插入图片描述

点击操作按钮会出现弹窗的表单, 填写完保存:
在这里插入图片描述

问题:

  • 页面不是只有一个人在操作的,同一段时间内,如果有不同的人打开弹窗,巴拉巴拉填完,后面保存的操作会覆盖前面保存的操作。

解决方案

websocket 是在这个场景就是一个很好的解决方法。

  • 首先,在订单的操作栏增加一个 lock 按钮:当我要编辑这笔订单时,点击 lock 按钮,通知到服务端。
  • 服务端接受到消息,将订单锁定的状态存储起来。
  • 再推送到所有客户端浏览器,让这个订单变成已锁定,隐藏所有按钮,不能再编辑。

这样,一个订单,在同一个时间段内,只能有一个人在处理。

在这里插入图片描述

实现

客户端部分代码。用 vue 编写。
这里用了一个 reconnecting-websocket 库,他封装了原生的 websocket,并且可以实现断开后自动重连。(有一些场景,例如网络断开一段时间,或者 node 服务需要重新发布,或者挂掉重启,这个时候是需要重新连接的。)

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
<template>
<!-- 订单列表 -->
<div v-for="order in orderList">
<!-- 点击后锁定 -->
<el-button @click="lockOrder" />
<!-- 其他按钮, 如果是锁定的,则不展示 -->
<el-button @click="edit" v-if="all_lock_data.includes(order)">
操作
</el-button>
<el-dialog>
<!-- 保存,这时解锁订单 -->
<el-button @click="save() && unlockOrder()" />
</el-dialog>
</div>
</template>
<script>
// 引入
import ReconnectingWebSocket from 'reconnecting-websocket'

export default {
methods: {
// 初始化
initWebsocket() {
// 连接接口
const socket = new ReconnectingWebSocket('/websocket/lock_fruad_order')
this.socket = socket
// 建立连接
socket.onopen = function (event) {
console.info('websocket connection established')
}
/**
* data 格式
* [
* order_id: 订单编号
* is_locked: 是否锁定
* account_name: 操作人
* ]
*/
this.socket.onmessage = function (event) {
let data = JSON.parse(event.data)
// 记录lock的数据
$vm.all_lock_data = data
}

// 关闭
this.socket.onclose = function (event) {
console.error('[close] Connection died')
}

// 出错
this.socket.onerror = function (error) {
console.error(`[error] ${error.message}`)
}
},
// 点击锁定
lockOrder(order) {
const data = JSON.stringify([
isLock: true,
order_number: order.order_number,
account_name: window.KLK_PAGE_DATA._session.account.account_name
]);
this.socket.send(data);
},
// 保存后取消锁定
unlockOrder(order) {
const data = JSON.stringify([
isLock: false,
order_number: order.order_number,
account_name: window.KLK_PAGE_DATA._session.account.account_name
]);
this.socket.send(data);
}
},
created() {
// 初始化
this.initWebsocket()
},
}
</script>

上面就是客户端的写法,比较简单,再看看服务端。
服务端用的是 koa。
为了防止服务崩溃,发布或者重启,这里将锁定的订单都存在 redis 持久化。

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
const Koa = require('koa')
const websockify = require('koa-websocket')
const KoaRouter = require('koa-router')
const redis = require('redis')
const app = new Koa()
const router = KoaRouter()
const rds = redis.createClient()
// 使koa实例支持 websocket
app = websockify(app)
// redis的对应的数据key prefix
const RDS_PREFIX = 'fruad_order_lock:'

// 定义一个方法,用于广播
function broadcastAllData($ws) {
// 根据 key prefix 找到所有数据
rds.keys(`${RDS_PREFIX}*`, (err, res) => {
if (err) {
console.log(err)
}
let keys = res
if (keys && keys.length) {
rds.mget(keys, function (err, res) {
if (err) {
console.log(err)
}
let values = res
let data = keys.reduce(
(acc, key, index) => ({
...acc,
[key.replace(RDS_PREFIX, '')]: values[index],
}),
{}
)
// 广播
$ws.broadcast(JSON.stringify(data))
})
} else {
// 未找到,广播
$ws.broadcast(JSON.stringify({}))
}
})
}

// 当收到客户端链接
router.get(
'/websocket/lock_fruad_order',
function* (next) {
// 做一些鉴权的操作,比如登录失效了,则不允许处理
let has_auth = yield web_comm.has_auth.call(this) // 根据 this 上下文中的 cookie,判断登陆态
if (!has_auth) {
// 未登录,直接断开
this.websocket.close(1003, 'Auth Error')
return
} else {
// 否则下一步
yield next
}
},
// 下一个中间件
function* (next) {
let $ws = this.websocket
// 收到 lock message
$ws.on('message', function (message) {
// print message from the client
let [isLock, order_number, name] = JSON.parse(message)
let KEY = `${RDS_PREFIX}${order_number}`
let updateHandler = function (err, res) {
if (err) {
console.log(err)
}
broadcastAllData($ws)
}
// 将数据存到 redis
if (isLock) {
rds.setex(KEY, 10 * 60, name, updateHandler)
} else {
// 解除锁定时,删除 key
rds.del(KEY, updateHandler)
}
})

// send a message to our client
broadcastAllData($ws)

// yielding `next` will pass the context (this) on to the next ws middleware
yield next
}
)

app.use(router.routes())

以上。