├── README.md
└── worker.js
/README.md:
--------------------------------------------------------------------------------
1 | # car-qrcode-notify
2 | 挪车二维码
3 |
4 | 在大佬的基础上增加了,部分后台管理功能(需要绑定 KV 数据库),可通过 api 添加,删除,更新、查看车辆列表,部署一次可多辆车辆一起使用,无需重复部署。通知发送、获取手机号都由服务器处理,通知渠道支持 WxPusher、Bark、飞书机器人、微信机器人、钉钉机器人、NapCatQQ、Lagrange.Onebot,如需更多渠道可自行增加。可设置发送通知的速率,可设置首页风格。
5 |
6 | 【V1.5更新内容】\
7 | 1、增加消息通知、电话通知开关,可单独控制通知功能开启(默认为全开) \
8 | 2、修复已知问题
9 |
10 | 【V1.4更新内容】\
11 | 1、后台管理增加登录页面https://xxxxxx.workers.dev/login \
12 | 2、移除/add页面,添加车辆登录后,统一到/manager页面添加 \
13 | 3、增加车牌号字段,发送通知时会附带车牌号。
14 |
15 | 【V1.3 更新内容】\
16 | 1、新增一套首页风格,https://xxxxxx.workers.dev/?id=1&style=2 ,默认为风格1(sytle=1 风格1,style=2 风格2)\
17 | 2、页面交互优化\
18 | 3、修复已知问题
19 |
20 | 【V1.2 更新内容】\
21 | 1、增加多个通知渠道现在支持(WxPusher、Bark、飞书机器人、微信机器人、钉钉机器人、NapCatQQ、Lagrange.Onebot)\
22 | 2、修复已知问题
23 |
24 | # 正文开始
25 |
26 | 1、复制 [worker.js](https://github.com/oozzbb/car-qrcode-notify/blob/main/worker.js) 文件内的代码部署到 cloudflare workers 即可\
27 | 2、在新建的 workers 的设置中绑定 KV 数据库,具体如下图(变量名称必须为 DATA)
28 | 
29 |
30 | # API 部分
31 |
32 | 可通过 http 请求软件,或者去 KV 数据库直接修改,所有请求都为 POST 模式,不可在浏览器中直接访问,请求时需要添加 Authorization: Bearer API_KEY 授权头\
33 |
34 | ```
35 | 添加车辆api
36 | * https://xxxxxx.workers.dev/api/addOwner
37 | {"id":"1","phone":"1234567890","notifyType":"1","notifyToken":"AT_xxxxxx|UID_xxxxxx"}
38 | id:每台车辆唯一标识
39 | phone:手机号
40 | notifyType:通知方式,notifyTypeMap对应即可
41 | notifyToken:通知渠道所使用的token
42 |
43 | notifyType为1则使用wxpusher,notifyToken格式为AT_xxxxxx|UID_xxxxxx
44 | notifyType为2则使用bark,notifyToken为bark token
45 | notifyType为3则使用feishu,notifyToken为xxxxxx的值,不需要输入完整链接,https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxx
46 |
47 | 删除车辆api
48 | * https://xxxxxx.workers.dev/api/deleteOwner
49 | {"id":"1"}
50 |
51 | 车辆列表api
52 | * https://xxxxxx.workers.dev/api/listOwner
53 | ```
54 |
55 | # 使用方法
56 |
57 | 部署完后访问你自己的 workers 即可\
58 | 1、https://xxxxxx.workers.dev/add 访问你自己的链接添加车辆 (该页面已移除,请访问登录页面,进后台添加) \
59 | 2、https://xxxxxx.workers.dev/login 后台车辆管理登录页面\
60 | 3、https://xxxxxx.workers.dev/manager 后台车辆管理页面\
61 | 4、https://xxxxxx.workers.dev/?id=1&style=2 默认为风格1(sytle=1 风格1,style=2 风格2)访问你自己的链接发送通知或拨打电话。需要带相应的车辆 id
62 |
63 | 
64 |
65 | 
66 |
67 | 
68 |
69 | 
70 |
--------------------------------------------------------------------------------
/worker.js:
--------------------------------------------------------------------------------
1 | //Version:1.5.0
2 | //Date:2024-11-22 10:50:47
3 |
4 | addEventListener('fetch', event => {
5 | event.respondWith(handleRequest(event.request));
6 | });
7 |
8 | //防止被滥用,在添加车辆信息时需要用来鉴权
9 | const API_KEY = "sk-@Admin123";
10 | const notifyMessage = "您好,有人需要您挪车,请及时处理。";
11 | const sendSuccessMessage = "您好,我已收到你的挪车通知,我正在赶来的路上,请稍等片刻!";
12 | //300秒内可发送5次通知
13 | const rateLimitDelay = 300;
14 | const rateLimitMaxRequests = 5;
15 | //达到速率限制时返回内容
16 | const rateLimitMessage = "我正在赶来的路上,请稍等片刻~~~";
17 |
18 | //通知类型,其他的通知类型可自行实现
19 | const notifyTypeMap = [
20 | { "id": "1", "name": "WxPusher", "functionName": wxpusher, "tip": "\r\nAT_xxxxxx|UID_xxxxxx" },
21 | { "id": "2", "name": "Bark", "functionName": bark, "tip": "\r\ntoken|soundName\r\n\r\n注:token为xxxxxx代表的值,直接输入该值即可,请勿输入完整链接(https://api.day.app/xxxxxx),soundName为铃声名称(默认使用:multiwayinvitation),如需自定义铃声需要把铃声文件先上传到BarkApp" },
22 | { "id": "3", "name": "飞书机器人", "functionName": feishu, "tip": "\r\ntoken\r\n\r\n注:token为xxxxxx代表的值,直接输入该值即可,请勿输入完整链接(https://open.feishu.cn/open-apis/bot/v2/hook/xxxxxx)" },
23 | { "id": "4", "name": "企业微信机器人", "functionName": weixin, "tip": "\r\ntoken\r\n\r\n注:token为xxxxxx代表的值,直接输入该值即可,请勿输入完整链接(https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxx)" },
24 | { "id": "5", "name": "钉钉机器人", "functionName": dingtalk, "tip": "\r\ntoken\r\n\r\n注:token为xxxxxx代表的值,直接输入该值即可,请勿输入完整链接(https://oapi.dingtalk.com/robot/send?access_token=xxxxxx)" },
25 | { "id": "6", "name": "NapCatQQ", "functionName": onebot, "tip": "http://127.0.0.1:8000/send_private_msg|access_token|接收人QQ号" },
26 | { "id": "7", "name": "Lagrange.Onebot", "functionName": onebot, "tip": "http://127.0.0.1:8000/send_private_msg|access_token|接收人QQ号" }
27 | ]
28 |
29 | async function handleRequest(request) {
30 | try {
31 | const url = new URL(request.url);
32 | const pathname = url.pathname;
33 | if (request.method === "OPTIONS") {
34 | return getResponse("", 204);
35 | }
36 | else if (request.method == "POST") {
37 | if (pathname == '/api/notifyOwner') {
38 | const json = await request.json();
39 | return await notifyOwner(json);
40 | }
41 | else if (pathname == '/api/callOwner') {
42 | const json = await request.json();
43 | return await callOwner(json);
44 | }
45 | else if (pathname == '/api/addOwner') {
46 | if (!isAuth(request)) {
47 | return getResponse(JSON.stringify({ code: 500, data: "Auth error", message: "fail" }), 200);
48 | }
49 | const json = await request.json();
50 | return await addOwner(json);
51 | }
52 | else if (pathname == '/api/deleteOwner') {
53 | if (!isAuth(request)) {
54 | return getResponse(JSON.stringify({ code: 500, data: "Auth error", message: "fail" }), 200);
55 | }
56 | const json = await request.json();
57 | return await deleteOwner(json);
58 | }
59 | else if (pathname == '/api/listOwner') {
60 | if (!isAuth(request)) {
61 | return getResponse(JSON.stringify({ code: 500, data: "Auth error", message: "fail" }), 200);
62 | }
63 | return await listOwner();
64 | }
65 | else if (pathname == '/api/notifyTypeList') {
66 | return getNotifyTypeList();
67 | }
68 | else if (pathname == '/api/login') {
69 | const { apiKey } = await request.json();
70 | if (apiKey && apiKey == API_KEY) {
71 | return getResponse(JSON.stringify({ code: 200, data: "Authorized", message: "success" }), 200);
72 | }
73 | else {
74 | return getResponse(JSON.stringify({ code: 401, data: "Unauthorized", message: "fail" }), 200);
75 | }
76 | }
77 | }
78 | else if (request.method == "GET") {
79 | if (pathname == "/login") {
80 | return login();
81 | }
82 | else if (pathname == "/manager") {
83 | return managerOwnerIndex();
84 | }
85 | else {
86 | const style = url.searchParams.get("style") || "1";
87 | const id = url.searchParams.get("id") || "";
88 | return style == "2" ? await index2(id) : await index1(id);
89 | }
90 | }
91 | } catch (error) {
92 | return getResponse(JSON.stringify({ code: 500, data: error.message, message: "fail" }), 200);
93 | }
94 | }
95 |
96 | function isAuth(request) {
97 | const authHeader = request.headers.get("Authorization");
98 | if (!authHeader || !authHeader.startsWith("Bearer ") || authHeader.split(" ")[1] !== API_KEY) {
99 | return false;
100 | }
101 | else {
102 | return true;
103 | }
104 | }
105 |
106 | async function getKV(id) {
107 | try {
108 | if (id) {
109 | const owner = await DATA.get(id) || null;
110 | if (owner) {
111 | return JSON.parse(owner);
112 | }
113 | }
114 | } catch (e) {
115 | }
116 | return null;
117 | }
118 |
119 | async function putKV(id, owner, cfg) {
120 | if (id) {
121 | await DATA.put(id, JSON.stringify(owner), cfg);
122 | return true;
123 | }
124 | else {
125 | return false;
126 | }
127 | }
128 |
129 | async function delKV(id) {
130 | if (id) {
131 | await DATA.delete(id);
132 | return true;
133 | }
134 | else {
135 | return false;
136 | }
137 | }
138 |
139 | async function listKV(prefix, limit) {
140 | return await DATA.list({ prefix, limit });
141 | }
142 |
143 | async function rateLimit(id) {
144 | const key = `ratelimit:${id.toLowerCase()}`;
145 | const currentCount = await getKV(key) || 0;
146 | const notifyCount = parseInt(currentCount);
147 | if (notifyCount >= rateLimitMaxRequests) {
148 | return false;
149 | }
150 | await putKV(key, notifyCount + 1, {
151 | expirationTtl: rateLimitDelay
152 | });
153 | return true
154 | }
155 |
156 | async function notifyOwner(json) {
157 | const { id, message } = json;
158 | const isCanSend = await rateLimit(id);
159 | if (!isCanSend) {
160 | return getResponse(JSON.stringify({ code: 200, data: rateLimitMessage, message: "success" }), 200);
161 | }
162 | const owner = await getKV(`car_${id.toLowerCase()}`);
163 | if (!owner) {
164 | return getResponse(JSON.stringify({ code: 500, data: "车辆信息错误!", message: "fail" }), 200);
165 | }
166 | if(!owner.isNotify){
167 | return getResponse(JSON.stringify({ code: 500, data: "车主未开启该功能,请使用其他方式联系车主!", message: "fail" }), 200);
168 | }
169 | let resp = null;
170 | const { no, notifyType, notifyToken } = owner;
171 | const provider = notifyTypeMap.find(element => element.id == notifyType);
172 | if (provider && provider.functionName && typeof provider.functionName === 'function') {
173 | const sendMsg = `【${no}】${message || notifyMessage}`;
174 | resp = await provider.functionName(notifyToken, sendMsg);
175 | }
176 | else {
177 | resp = { code: 500, data: "发送失败!", message: "fail" };
178 | }
179 | return getResponse(JSON.stringify(resp), 200);
180 | }
181 |
182 | async function callOwner(json) {
183 | const { id } = json;
184 | const owner = await getKV(`car_${id.toLowerCase()}`);
185 | if (!owner) {
186 | return getResponse(JSON.stringify({ code: 500, data: "车辆信息错误!", message: "fail" }), 200);
187 | }
188 | if(!owner.isCall){
189 | return getResponse(JSON.stringify({ code: 500, data: "车主未开启该功能,请使用其他方式联系车主!", message: "fail" }), 200);
190 | }
191 | const { phone } = owner;
192 | return getResponse(JSON.stringify({ code: 200, data: phone, message: "success" }), 200);
193 | }
194 |
195 | async function addOwner(json) {
196 | try {
197 | const { id, no, phone, notifyType, notifyToken, isNotify, isCall } = json;
198 | await putKV(`car_${id.toLowerCase()}`, { id, no, phone, notifyType, notifyToken, isNotify, isCall });
199 | return getResponse(JSON.stringify({ code: 200, data: "添加成功", message: "success" }), 200);
200 | } catch (e) {
201 | return getResponse(JSON.stringify({ code: 500, data: "添加失败," + e.message, message: "success" }), 200);
202 | }
203 | }
204 |
205 | async function deleteOwner(json) {
206 | try {
207 | const { id } = json;
208 | await delKV(`car_${id.toLowerCase()}`);
209 | return getResponse(JSON.stringify({ code: 200, data: "删除成功", message: "success" }), 200);
210 | } catch (e) {
211 | return getResponse(JSON.stringify({ code: 500, data: "删除失败," + e.message, message: "success" }), 200);
212 | }
213 | }
214 |
215 | async function listOwner() {
216 | const value = await listKV("car_", 50);
217 | const keys = value.keys;
218 | const arrys = [];
219 | for (let i = 0; i < keys.length; i++) {
220 | const owner = await getKV(keys[i].name);
221 | if (!owner || !owner?.id) {
222 | continue;
223 | }
224 | arrys.push(owner);
225 | }
226 | return getResponse(JSON.stringify({ code: 200, data: arrys, message: "success" }), 200);
227 | }
228 |
229 | function getNotifyTypeList() {
230 | const types = [];
231 | notifyTypeMap.forEach(element => {
232 | types.push({ text: element.name, value: element.id, tip: element.tip })
233 | });
234 |
235 | return getResponse(JSON.stringify({ code: 200, data: types, message: "success" }), 200);
236 | }
237 |
238 | function login() {
239 | const htmlContent = `
240 |
241 |
242 |
如需通知车主,请点击以下按钮
606 | 607 | 608 |不好意思阻碍到您的出行了
请通过以下方式联系我,我会立即前来挪车
| 车辆ID | 1334 |车牌号 | 1335 |手机号 | 1336 |通知方式 | 1337 |通知Token | 1338 |消息通知 | 1339 |电话通知 | 1340 |操作 | 1341 |
|---|