koishi-plugin-rocom 1.0.11 → 1.0.13
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/client.d.ts +17 -1
- package/lib/client.js +121 -21
- package/lib/commands/merchant.js +409 -61
- package/lib/commands/query.js +95 -27
- package/lib/index.d.ts +4 -0
- package/lib/index.js +600 -127
- 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/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 +233 -148
- 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 +11 -4
- 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 +4 -0
- package/package.json +1 -1
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',
|
|
@@ -15,6 +16,72 @@ const TEXT = {
|
|
|
15
16
|
defaultSource: '\u9ed8\u8ba4',
|
|
16
17
|
customSource: '\u81ea\u5b9a\u4e49',
|
|
17
18
|
};
|
|
19
|
+
const CATEGORY_ORDER = ['normal', 'round', 'weekend'];
|
|
20
|
+
const CATEGORY_LABELS = {
|
|
21
|
+
normal: '热销商品',
|
|
22
|
+
round: '常规商品',
|
|
23
|
+
weekend: '周末限定',
|
|
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
|
+
];
|
|
31
|
+
const CHINA_TIMEZONE = 'Asia/Shanghai';
|
|
32
|
+
const chinaPartsFormatter = new Intl.DateTimeFormat('zh-CN', {
|
|
33
|
+
timeZone: CHINA_TIMEZONE,
|
|
34
|
+
year: 'numeric',
|
|
35
|
+
month: '2-digit',
|
|
36
|
+
day: '2-digit',
|
|
37
|
+
hour: '2-digit',
|
|
38
|
+
minute: '2-digit',
|
|
39
|
+
second: '2-digit',
|
|
40
|
+
hour12: false,
|
|
41
|
+
});
|
|
42
|
+
function getChinaParts(input = Date.now()) {
|
|
43
|
+
const parts = {};
|
|
44
|
+
const date = input instanceof Date ? input : new Date(input);
|
|
45
|
+
for (const item of chinaPartsFormatter.formatToParts(date)) {
|
|
46
|
+
if (item.type !== 'literal')
|
|
47
|
+
parts[item.type] = item.value;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
year: Number(parts.year || '0'),
|
|
51
|
+
month: Number(parts.month || '0'),
|
|
52
|
+
day: Number(parts.day || '0'),
|
|
53
|
+
hour: Number(parts.hour || '0'),
|
|
54
|
+
minute: Number(parts.minute || '0'),
|
|
55
|
+
second: Number(parts.second || '0'),
|
|
56
|
+
};
|
|
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
|
+
}
|
|
69
|
+
function classifyMerchantItem(item) {
|
|
70
|
+
const start = normalizeTimestamp(item?.start_time);
|
|
71
|
+
const end = normalizeTimestamp(item?.end_time);
|
|
72
|
+
if (!start || !end)
|
|
73
|
+
return 'normal';
|
|
74
|
+
const durationDays = (end - start) / (1000 * 60 * 60 * 24);
|
|
75
|
+
if (durationDays >= 2)
|
|
76
|
+
return 'weekend';
|
|
77
|
+
const startParts = getChinaParts(start);
|
|
78
|
+
const endParts = getChinaParts(end);
|
|
79
|
+
const startHour = startParts.hour + startParts.minute / 60;
|
|
80
|
+
const endHour = endParts.hour + endParts.minute / 60;
|
|
81
|
+
if (startHour <= 8 && endHour >= 23.5)
|
|
82
|
+
return 'normal';
|
|
83
|
+
return 'round';
|
|
84
|
+
}
|
|
18
85
|
function normalizeTimestamp(value) {
|
|
19
86
|
if (value === null || value === undefined || value === '')
|
|
20
87
|
return null;
|
|
@@ -29,16 +96,12 @@ function formatProductWindow(product) {
|
|
|
29
96
|
if (!start && !end)
|
|
30
97
|
return '';
|
|
31
98
|
const formatDate = (timestamp) => {
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
const day = String(date.getDate()).padStart(2, '0');
|
|
35
|
-
return `${month}-${day}`;
|
|
99
|
+
const parts = getChinaParts(timestamp);
|
|
100
|
+
return `${padNumber(parts.month)}-${padNumber(parts.day)}`;
|
|
36
101
|
};
|
|
37
102
|
const formatTime = (timestamp) => {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
const minute = String(date.getMinutes()).padStart(2, '0');
|
|
41
|
-
return `${hour}:${minute}`;
|
|
103
|
+
const parts = getChinaParts(timestamp);
|
|
104
|
+
return `${padNumber(parts.hour)}:${padNumber(parts.minute)}`;
|
|
42
105
|
};
|
|
43
106
|
if (start && end) {
|
|
44
107
|
const datePart = formatDate(start);
|
|
@@ -52,46 +115,86 @@ function getMerchantActivity(res) {
|
|
|
52
115
|
const activities = res?.merchantActivities || res?.merchant_activities || [];
|
|
53
116
|
return activities[0] || {};
|
|
54
117
|
}
|
|
55
|
-
function
|
|
118
|
+
function getMerchantProducts(res) {
|
|
56
119
|
const activity = getMerchantActivity(res);
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
120
|
+
const groups = [];
|
|
121
|
+
if (Array.isArray(activity?.products))
|
|
122
|
+
groups.push(activity.products);
|
|
123
|
+
if (Array.isArray(activity?.product_list))
|
|
124
|
+
groups.push(activity.product_list);
|
|
125
|
+
if (Array.isArray(activity?.get_props))
|
|
126
|
+
groups.push(activity.get_props);
|
|
127
|
+
if (Array.isArray(activity?.get_extra_props))
|
|
128
|
+
groups.push(activity.get_extra_props);
|
|
129
|
+
if (Array.isArray(activity?.get_pets))
|
|
130
|
+
groups.push(activity.get_pets);
|
|
131
|
+
const merged = [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
for (const list of groups) {
|
|
134
|
+
for (const item of list) {
|
|
135
|
+
const start = normalizeTimestamp(item?.start_time) ?? 0;
|
|
136
|
+
const end = normalizeTimestamp(item?.end_time) ?? Infinity;
|
|
137
|
+
const key = `${item?.id ?? ''}|${item?.name ?? ''}|${start}|${end === Infinity ? 'inf' : end}`;
|
|
138
|
+
if (seen.has(key))
|
|
139
|
+
continue;
|
|
140
|
+
seen.add(key);
|
|
141
|
+
merged.push(item);
|
|
76
142
|
}
|
|
77
143
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
144
|
+
return merged;
|
|
145
|
+
}
|
|
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
|
+
};
|
|
188
|
+
}
|
|
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);
|
|
85
193
|
const hours = Math.floor(diff / 3600000);
|
|
86
194
|
const mins = Math.floor((diff % 3600000) / 60000);
|
|
87
|
-
const datePart = [
|
|
88
|
-
now.getFullYear(),
|
|
89
|
-
String(now.getMonth() + 1).padStart(2, '0'),
|
|
90
|
-
String(now.getDate()).padStart(2, '0'),
|
|
91
|
-
].join('-');
|
|
92
195
|
return {
|
|
93
196
|
current: currentRound,
|
|
94
|
-
total:
|
|
197
|
+
total: ROUND_WINDOWS.length,
|
|
95
198
|
countdown: `${hours}\u5c0f\u65f6${mins}\u5206\u949f`,
|
|
96
199
|
is_open: currentRound !== null,
|
|
97
200
|
round_id: `${datePart}-${currentRound || 'closed'}`,
|
|
@@ -128,36 +231,272 @@ function isBotAdmin(session, adminUserIds) {
|
|
|
128
231
|
function sameStringArray(left, right) {
|
|
129
232
|
return JSON.stringify(left) === JSON.stringify(right);
|
|
130
233
|
}
|
|
131
|
-
function
|
|
132
|
-
const
|
|
133
|
-
const
|
|
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),
|
|
310
|
+
}));
|
|
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('');
|
|
346
|
+
}
|
|
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) {
|
|
134
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: [],
|
|
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);
|
|
135
413
|
const data = {
|
|
136
414
|
background: '',
|
|
137
|
-
title:
|
|
138
|
-
subtitle:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
products: products.map((p) => ({
|
|
143
|
-
name: p.name || TEXT.unknown,
|
|
144
|
-
image: p.icon_url || '',
|
|
145
|
-
time_label: formatProductWindow(p),
|
|
146
|
-
})),
|
|
415
|
+
title: getMerchantActivityTitle(res),
|
|
416
|
+
subtitle: getMerchantActivitySubtitle(res),
|
|
417
|
+
categories,
|
|
418
|
+
roundGroups,
|
|
419
|
+
total_products: products.length,
|
|
147
420
|
};
|
|
148
|
-
const fallback = products
|
|
149
|
-
? `\u8fdc\u884c\u5546\u4eba\u5f53\u524d\u5546\u54c1\uff1a${products.map((p) => p.name || TEXT.unknown).join('\u3001')}\n\u8f6e\u6b21\uff1a${roundInfo.current || TEXT.notOpen}\n\u5269\u4f59\uff1a${roundInfo.countdown}`
|
|
150
|
-
: '\u5f53\u524d\u8fdc\u884c\u5546\u4eba\u6682\u65e0\u5546\u54c1\u3002';
|
|
421
|
+
const fallback = buildMerchantFallbackText(TEXT.merchant, products, roundInfo);
|
|
151
422
|
return { products, roundInfo, data, fallback };
|
|
152
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);
|
|
458
|
+
}
|
|
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
|
+
};
|
|
491
|
+
}
|
|
153
492
|
async function checkMerchantSubscriptions(deps) {
|
|
154
493
|
const { ctx, client, merchantSubMgr, renderer, config } = deps;
|
|
155
494
|
const res = await client.getMerchantInfo(ctx, true);
|
|
156
495
|
if (!res)
|
|
157
496
|
return { subscriptions: 0, matched: 0, pushed: 0 };
|
|
158
|
-
const { products, roundInfo, data, fallback } =
|
|
497
|
+
const { products, roundInfo, data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
159
498
|
const productNames = products.map((p) => p.name || '').filter(Boolean);
|
|
160
|
-
const rendered = await renderer.renderHtml(ctx,
|
|
499
|
+
const rendered = await renderer.renderHtml(ctx, templateName, data);
|
|
161
500
|
const renderedImage = rendered ? (0, send_image_1.compressPngImage)(rendered, config) : null;
|
|
162
501
|
const subs = merchantSubMgr.getAll();
|
|
163
502
|
let matchedCount = 0;
|
|
@@ -214,10 +553,19 @@ function register(deps) {
|
|
|
214
553
|
const res = await client.getMerchantInfo(ctx, true);
|
|
215
554
|
if (!res)
|
|
216
555
|
return `\u83b7\u53d6\u8fdc\u884c\u5546\u4eba\u6570\u636e\u5931\u8d25\uff1a${client.getLastErrorBrief()}`;
|
|
217
|
-
const { data, fallback } =
|
|
218
|
-
const png = await deps.renderer.renderHtml(ctx,
|
|
556
|
+
const { data, fallback, templateName } = buildConfiguredMerchantRenderPayload(deps, res);
|
|
557
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
219
558
|
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren', deps.config);
|
|
220
559
|
});
|
|
560
|
+
ctx.command(TEXT.todayMerchant, '查看今日远行商人全部商品')
|
|
561
|
+
.action(async ({ session }) => {
|
|
562
|
+
const res = await client.getMerchantInfo(ctx, true);
|
|
563
|
+
if (!res)
|
|
564
|
+
return `获取今日远行商人数据失败:${client.getLastErrorBrief()}`;
|
|
565
|
+
const { data, fallback, templateName } = buildConfiguredTodayMerchantRenderPayload(deps, res);
|
|
566
|
+
const png = await deps.renderer.renderHtml(ctx, templateName, data);
|
|
567
|
+
await (0, send_image_1.sendImageWithFallback)(session, png, fallback, 'merchant:yuanxing-shangren:today', deps.config);
|
|
568
|
+
});
|
|
221
569
|
ctx.command(`${TEXT.subscribe} [args:text]`, '\u8ba2\u9605\u8fdc\u884c\u5546\u4eba\u5546\u54c1\u63d0\u9192')
|
|
222
570
|
.action(async ({ session }, args) => {
|
|
223
571
|
const target = getSubscriptionTarget(session);
|