koishi-plugin-rocom 1.0.12 → 1.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/atlas-service.d.ts +33 -0
- package/lib/atlas-service.js +439 -0
- package/lib/client.d.ts +36 -1
- package/lib/client.js +221 -21
- package/lib/commands/account.js +1 -0
- package/lib/commands/merchant.js +351 -91
- package/lib/commands/query.js +162 -14
- package/lib/commands/tools.js +104 -0
- package/lib/commands/wiki.js +167 -6
- package/lib/egg-service.d.ts +2 -0
- package/lib/egg-service.js +50 -15
- package/lib/index.d.ts +9 -0
- package/lib/index.js +3626 -303
- package/lib/pet-data-service.d.ts +62 -0
- package/lib/pet-data-service.js +468 -0
- package/lib/render-templates/home/img/rocomuid/a_title.png +0 -0
- package/lib/render-templates/home/img/rocomuid/banner_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/bg.jpg +0 -0
- package/lib/render-templates/home/img/rocomuid/img_head.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_bar.png +0 -0
- package/lib/render-templates/home/img/rocomuid/jindu_tc.png +0 -0
- package/lib/render-templates/home/img/rocomuid/level_icon.png +0 -0
- package/lib/render-templates/home/img/rocomuid/pet_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/plant_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_1.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_8.png +0 -0
- package/lib/render-templates/home/img/rocomuid/star_9.png +0 -0
- package/lib/render-templates/home/img/rocomuid/title_fg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/top_bg.png +0 -0
- package/lib/render-templates/home/img/rocomuid/touxiang_mask.png +0 -0
- package/lib/render-templates/home/index.html +81 -97
- package/lib/render-templates/home/style.css +316 -375
- package/lib/render-templates/pet-data/index.html +172 -0
- package/lib/render-templates/pet-data/style.css +553 -0
- package/lib/render-templates/wiki/detail/index.html +84 -0
- package/lib/render-templates/wiki/list/index.html +84 -0
- package/lib/render-templates/wiki/menu/index.html +65 -0
- package/lib/render-templates/wiki/pet/index.html +158 -0
- package/lib/render-templates/wiki/pet/style.css +393 -0
- package/lib/render-templates/wiki/shared/style.css +431 -0
- package/lib/render-templates/wiki/skill/index.html +79 -0
- package/lib/render-templates/wiki/skill/style.css +233 -0
- package/lib/render-templates/wiki/suggestions/index.html +84 -0
- package/lib/render-templates/yuanxing-shangren/CHANGELOG.md +138 -0
- package/lib/render-templates/yuanxing-shangren/README.md +86 -0
- package/lib/render-templates/yuanxing-shangren/img/coin.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//345/272/225/351/203/250/344/277/241/346/201/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//346/240/207/351/242/230.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/203/255/351/224/200.png +0 -0
- package/lib/render-templates/yuanxing-shangren/img//347/274/226/345/217/267/350/203/214/346/231/257/346/241/206.png +0 -0
- package/lib/render-templates/yuanxing-shangren/index.html +198 -170
- package/lib/render-templates/yuanxing-shangren/index.html.backup +356 -0
- package/lib/render-templates/yuanxing-shangren/merchant.html +461 -0
- package/lib/render-templates/yuanxing-shangren/style.css +201 -256
- package/lib/render-templates/yuanxing-shangren/today.html +461 -0
- package/lib/render-templates/yuanxing-shangren/today.style.css +366 -0
- package/lib/render.js +48 -12
- package/lib/subscription-send.js +4 -5
- package/lib/ttf/rocom_skill_origin.ttf +0 -0
- package/lib/ttf//351/200/240/345/255/227/345/267/245/346/210/277/345/220/257/351/273/221/344/275/223.ttf +0 -0
- package/lib/types.d.ts +13 -1
- package/lib/user.d.ts +23 -1
- package/lib/user.js +32 -1
- package/lib/wiki-catalog.d.ts +19 -0
- package/lib/wiki-catalog.js +124 -0
- package/lib/wiki-service.d.ts +220 -0
- package/lib/wiki-service.js +1463 -0
- package/package.json +1 -1
- package/readme.md +4 -5
package/lib/commands/merchant.js
CHANGED
|
@@ -7,6 +7,7 @@ const subscription_send_1 = require("../subscription-send");
|
|
|
7
7
|
const logger = new koishi_1.Logger('rocom-merchant');
|
|
8
8
|
const TEXT = {
|
|
9
9
|
merchant: '\u8fdc\u884c\u5546\u4eba',
|
|
10
|
+
todayMerchant: '今日远行商人',
|
|
10
11
|
subscribe: '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba',
|
|
11
12
|
unsubscribe: '\u53d6\u6d88\u8ba2\u9605\u8fdc\u884c\u5546\u4eba',
|
|
12
13
|
viewSubscribe: '\u67e5\u770b\u8fdc\u884c\u5546\u4eba\u8ba2\u9605',
|
|
@@ -21,6 +22,12 @@ const CATEGORY_LABELS = {
|
|
|
21
22
|
round: '常规商品',
|
|
22
23
|
weekend: '周末限定',
|
|
23
24
|
};
|
|
25
|
+
const ROUND_WINDOWS = [
|
|
26
|
+
{ id: 1, label: '08:00-12:00', startHour: 8, endHour: 12 },
|
|
27
|
+
{ id: 2, label: '12:00-16:00', startHour: 12, endHour: 16 },
|
|
28
|
+
{ id: 3, label: '16:00-20:00', startHour: 16, endHour: 20 },
|
|
29
|
+
{ id: 4, label: '20:00-24:00', startHour: 20, endHour: 24 },
|
|
30
|
+
];
|
|
24
31
|
const CHINA_TIMEZONE = 'Asia/Shanghai';
|
|
25
32
|
const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
|
|
26
33
|
timeZone: CHINA_TIMEZONE,
|
|
@@ -29,19 +36,36 @@ const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
|
|
|
29
36
|
day: '2-digit',
|
|
30
37
|
hour: '2-digit',
|
|
31
38
|
minute: '2-digit',
|
|
39
|
+
second: '2-digit',
|
|
32
40
|
hour12: false,
|
|
33
41
|
});
|
|
34
|
-
function getChinaParts(
|
|
42
|
+
function getChinaParts(input = Date.now()) {
|
|
35
43
|
const parts = {};
|
|
36
|
-
|
|
44
|
+
const date = input instanceof Date ? input : new Date(input);
|
|
45
|
+
for (const item of chinaPartsFormatter.formatToParts(date)) {
|
|
37
46
|
if (item.type !== 'literal')
|
|
38
47
|
parts[item.type] = item.value;
|
|
39
48
|
}
|
|
40
49
|
return {
|
|
50
|
+
year: Number(parts.year || '0'),
|
|
51
|
+
month: Number(parts.month || '0'),
|
|
52
|
+
day: Number(parts.day || '0'),
|
|
41
53
|
hour: Number(parts.hour || '0'),
|
|
42
54
|
minute: Number(parts.minute || '0'),
|
|
55
|
+
second: Number(parts.second || '0'),
|
|
43
56
|
};
|
|
44
57
|
}
|
|
58
|
+
function padNumber(value) {
|
|
59
|
+
return String(value).padStart(2, '0');
|
|
60
|
+
}
|
|
61
|
+
function getChinaDateText(input = Date.now()) {
|
|
62
|
+
const parts = getChinaParts(input);
|
|
63
|
+
return `${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
64
|
+
}
|
|
65
|
+
function getChinaDayStartMs(input = Date.now()) {
|
|
66
|
+
const parts = getChinaParts(input);
|
|
67
|
+
return new Date(`${parts.year}-${padNumber(parts.month)}-${padNumber(parts.day)}T00:00:00+08:00`).getTime();
|
|
68
|
+
}
|
|
45
69
|
function classifyMerchantItem(item) {
|
|
46
70
|
const start = normalizeTimestamp(item?.start_time);
|
|
47
71
|
const end = normalizeTimestamp(item?.end_time);
|
|
@@ -72,16 +96,12 @@ function formatProductWindow(product) {
|
|
|
72
96
|
if (!start && !end)
|
|
73
97
|
return '';
|
|
74
98
|
const formatDate = (timestamp) => {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
78
|
-
return `${month}-${day}`;
|
|
99
|
+
const parts = getChinaParts(timestamp);
|
|
100
|
+
return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
79
101
|
};
|
|
80
102
|
const formatTime = (timestamp) => {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
84
|
-
return `${hour}:${minute}`;
|
|
103
|
+
const parts = getChinaParts(timestamp);
|
|
104
|
+
return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
|
|
85
105
|
};
|
|
86
106
|
if (start && end) {
|
|
87
107
|
const datePart = formatDate(start);
|
|
@@ -95,7 +115,7 @@ function getMerchantActivity(res) {
|
|
|
95
115
|
const activities = res?.merchantActivities || res?.merchant_activities || [];
|
|
96
116
|
return activities[0] || {};
|
|
97
117
|
}
|
|
98
|
-
function
|
|
118
|
+
function getMerchantProducts(res) {
|
|
99
119
|
const activity = getMerchantActivity(res);
|
|
100
120
|
const groups = [];
|
|
101
121
|
if (Array.isArray(activity?.products))
|
|
@@ -110,13 +130,10 @@ function getActiveProducts(res) {
|
|
|
110
130
|
groups.push(activity.get_pets);
|
|
111
131
|
const merged = [];
|
|
112
132
|
const seen = new Set();
|
|
113
|
-
const now = Date.now();
|
|
114
133
|
for (const list of groups) {
|
|
115
134
|
for (const item of list) {
|
|
116
135
|
const start = normalizeTimestamp(item?.start_time) ?? 0;
|
|
117
136
|
const end = normalizeTimestamp(item?.end_time) ?? Infinity;
|
|
118
|
-
if (now < start || now >= end)
|
|
119
|
-
continue;
|
|
120
137
|
const key = `${item?.id ?? ''}|${item?.name ?? ''}|${start}|${end === Infinity ? 'inf' : end}`;
|
|
121
138
|
if (seen.has(key))
|
|
122
139
|
continue;
|
|
@@ -126,36 +143,58 @@ function getActiveProducts(res) {
|
|
|
126
143
|
}
|
|
127
144
|
return merged;
|
|
128
145
|
}
|
|
129
|
-
function
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
function isMerchantItemActive(item, now = Date.now()) {
|
|
147
|
+
const nowMs = now instanceof Date ? now.getTime() : now;
|
|
148
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
149
|
+
const end = normalizeTimestamp(item?.end_time);
|
|
150
|
+
return (start === null || nowMs >= start) && (end === null || nowMs < end);
|
|
151
|
+
}
|
|
152
|
+
function isMerchantItemToday(item, now = Date.now()) {
|
|
153
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
154
|
+
const end = normalizeTimestamp(item?.end_time);
|
|
155
|
+
if (start === null || end === null)
|
|
156
|
+
return true;
|
|
157
|
+
const startOfDay = getChinaDayStartMs(now);
|
|
158
|
+
const endOfDay = startOfDay + 24 * 60 * 60 * 1000;
|
|
159
|
+
return start < endOfDay && end > startOfDay;
|
|
160
|
+
}
|
|
161
|
+
function getRoundForItem(item, now = Date.now()) {
|
|
162
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
163
|
+
if (start === null)
|
|
164
|
+
return null;
|
|
165
|
+
const startParts = getChinaParts(start);
|
|
166
|
+
const nowParts = getChinaParts(now);
|
|
167
|
+
if (startParts.year !== nowParts.year || startParts.month !== nowParts.month || startParts.day !== nowParts.day) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
const startHour = startParts.hour + startParts.minute / 60;
|
|
171
|
+
const round = ROUND_WINDOWS.find(win => startHour >= win.startHour && startHour < win.endHour);
|
|
172
|
+
return round?.id ?? null;
|
|
173
|
+
}
|
|
174
|
+
function getCurrentMerchantRound(now = new Date()) {
|
|
175
|
+
const parts = getChinaParts(now);
|
|
176
|
+
const secondsOfDay = parts.hour * 3600 + parts.minute * 60 + parts.second;
|
|
177
|
+
const marketStartSeconds = 8 * 3600;
|
|
178
|
+
const marketEndSeconds = 24 * 3600;
|
|
179
|
+
const datePart = getChinaDateText(now);
|
|
180
|
+
if (secondsOfDay < marketStartSeconds || secondsOfDay >= marketEndSeconds) {
|
|
181
|
+
return {
|
|
182
|
+
current: null,
|
|
183
|
+
total: ROUND_WINDOWS.length,
|
|
184
|
+
countdown: TEXT.notOpen,
|
|
185
|
+
is_open: false,
|
|
186
|
+
round_id: `${datePart}-closed`,
|
|
187
|
+
};
|
|
141
188
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
nextTime.setDate(nextTime.getDate() + 1);
|
|
147
|
-
nextTime.setHours(nextRound, 0, 0, 0);
|
|
148
|
-
const diff = nextTime.getTime() - now.getTime();
|
|
189
|
+
const currentWindow = ROUND_WINDOWS.find(win => parts.hour >= win.startHour && parts.hour < win.endHour);
|
|
190
|
+
const currentRound = currentWindow?.id ?? null;
|
|
191
|
+
const roundEndSeconds = (currentWindow?.endHour ?? 24) * 3600;
|
|
192
|
+
const diff = Math.max(0, (roundEndSeconds - secondsOfDay) * 1000);
|
|
149
193
|
const hours = Math.floor(diff / 3600000);
|
|
150
194
|
const mins = Math.floor((diff % 3600000) / 60000);
|
|
151
|
-
const datePart = [
|
|
152
|
-
now.getFullYear(),
|
|
153
|
-
String(now.getMonth() + 1).padStart(2, '0'),
|
|
154
|
-
String(now.getDate()).padStart(2, '0'),
|
|
155
|
-
].join('-');
|
|
156
195
|
return {
|
|
157
196
|
current: currentRound,
|
|
158
|
-
total:
|
|
197
|
+
total: ROUND_WINDOWS.length,
|
|
159
198
|
countdown: `${hours}\u5c0f\u65f6${mins}\u5206\u949f`,
|
|
160
199
|
is_open: currentRound !== null,
|
|
161
200
|
round_id: `${datePart}-${currentRound || 'closed'}`,
|
|
@@ -192,69 +231,272 @@ function isBotAdmin(session, adminUserIds) {
|
|
|
192
231
|
function sameStringArray(left, right) {
|
|
193
232
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
194
233
|
}
|
|
195
|
-
function
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
234
|
+
function getRandomGoodsMaps(res) {
|
|
235
|
+
const priceMap = new Map();
|
|
236
|
+
const limitMap = new Map();
|
|
237
|
+
const randomGoods = Array.isArray(res?.random_goods)
|
|
238
|
+
? res.random_goods
|
|
239
|
+
: Array.isArray(res?.randomGoods)
|
|
240
|
+
? res.randomGoods
|
|
241
|
+
: [];
|
|
242
|
+
for (const item of randomGoods) {
|
|
243
|
+
const name = String(item?.goods_name || item?.name || '').trim();
|
|
244
|
+
if (!name)
|
|
245
|
+
continue;
|
|
246
|
+
if (item?.price !== undefined && item?.price !== null && item.price !== '')
|
|
247
|
+
priceMap.set(name, item.price);
|
|
248
|
+
if (item?.buy_limit_num !== undefined && item?.buy_limit_num !== null && item.buy_limit_num !== '') {
|
|
249
|
+
limitMap.set(name, item.buy_limit_num);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { priceMap, limitMap };
|
|
253
|
+
}
|
|
254
|
+
function normalizeMerchantProducts(res, now = new Date()) {
|
|
255
|
+
return getMerchantProducts(res).map((item) => {
|
|
256
|
+
const name = String(item?.name || item?.goods_name || TEXT.unknown).trim() || TEXT.unknown;
|
|
257
|
+
return {
|
|
258
|
+
name,
|
|
259
|
+
image: String(item?.icon_url || item?.iconUrl || ''),
|
|
260
|
+
time_label: formatProductWindow(item),
|
|
261
|
+
category: classifyMerchantItem(item),
|
|
262
|
+
round_id: getRoundForItem(item, now),
|
|
263
|
+
is_active: isMerchantItemActive(item, now),
|
|
264
|
+
start_time: normalizeTimestamp(item?.start_time),
|
|
265
|
+
end_time: normalizeTimestamp(item?.end_time),
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function buildMerchantCardItems(products, res, options) {
|
|
270
|
+
const { priceMap, limitMap } = getRandomGoodsMaps(res);
|
|
271
|
+
const catOrder = { round: 0, normal: 1, weekend: 2 };
|
|
272
|
+
const startY = 592;
|
|
273
|
+
const cardHeight = 308;
|
|
274
|
+
const gap = 43;
|
|
275
|
+
const goodsAll = products.map((product) => {
|
|
276
|
+
const limit = limitMap.get(product.name);
|
|
277
|
+
const limitText = limit === undefined || limit === null || limit === '' ? '--' : String(limit);
|
|
278
|
+
const isHot = product.category !== 'round';
|
|
279
|
+
const isEnded = options.includeEnded ? !product.is_active : false;
|
|
280
|
+
const roundPrefix = product.round_id ? `第${product.round_id}轮·` : '';
|
|
281
|
+
const remainingStr = product.category === 'normal'
|
|
282
|
+
? `本日限购${limitText}个`
|
|
283
|
+
: product.category === 'weekend'
|
|
284
|
+
? `活动期间限购${limitText}个`
|
|
285
|
+
: `${isEnded ? roundPrefix : ''}本轮限购${limitText}个`;
|
|
286
|
+
return {
|
|
287
|
+
goods_name: product.name,
|
|
288
|
+
iconUrl: product.image,
|
|
289
|
+
price: priceMap.get(product.name) ?? 0,
|
|
290
|
+
num: '',
|
|
291
|
+
category: product.category,
|
|
292
|
+
roundId: product.round_id || 0,
|
|
293
|
+
isHot,
|
|
294
|
+
isEnded,
|
|
295
|
+
remainingStr,
|
|
296
|
+
top: 0,
|
|
297
|
+
};
|
|
298
|
+
});
|
|
299
|
+
goodsAll.sort((a, b) => {
|
|
300
|
+
if (options.includeEnded && a.isEnded !== b.isEnded)
|
|
301
|
+
return a.isEnded ? 1 : -1;
|
|
302
|
+
if (a.category !== b.category)
|
|
303
|
+
return catOrder[a.category] - catOrder[b.category];
|
|
304
|
+
return Number(b.price || 0) - Number(a.price || 0);
|
|
305
|
+
});
|
|
306
|
+
const goods = goodsAll.map((item, index) => ({
|
|
307
|
+
...item,
|
|
308
|
+
num: String(index + 1).padStart(2, '0'),
|
|
309
|
+
top: startY + index * (cardHeight + gap),
|
|
204
310
|
}));
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
311
|
+
const lastCardTop = goods.length > 0 ? goods[goods.length - 1].top : startY;
|
|
312
|
+
const bottomFrameTop = lastCardTop + 287;
|
|
313
|
+
const pageHeight = bottomFrameTop + 160;
|
|
314
|
+
return { goods, bottomFrameTop, pageHeight };
|
|
315
|
+
}
|
|
316
|
+
function getCurrentMerchantProducts(products, roundInfo) {
|
|
317
|
+
return products.filter((product) => {
|
|
318
|
+
if (!product.is_active)
|
|
319
|
+
return false;
|
|
320
|
+
if (product.category === 'round')
|
|
321
|
+
return !!roundInfo.current && product.round_id === roundInfo.current;
|
|
322
|
+
return true;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function buildMerchantFallbackText(title, products, roundInfo) {
|
|
326
|
+
const fallbackLines = [title];
|
|
327
|
+
if (roundInfo) {
|
|
328
|
+
fallbackLines.push(`轮次:第 ${roundInfo.current || TEXT.notOpen} / ${roundInfo.total} 轮`);
|
|
329
|
+
fallbackLines.push(`剩余:${roundInfo.countdown}`);
|
|
330
|
+
}
|
|
331
|
+
fallbackLines.push('');
|
|
332
|
+
if (!products.length) {
|
|
333
|
+
fallbackLines.push(roundInfo ? '当前轮次暂无商品。' : '今日暂无已公布的远行商人商品。');
|
|
334
|
+
return fallbackLines.join('\n').trimEnd();
|
|
335
|
+
}
|
|
336
|
+
for (const key of CATEGORY_ORDER) {
|
|
337
|
+
const group = products.filter(product => product.category === key);
|
|
338
|
+
if (!group.length)
|
|
339
|
+
continue;
|
|
340
|
+
fallbackLines.push(`【${CATEGORY_LABELS[key]}】`);
|
|
341
|
+
group.forEach((product, index) => {
|
|
342
|
+
const tail = product.time_label ? ` (${product.time_label})` : '';
|
|
343
|
+
fallbackLines.push(` ${index + 1}. ${product.name}${tail}`);
|
|
344
|
+
});
|
|
345
|
+
fallbackLines.push('');
|
|
212
346
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
347
|
+
return fallbackLines.join('\n').trimEnd();
|
|
348
|
+
}
|
|
349
|
+
function getMerchantActivityTitle(res) {
|
|
350
|
+
const activity = getMerchantActivity(res);
|
|
351
|
+
return String(activity?.name || TEXT.merchant).trim() || TEXT.merchant;
|
|
352
|
+
}
|
|
353
|
+
function getMerchantActivitySubtitle(res) {
|
|
354
|
+
const activity = getMerchantActivity(res);
|
|
355
|
+
return String(activity?.start_date || '每日 08:00 / 12:00 / 16:00 / 20:00 刷新').trim();
|
|
356
|
+
}
|
|
357
|
+
function getLegacyRoundGroups(now, roundInfo) {
|
|
358
|
+
return ROUND_WINDOWS.map(win => ({
|
|
359
|
+
round_id: win.id,
|
|
360
|
+
label: `${padNumber(win.startHour)}:00 - ${padNumber(win.endHour)}:00`,
|
|
361
|
+
is_current: roundInfo.current === win.id,
|
|
362
|
+
products: [],
|
|
219
363
|
}));
|
|
364
|
+
}
|
|
365
|
+
function buildLegacyCategoryGroups(products, roundGroups) {
|
|
366
|
+
return CATEGORY_ORDER
|
|
367
|
+
.map((key) => {
|
|
368
|
+
const groups = roundGroups
|
|
369
|
+
.map(group => ({
|
|
370
|
+
round_id: group.round_id,
|
|
371
|
+
label: group.label,
|
|
372
|
+
is_current: group.is_current,
|
|
373
|
+
products: group.products.filter(product => product.category === key),
|
|
374
|
+
}))
|
|
375
|
+
.filter(group => group.products.length > 0);
|
|
376
|
+
return {
|
|
377
|
+
key,
|
|
378
|
+
label: CATEGORY_LABELS[key],
|
|
379
|
+
roundGroups: groups,
|
|
380
|
+
product_count: groups.reduce((sum, group) => sum + group.products.length, 0),
|
|
381
|
+
};
|
|
382
|
+
})
|
|
383
|
+
.filter(category => category.product_count > 0);
|
|
384
|
+
}
|
|
385
|
+
function getMerchantDateStr(now = new Date()) {
|
|
386
|
+
const parts = getChinaParts(now);
|
|
387
|
+
return `${parts.month}.${parts.day}`;
|
|
388
|
+
}
|
|
389
|
+
function buildMerchantRenderPayload(res, now = new Date()) {
|
|
390
|
+
const allProducts = normalizeMerchantProducts(res, now);
|
|
391
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
392
|
+
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
393
|
+
const currentWindow = ROUND_WINDOWS.find(win => win.id === roundInfo.current);
|
|
394
|
+
const timeRange = currentWindow
|
|
395
|
+
? `${padNumber(currentWindow.startHour)}:00-${padNumber(currentWindow.endHour)}:00`
|
|
396
|
+
: '--:--~--:--';
|
|
397
|
+
const data = {
|
|
398
|
+
dateStr: getMerchantDateStr(now),
|
|
399
|
+
timeRange,
|
|
400
|
+
...buildMerchantCardItems(products, res, { includeEnded: false }),
|
|
401
|
+
};
|
|
402
|
+
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
403
|
+
return { products, roundInfo, data, fallback };
|
|
404
|
+
}
|
|
405
|
+
function buildLegacyMerchantRenderPayload(res, now = new Date()) {
|
|
406
|
+
const allProducts = normalizeMerchantProducts(res, now);
|
|
407
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
408
|
+
const products = getCurrentMerchantProducts(allProducts, roundInfo);
|
|
409
|
+
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
410
|
+
const currentGroup = roundGroups.find(group => group.round_id === roundInfo.current) || roundGroups[0];
|
|
411
|
+
currentGroup.products.push(...products);
|
|
412
|
+
const categories = buildLegacyCategoryGroups(products, roundGroups);
|
|
220
413
|
const data = {
|
|
221
414
|
background: '',
|
|
222
|
-
title:
|
|
223
|
-
subtitle:
|
|
224
|
-
titleIcon: true,
|
|
225
|
-
product_count: renderedProducts.length,
|
|
226
|
-
round_info: roundInfo,
|
|
415
|
+
title: getMerchantActivityTitle(res),
|
|
416
|
+
subtitle: getMerchantActivitySubtitle(res),
|
|
227
417
|
categories,
|
|
228
|
-
|
|
418
|
+
roundGroups,
|
|
419
|
+
total_products: products.length,
|
|
229
420
|
};
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
421
|
+
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
422
|
+
return { products, roundInfo, data, fallback };
|
|
423
|
+
}
|
|
424
|
+
function buildTodayMerchantRenderPayload(res, now = new Date()) {
|
|
425
|
+
const products = normalizeMerchantProducts(res, now)
|
|
426
|
+
.filter(product => {
|
|
427
|
+
const source = {
|
|
428
|
+
start_time: product.start_time,
|
|
429
|
+
end_time: product.end_time,
|
|
430
|
+
};
|
|
431
|
+
return isMerchantItemToday(source, now);
|
|
432
|
+
});
|
|
433
|
+
const data = {
|
|
434
|
+
dateStr: getMerchantDateStr(now),
|
|
435
|
+
...buildMerchantCardItems(products, res, { includeEnded: true }),
|
|
436
|
+
};
|
|
437
|
+
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, products);
|
|
438
|
+
return { products, data, fallback };
|
|
439
|
+
}
|
|
440
|
+
function buildLegacyTodayMerchantRenderPayload(res, now = new Date()) {
|
|
441
|
+
const allProducts = normalizeMerchantProducts(res, now)
|
|
442
|
+
.filter(product => {
|
|
443
|
+
const source = {
|
|
444
|
+
start_time: product.start_time,
|
|
445
|
+
end_time: product.end_time,
|
|
446
|
+
};
|
|
447
|
+
return isMerchantItemToday(source, now);
|
|
448
|
+
});
|
|
449
|
+
const roundInfo = getCurrentMerchantRound(now);
|
|
450
|
+
const roundGroups = getLegacyRoundGroups(now, roundInfo);
|
|
451
|
+
for (const product of allProducts) {
|
|
452
|
+
const roundId = product.category === 'round' ? product.round_id : null;
|
|
453
|
+
const group = roundId
|
|
454
|
+
? roundGroups.find(item => item.round_id === roundId)
|
|
455
|
+
: roundGroups[0];
|
|
456
|
+
if (group)
|
|
457
|
+
group.products.push(product);
|
|
246
458
|
}
|
|
247
|
-
const
|
|
248
|
-
|
|
459
|
+
const categories = buildLegacyCategoryGroups(allProducts, roundGroups);
|
|
460
|
+
const data = {
|
|
461
|
+
background: '',
|
|
462
|
+
title: TEXT.todayMerchant,
|
|
463
|
+
subtitle: `${getChinaDateText(now)} · 每日 08:00 / 12:00 / 16:00 / 20:00 刷新`,
|
|
464
|
+
categories,
|
|
465
|
+
roundGroups,
|
|
466
|
+
total_products: allProducts.length,
|
|
467
|
+
};
|
|
468
|
+
const fallback = buildMerchantFallbackText(`今日远行商人 (${getChinaDateText(now)})`, allProducts);
|
|
469
|
+
return { products: allProducts, data, fallback };
|
|
470
|
+
}
|
|
471
|
+
function useLegacyMerchantUi(deps) {
|
|
472
|
+
return deps.config.merchantUiStyle === 'old';
|
|
473
|
+
}
|
|
474
|
+
function buildConfiguredMerchantRenderPayload(deps, res, now = new Date()) {
|
|
475
|
+
const payload = useLegacyMerchantUi(deps)
|
|
476
|
+
? buildLegacyMerchantRenderPayload(res, now)
|
|
477
|
+
: buildMerchantRenderPayload(res, now);
|
|
478
|
+
return {
|
|
479
|
+
...payload,
|
|
480
|
+
templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/merchant',
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function buildConfiguredTodayMerchantRenderPayload(deps, res, now = new Date()) {
|
|
484
|
+
const payload = useLegacyMerchantUi(deps)
|
|
485
|
+
? buildLegacyTodayMerchantRenderPayload(res, now)
|
|
486
|
+
: buildTodayMerchantRenderPayload(res, now);
|
|
487
|
+
return {
|
|
488
|
+
...payload,
|
|
489
|
+
templateName: useLegacyMerchantUi(deps) ? 'yuanxing-shangren' : 'yuanxing-shangren/today',
|
|
490
|
+
};
|
|
249
491
|
}
|
|
250
492
|
async function checkMerchantSubscriptions(deps) {
|
|
251
493
|
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
252
494
|
const res = await client.getMerchantInfo(ctx, true);
|
|
253
495
|
if (!res)
|
|
254
496
|
return { subscriptions: 0, matched: 0, pushed: 0 };
|
|
255
|
-
const { products, roundInfo, data, fallback } =
|
|
497
|
+
const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
256
498
|
const productNames = products.map((p) => p.name || '').filter(Boolean);
|
|
257
|
-
const rendered = await renderer.renderHtml(ctx,
|
|
499
|
+
const rendered = await renderer.renderHtml(ctx, templateName, data);
|
|
258
500
|
const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
|
|
259
501
|
const subs = merchantSubMgr.getAll();
|
|
260
502
|
let matchedCount = 0;
|
|
@@ -307,14 +549,24 @@ async function checkMerchantSubscriptions(deps) {
|
|
|
307
549
|
function register(deps) {
|
|
308
550
|
const { ctx, config, client, merchantSubMgr } = deps;
|
|
309
551
|
ctx.command(TEXT.merchant, '\u67e5\u770b\u8fdc\u884c\u5546\u4eba\u5546\u54c1')
|
|
552
|
+
.alias('yxsr')
|
|
310
553
|
.action(async ({ session }) => {
|
|
311
554
|
const res = await client.getMerchantInfo(ctx, true);
|
|
312
555
|
if (!res)
|
|
313
556
|
return `\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\uff1a${client.getLastErrorBrief()}`;
|
|
314
|
-
const { data, fallback } =
|
|
315
|
-
const png = await deps.renderer.renderHtml(ctx,
|
|
557
|
+
const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
558
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
316
559
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
|
|
317
560
|
});
|
|
561
|
+
ctx.command(TEXT.todayMerchant, '查看今日远行商人全部商品')
|
|
562
|
+
.action(async ({ session }) => {
|
|
563
|
+
const res = await client.getMerchantInfo(ctx, true);
|
|
564
|
+
if (!res)
|
|
565
|
+
return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
|
|
566
|
+
const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
|
|
567
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
568
|
+
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren:today', deps.config);
|
|
569
|
+
});
|
|
318
570
|
ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
|
|
319
571
|
.action(async ({ session }, args) => {
|
|
320
572
|
const target = getSubscriptionTarget(session);
|
|
@@ -368,6 +620,14 @@ function register(deps) {
|
|
|
368
620
|
return `远行商人订阅检查完成:订阅 ${result.subscriptions} 条,命中 ${result.matched} 条,推送 ${result.pushed} 条。`;
|
|
369
621
|
});
|
|
370
622
|
if (config.merchantSubscriptionEnabled) {
|
|
623
|
+
// 上游 v3.1.0:检查前加入 ±30s 随机延迟,降低刷新窗口和并发请求冲突。
|
|
624
|
+
const MERCHANT_JITTER_MS = 30000;
|
|
625
|
+
const runCheckWithJitter = () => {
|
|
626
|
+
const jitter = Math.floor(Math.random() * MERCHANT_JITTER_MS);
|
|
627
|
+
setTimeout(() => {
|
|
628
|
+
checkMerchantSubscriptions(deps).catch(err => logger.warn(`远行商人订阅检查失败: ${err}`));
|
|
629
|
+
}, jitter);
|
|
630
|
+
};
|
|
371
631
|
if (config.merchantCheckMode === 'times' && config.merchantCheckTimes.length > 0) {
|
|
372
632
|
let lastMerchantCheckKey = '';
|
|
373
633
|
ctx.setInterval(async () => {
|
|
@@ -379,12 +639,12 @@ function register(deps) {
|
|
|
379
639
|
if (checkKey === lastMerchantCheckKey)
|
|
380
640
|
return;
|
|
381
641
|
lastMerchantCheckKey = checkKey;
|
|
382
|
-
|
|
642
|
+
runCheckWithJitter();
|
|
383
643
|
}, 60000);
|
|
384
644
|
}
|
|
385
645
|
else {
|
|
386
646
|
ctx.setInterval(async () => {
|
|
387
|
-
|
|
647
|
+
runCheckWithJitter();
|
|
388
648
|
}, config.merchantCheckInterval);
|
|
389
649
|
}
|
|
390
650
|
}
|