timarai-dashboard-mcp 1.0.3

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.
@@ -0,0 +1,708 @@
1
+ import { statDateLabel } from "../date-range.js";
2
+ function rows(payload, key) {
3
+ const obj = (payload ?? {});
4
+ const value = obj[key];
5
+ return Array.isArray(value) ? value : [];
6
+ }
7
+ function num(value) {
8
+ const n = typeof value === "number" ? value : Number(value);
9
+ return Number.isFinite(n) ? n : 0;
10
+ }
11
+ // 序列日期字段可能是字符串、Unix 秒或毫秒,统一交给 statDateLabel 归一化。
12
+ function day(value) {
13
+ return statDateLabel(value);
14
+ }
15
+ function seriesFromPoints(payload, name, field, format) {
16
+ return {
17
+ name,
18
+ format,
19
+ values: rows(payload, "points").map((p) => num(p[field])),
20
+ };
21
+ }
22
+ function labelsFromPoints(payload) {
23
+ return rows(payload, "points").map((p) => day(p.statDate));
24
+ }
25
+ function labelsFromItems(payload, key = "items") {
26
+ return rows(payload, key).map((it) => String(it.dimension ?? it.code ?? it.channelCode ?? it.merchantName ?? "-"));
27
+ }
28
+ function seriesFromItems(payload, name, field, format, key = "items", fallbackField) {
29
+ return {
30
+ name,
31
+ format,
32
+ values: rows(payload, key).map((it) => it[field] === undefined || it[field] === null ? num(fallbackField ? it[fallbackField] : null) : num(it[field])),
33
+ };
34
+ }
35
+ /** 排行图用商户名,缺名时退回 ID 前 8 位,避免出现一排「-」。 */
36
+ function merchantLabels(payload) {
37
+ return rows(payload, "items").map((it) => {
38
+ const name = typeof it.merchantName === "string" && it.merchantName.trim() ? it.merchantName : "";
39
+ if (name)
40
+ return name;
41
+ const id = String(it.merchantId ?? "-");
42
+ return id.length > 8 ? `${id.slice(0, 8)}…` : id;
43
+ });
44
+ }
45
+ /**
46
+ * CEO 单指标趋势:只需换 metricType,其余完全一致。
47
+ *
48
+ * ⚠️ 接口侧已不再把多币种加总成一条序列——金额类趋势(GMV/Revenue/Profit)现在返回
49
+ * **「日期 × 币种」展开的点**(同一天会有 USD、USDT 各一个点,各自带真实 `currency`)。
50
+ * 这时如果仍按「一条序列 + 逐点标签」画折线,X 轴会出现重复日期,
51
+ * 折线在 USD/USDT 之间来回跳,画出来是一条完全错误的锯齿。
52
+ * 因此这里按币种拆成多条序列;单币种 / 无 currency 时保持原来的单序列行为。
53
+ */
54
+ function ceoTrend(metric, name, format) {
55
+ return (p) => {
56
+ const pts = rows(p, "points");
57
+ const currencies = [
58
+ ...new Set(pts.map((it) => (typeof it.currency === "string" && it.currency ? it.currency : ""))),
59
+ ].filter(Boolean);
60
+ // 单一币种(或接口未标币种):维持原有形态,避免给已有的图引入无谓改动。
61
+ if (currencies.length < 2) {
62
+ return { labels: labelsFromPoints(p), series: [seriesFromPoints(p, name, "value", format)] };
63
+ }
64
+ // 按首次出现顺序取去重日期;接口按「日 × 币种」成对返回,去重后即日期升序。
65
+ const labels = [...new Set(pts.map((it) => day(it.statDate)))];
66
+ const series = currencies.map((cur) => ({
67
+ name: `${name}·${cur}`,
68
+ format,
69
+ values: labels.map((d) => {
70
+ const hit = pts.find((it) => day(it.statDate) === d && it.currency === cur);
71
+ return hit ? num(hit.value) : 0;
72
+ }),
73
+ }));
74
+ return { labels, series };
75
+ };
76
+ }
77
+ function currencyOf(it) {
78
+ return typeof it.currency === "string" && it.currency ? it.currency : "";
79
+ }
80
+ /** CEO 业务维度:柱/环共用,只换字段与单位;多币种时按 mode 决定怎么拆。 */
81
+ function ceoBusiness(field, name, format, mode = "split") {
82
+ return (p) => {
83
+ const items = rows(p, "items");
84
+ const currencies = [...new Set(items.map(currencyOf))].filter(Boolean);
85
+ // 单一币种(或接口未标币种):维持原有形态,避免给已有的图引入无谓改动。
86
+ if (currencies.length < 2) {
87
+ return { labels: labelsFromItems(p), series: [seriesFromItems(p, name, field, format)] };
88
+ }
89
+ // 笔数类:跨币种可加,合并回一条;页面顺序按接口返回的业务线首次出现顺序。
90
+ if (mode === "sum") {
91
+ const labels = [...new Set(items.map((it) => String(it.dimension ?? "-")))];
92
+ return {
93
+ labels,
94
+ series: [
95
+ {
96
+ name,
97
+ format,
98
+ values: labels.map((l) => items
99
+ .filter((it) => String(it.dimension ?? "-") === l)
100
+ .reduce((a, it) => a + num(it[field]), 0)),
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ // 环形图:只画主币种。
106
+ if (mode === "primary") {
107
+ const totals = currencies.map((c) => ({
108
+ currency: c,
109
+ total: items.filter((it) => currencyOf(it) === c).reduce((a, it) => a + Math.abs(num(it[field])), 0),
110
+ }));
111
+ const winner = totals.reduce((a, b) => (b.total > a.total ? b : a)).currency;
112
+ const picked = items.filter((it) => currencyOf(it) === winner);
113
+ return {
114
+ labels: picked.map((it) => String(it.dimension ?? "-")),
115
+ series: [{ name: `${name}·${winner}`, format, values: picked.map((it) => num(it[field])) }],
116
+ };
117
+ }
118
+ // 柱状图:按币种分组,缺失组合补 0(否则两根柱会错位到错误的业务线下)。
119
+ const labels = [...new Set(items.map((it) => String(it.dimension ?? "-")))];
120
+ const series = currencies.map((cur) => ({
121
+ name: `${name}·${cur}`,
122
+ format,
123
+ values: labels.map((l) => {
124
+ const hit = items.find((it) => String(it.dimension ?? "-") === l && currencyOf(it) === cur);
125
+ return hit ? num(hit[field]) : 0;
126
+ }),
127
+ }));
128
+ return { labels, series };
129
+ };
130
+ }
131
+ /**
132
+ * `currency=MIXED` 时,`ceo/overview` 不再给跨币种加总的金额——
133
+ * `gmv` / `revenue` / `grossProfit` / `aum` 全部为 **null**,
134
+ * 真实数值改放在 `amountsByCurrency` 里按币种逐个给:
135
+ * `{currency, value(GMV), secondaryValue(收入), tertiaryValue(毛利), aum}`。
136
+ *
137
+ * KPI 卡必须显示数字,所以这里取**金额最大的那个币种**作为代表口径,
138
+ * 并把币种标进标签(「今日GMV (USD)」),避免读者以为是全平台合计。
139
+ * 非金额指标(笔数 / 商户数 / 成功率)本身与币种无关,仍取标量。
140
+ */
141
+ function pickPrimaryCurrency(payload) {
142
+ const o = (payload ?? {});
143
+ const list = Array.isArray(o.amountsByCurrency) ? o.amountsByCurrency : [];
144
+ if (list.length === 0)
145
+ return null;
146
+ const row = list.reduce((a, b) => (num(b.value) > num(a.value) ? b : a));
147
+ return { currency: String(row.currency ?? ""), row };
148
+ }
149
+ /** 金额取值:有标量用标量;为 null 时回退到主币种行的对应字段。 */
150
+ function moneyOrPrimary(o, key, abcField) {
151
+ const v = o[key];
152
+ if (v !== null && v !== undefined)
153
+ return num(v);
154
+ const primary = pickPrimaryCurrency(o);
155
+ return primary ? num(primary.row[abcField]) : 0;
156
+ }
157
+ const CEO_KPI_FORMATS = ["money", "number", "money", "money", "number", "number", "percent", "money"];
158
+ const OPS_KPI_FORMATS = ["number", "number", "number", "percent", "number", "number", "number", "seconds"];
159
+ const FIN_KPI_FORMATS = ["money", "money", "money", "money", "money", "money", "money", "money"];
160
+ export const CHART_SPECS = [
161
+ // ================================================================ CEO 舱
162
+ // 需求:8 个 KPI + 9 张图
163
+ {
164
+ id: "ceo.kpi",
165
+ cabin: "ceo",
166
+ title: "CEO 经营概览",
167
+ kind: "kpi",
168
+ endpointId: "ceo.overview",
169
+ purpose: "一眼看清 GMV、交易笔数、营业收入、毛利润、活跃/新增商户、成功率、AUM,含环比。",
170
+ formats: CEO_KPI_FORMATS,
171
+ extract: (payload) => {
172
+ const o = (payload ?? {});
173
+ // MIXED 时金额标量为 null,改取主币种(金额最大者)并在标签上标注币种。
174
+ const primary = o.currency === "MIXED" ? pickPrimaryCurrency(o) : null;
175
+ const tag = primary?.currency ? ` (${primary.currency})` : "";
176
+ return {
177
+ labels: [
178
+ `今日GMV${tag}`,
179
+ "交易笔数",
180
+ `营业收入${tag}`,
181
+ `毛利润${tag}`,
182
+ "活跃商户",
183
+ "新增商户",
184
+ "整体成功率",
185
+ `AUM${tag}`,
186
+ ],
187
+ series: [
188
+ {
189
+ name: "当前值",
190
+ values: [
191
+ moneyOrPrimary(o, "gmv", "value"),
192
+ num(o.transactionCount),
193
+ moneyOrPrimary(o, "revenue", "secondaryValue"),
194
+ moneyOrPrimary(o, "grossProfit", "tertiaryValue"),
195
+ num(o.activeMerchantCount),
196
+ num(o.newMerchantCount),
197
+ num(o.successRate),
198
+ moneyOrPrimary(o, "aum", "aum"),
199
+ ],
200
+ },
201
+ ],
202
+ };
203
+ },
204
+ },
205
+ // 图表1 GMV趋势(折线,近30天)
206
+ {
207
+ id: "ceo.gmv_trend",
208
+ cabin: "ceo",
209
+ title: "GMV 趋势",
210
+ kind: "line",
211
+ endpointId: "ceo.trend",
212
+ purpose: "需求图表1:每日 GMV 走势。口径为**成功订单金额**,退款单独统计、不冲减 GMV。",
213
+ extraBody: { metricType: "GMV" },
214
+ defaultPreset: "last_30d",
215
+ extract: ceoTrend("GMV", "GMV", "money"),
216
+ },
217
+ // 图表2 收入趋势(折线,近30天)
218
+ {
219
+ id: "ceo.revenue_trend",
220
+ cabin: "ceo",
221
+ title: "营业收入趋势",
222
+ kind: "line",
223
+ endpointId: "ceo.trend",
224
+ purpose: "需求图表2:每日平台手续费收入走势。",
225
+ extraBody: { metricType: "Revenue" },
226
+ defaultPreset: "last_30d",
227
+ extract: ceoTrend("Revenue", "营业收入", "money"),
228
+ },
229
+ // 图表3 利润趋势(折线,近30天)
230
+ {
231
+ id: "ceo.profit_trend",
232
+ cabin: "ceo",
233
+ title: "毛利润趋势",
234
+ kind: "line",
235
+ endpointId: "ceo.trend",
236
+ purpose: "需求图表3:每日毛利润(platform_revenue - channel_cost)走势,可能出现负值。",
237
+ extraBody: { metricType: "Profit" },
238
+ defaultPreset: "last_30d",
239
+ extract: ceoTrend("Profit", "毛利润", "money"),
240
+ },
241
+ // 图表9 平台管理资产趋势(折线,近30天)
242
+ {
243
+ id: "ceo.aum_trend",
244
+ cabin: "ceo",
245
+ title: "平台管理资产(AUM)趋势",
246
+ kind: "line",
247
+ endpointId: "ceo.trend",
248
+ purpose: "需求图表9:AUM 逐日快照走势(商户资产余额口径)。",
249
+ extraBody: { metricType: "AUM" },
250
+ defaultPreset: "last_30d",
251
+ extract: ceoTrend("AUM", "AUM", "money"),
252
+ },
253
+ // 补充:CEO 舱另外四个 KPI 的趋势(接口已支持对应 metricType)
254
+ {
255
+ id: "ceo.transaction_count_trend",
256
+ cabin: "ceo",
257
+ title: "成功交易笔数趋势",
258
+ kind: "line",
259
+ endpointId: "ceo.trend",
260
+ purpose: "每日**成功交易笔数**走势。与 GMV 搭配看,可判断增长来自「单笔变大」还是「笔数变多」。",
261
+ extraBody: { metricType: "TransactionCount" },
262
+ defaultPreset: "last_30d",
263
+ extract: ceoTrend("TransactionCount", "交易笔数", "number"),
264
+ },
265
+ {
266
+ id: "ceo.success_rate_trend",
267
+ cabin: "ceo",
268
+ title: "整体成功率趋势",
269
+ kind: "line",
270
+ endpointId: "ceo.trend",
271
+ purpose: "每日**整体成功率**走势。掉点通常先于 GMV 下滑出现,是最 early 的异常信号。",
272
+ extraBody: { metricType: "SuccessRate" },
273
+ defaultPreset: "last_30d",
274
+ extract: ceoTrend("SuccessRate", "成功率", "percent"),
275
+ },
276
+ {
277
+ id: "ceo.active_merchant_trend",
278
+ cabin: "ceo",
279
+ title: "活跃商户数趋势",
280
+ kind: "line",
281
+ endpointId: "ceo.trend",
282
+ purpose: "每日**活跃商户数**走势。看平台活跃度是在扩张还是靠少数大户撑着。",
283
+ extraBody: { metricType: "ActiveMerchantCount" },
284
+ defaultPreset: "last_30d",
285
+ extract: ceoTrend("ActiveMerchantCount", "活跃商户", "number"),
286
+ },
287
+ {
288
+ id: "ceo.new_merchant_trend",
289
+ cabin: "ceo",
290
+ title: "新增商户数趋势",
291
+ kind: "line",
292
+ endpointId: "ceo.trend",
293
+ purpose: "每日**新增商户数**走势,衡量获客节奏(非金额指标,不受币种影响)。",
294
+ extraBody: { metricType: "NewMerchantCount" },
295
+ defaultPreset: "last_30d",
296
+ extract: ceoTrend("NewMerchantCount", "新增商户", "number"),
297
+ },
298
+ // 图表4 业务GMV分析(柱状)
299
+ {
300
+ id: "ceo.business_gmv",
301
+ cabin: "ceo",
302
+ title: "业务 GMV 分析",
303
+ kind: "bar",
304
+ endpointId: "ceo.business-breakdown",
305
+ purpose: "需求图表4:各业务线 GMV 对比。",
306
+ extraBody: { metricType: "GMV" },
307
+ extract: ceoBusiness("value", "GMV", "money"),
308
+ },
309
+ // 图表5 业务收入分析(柱状)
310
+ {
311
+ id: "ceo.business_revenue",
312
+ cabin: "ceo",
313
+ title: "业务收入分析",
314
+ kind: "bar",
315
+ endpointId: "ceo.business-breakdown",
316
+ purpose: "需求图表5:各业务线手续费收入对比。",
317
+ extraBody: { metricType: "Revenue" },
318
+ extract: ceoBusiness("value", "营业收入", "money"),
319
+ },
320
+ // 图表6 业务利润分析(柱状)
321
+ {
322
+ id: "ceo.business_profit",
323
+ cabin: "ceo",
324
+ title: "业务利润分析",
325
+ kind: "bar",
326
+ endpointId: "ceo.business-breakdown",
327
+ purpose: "需求图表6:各业务线毛利润对比,可出现负值。",
328
+ extraBody: { metricType: "Profit" },
329
+ extract: ceoBusiness("value", "毛利润", "money"),
330
+ },
331
+ // 图表7 业务占比分析(环形,需求要求支持 GMV/收入/利润/订单数切换)
332
+ {
333
+ id: "ceo.business_share",
334
+ cabin: "ceo",
335
+ title: "业务 GMV 占比",
336
+ kind: "donut",
337
+ endpointId: "ceo.business-breakdown",
338
+ purpose: "需求图表7(默认口径):各业务线 GMV 占比。**多币种时只画主币种**(跨币种金额不可相加,合成一张饼会让扇形比例失真)。",
339
+ extraBody: { metricType: "GMV" },
340
+ extract: ceoBusiness("value", "GMV", "money", "primary"),
341
+ },
342
+ {
343
+ id: "ceo.business_share_revenue",
344
+ cabin: "ceo",
345
+ title: "业务收入占比",
346
+ kind: "donut",
347
+ endpointId: "ceo.business-breakdown",
348
+ purpose: "需求图表7 的「收入」口径切换。**多币种时只画主币种**。",
349
+ extraBody: { metricType: "Revenue" },
350
+ extract: ceoBusiness("value", "营业收入", "money", "primary"),
351
+ },
352
+ {
353
+ id: "ceo.business_share_profit",
354
+ cabin: "ceo",
355
+ title: "业务利润占比",
356
+ kind: "donut",
357
+ endpointId: "ceo.business-breakdown",
358
+ purpose: "需求图表7 的「利润」口径切换。存在负利润时占比会失真,此时建议改用柱状图。**多币种时只画主币种**。",
359
+ extraBody: { metricType: "Profit" },
360
+ extract: ceoBusiness("value", "毛利润", "money", "primary"),
361
+ },
362
+ {
363
+ id: "ceo.business_share_orders",
364
+ cabin: "ceo",
365
+ title: "业务订单数占比",
366
+ kind: "donut",
367
+ endpointId: "ceo.business-breakdown",
368
+ purpose: "需求图表7 的「订单数」口径切换。笔数与币种无关,多币种时按业务线**合并**计数。",
369
+ extraBody: { metricType: "OrderCount" },
370
+ extract: ceoBusiness("value", "订单数", "number", "sum"),
371
+ },
372
+ // 图表8 TOP10商户(GMV)排行榜
373
+ {
374
+ id: "ceo.top_merchants",
375
+ cabin: "ceo",
376
+ title: "TOP10 商户(GMV)",
377
+ kind: "hbar",
378
+ endpointId: "ceo.top-merchants",
379
+ purpose: "需求图表8:按 GMV 排序前 10 商户,看头部集中度。榜单长度由接口决定。",
380
+ extract: (p) => ({ labels: merchantLabels(p), series: [seriesFromItems(p, "GMV", "value", "money")] }),
381
+ },
382
+ // ================================================================ 运营舱
383
+ // 需求:8 个 KPI + 9 张图
384
+ {
385
+ id: "ops.kpi",
386
+ cabin: "operation",
387
+ title: "运营 KPI 概览",
388
+ kind: "kpi",
389
+ endpointId: "operation.overview",
390
+ purpose: "订单数/成功/失败/成功率/异常/处理中/退款/平均耗时,含环比。",
391
+ formats: OPS_KPI_FORMATS,
392
+ extract: (payload) => {
393
+ const o = (payload ?? {});
394
+ return {
395
+ labels: ["今日订单数", "成功订单", "失败订单", "整体成功率", "异常订单", "处理中订单", "退款订单", "平均处理耗时"],
396
+ series: [
397
+ {
398
+ name: "当前值",
399
+ values: [
400
+ num(o.orderCount),
401
+ num(o.successOrderCount),
402
+ num(o.failOrderCount),
403
+ num(o.successRate),
404
+ num(o.exceptionOrderCount),
405
+ num(o.processingOrderCount),
406
+ num(o.refundOrderCount),
407
+ num(o.averageProcessingSeconds),
408
+ ],
409
+ },
410
+ ],
411
+ };
412
+ },
413
+ },
414
+ // 图表1 订单趋势(折线,近30天)
415
+ {
416
+ id: "ops.order_trend",
417
+ cabin: "operation",
418
+ title: "订单趋势",
419
+ kind: "line",
420
+ endpointId: "operation.order-trend",
421
+ purpose: "需求图表1:每日订单量。⚠️ 该接口只返回**笔数**,不含金额。",
422
+ defaultPreset: "last_30d",
423
+ extract: (p) => ({ labels: labelsFromPoints(p), series: [seriesFromPoints(p, "订单量", "value", "number")] }),
424
+ },
425
+ // 图表2 成功率趋势
426
+ {
427
+ id: "ops.success_rate_trend",
428
+ cabin: "operation",
429
+ title: "成功率趋势",
430
+ kind: "line",
431
+ endpointId: "operation.success-rate-trend",
432
+ purpose: "需求图表2:每日成功率。分母为有效订单(成功+失败,排除处理中/退款)。",
433
+ defaultPreset: "last_30d",
434
+ extract: (p) => ({ labels: labelsFromPoints(p), series: [seriesFromPoints(p, "成功率", "value", "percent")] }),
435
+ },
436
+ // 图表3 异常订单趋势
437
+ {
438
+ id: "ops.exception_trend",
439
+ cabin: "operation",
440
+ title: "异常订单趋势",
441
+ kind: "line",
442
+ endpointId: "operation.exception-trend",
443
+ purpose: "需求图表3:每日异常订单量,用于发现突发异常。",
444
+ defaultPreset: "last_30d",
445
+ extract: (p) => ({ labels: labelsFromPoints(p), series: [seriesFromPoints(p, "异常订单", "value", "number")] }),
446
+ },
447
+ // 图表4 业务订单分析(柱状)
448
+ {
449
+ id: "ops.business_orders",
450
+ cabin: "operation",
451
+ title: "业务订单分析",
452
+ kind: "bar",
453
+ endpointId: "operation.business-analysis",
454
+ purpose: "需求图表4:各业务订单量对比。取 `count`(订单总数),不是 `value`(成功订单数)。",
455
+ extract: (p) => ({ labels: labelsFromItems(p), series: [seriesFromItems(p, "订单量", "count", "number")] }),
456
+ },
457
+ // 图表5 业务成功率分析(柱状)
458
+ {
459
+ id: "ops.business_success_rate",
460
+ cabin: "operation",
461
+ title: "业务成功率分析",
462
+ kind: "bar",
463
+ endpointId: "operation.business-analysis",
464
+ purpose: "需求图表5:各业务成功率对比。取 `successRate`;旧响应把成功率塞在 `share` 字段(彼时 share 并非占比),两种写法都兼容。",
465
+ extract: (p) => ({
466
+ labels: labelsFromItems(p),
467
+ series: [seriesFromItems(p, "成功率", "successRate", "percent", "items", "share")],
468
+ }),
469
+ },
470
+ // 图表6 渠道订单分析(柱状)
471
+ {
472
+ id: "ops.channel_orders",
473
+ cabin: "operation",
474
+ title: "渠道订单分析",
475
+ kind: "bar",
476
+ endpointId: "operation.channel-analysis",
477
+ purpose: "需求图表6:各渠道订单量对比。",
478
+ extract: (p) => ({ labels: labelsFromItems(p), series: [seriesFromItems(p, "订单量", "orderCount", "number")] }),
479
+ },
480
+ // 图表7 渠道成功率分析(柱状)
481
+ {
482
+ id: "ops.channel_success_rate",
483
+ cabin: "operation",
484
+ title: "渠道成功率分析",
485
+ kind: "bar",
486
+ endpointId: "operation.channel-analysis",
487
+ purpose: "需求图表7:各渠道成功率对比,配合渠道成本判断路由质量。",
488
+ extract: (p) => ({ labels: labelsFromItems(p), series: [seriesFromItems(p, "成功率", "successRate", "percent")] }),
489
+ },
490
+ // 图表8 失败原因占比(环形)
491
+ {
492
+ id: "ops.fail_reason",
493
+ cabin: "operation",
494
+ title: "失败原因占比",
495
+ kind: "donut",
496
+ endpointId: "operation.fail-reason-breakdown",
497
+ purpose: "需求图表8:失败原因构成(按笔数)。占比按 count 自行重算,不取服务端 share。",
498
+ extract: (p) => ({ labels: labelsFromItems(p), series: [seriesFromItems(p, "失败笔数", "count", "number")] }),
499
+ },
500
+ // 图表9 TOP10异常商户(排行榜)
501
+ {
502
+ id: "ops.top_exception_merchants",
503
+ cabin: "operation",
504
+ title: "TOP10 异常商户",
505
+ kind: "hbar",
506
+ endpointId: "operation.top-exception-merchants",
507
+ purpose: "需求图表9:异常最集中的商户,判断是个别商户还是普遍问题。",
508
+ extract: (p) => ({ labels: merchantLabels(p), series: [seriesFromItems(p, "异常量", "value", "number")] }),
509
+ },
510
+ // 补充:需求文末「所有指标都应能驱动行动」——增长类指标用于判断运营动作成效
511
+ {
512
+ id: "ops.merchant_growth",
513
+ cabin: "operation",
514
+ title: "商户增长与活跃",
515
+ kind: "line",
516
+ endpointId: "operation.platform-metrics",
517
+ purpose: "新增/激活/活跃商户逐日走势,看增长质量而非只看总量(非需求清单项,为运营行动提供对照)。",
518
+ defaultPreset: "last_30d",
519
+ extract: (p) => ({
520
+ labels: labelsFromPoints(p),
521
+ series: [
522
+ seriesFromPoints(p, "新增商户", "newMerchantCount", "number"),
523
+ seriesFromPoints(p, "激活商户", "activatedMerchantCount", "number"),
524
+ seriesFromPoints(p, "活跃商户", "activeMerchantCount", "number"),
525
+ ],
526
+ }),
527
+ },
528
+ // ================================================================ 财务舱
529
+ // 需求:8 个 KPI + 9 张图,主线是「资金闭环」:规模 → 流动 → 安全 → 分布
530
+ {
531
+ id: "fin.kpi",
532
+ cabin: "finance",
533
+ title: "财务资金概览",
534
+ kind: "kpi",
535
+ endpointId: "finance.overview",
536
+ purpose: "总资产、流入、流出、待结算、待对账、对账异常、冻结、可用余额,含环比。",
537
+ formats: FIN_KPI_FORMATS,
538
+ extract: (payload) => {
539
+ const o = (payload ?? {});
540
+ return {
541
+ labels: ["平台总资产", "今日流入", "今日流出", "待结算", "待对账", "对账异常", "冻结资金", "可用余额"],
542
+ series: [
543
+ {
544
+ name: "当前值",
545
+ values: [
546
+ num(o.totalAsset),
547
+ num(o.inAmount),
548
+ num(o.outAmount),
549
+ num(o.unsettledAmount),
550
+ num(o.unreconciledAmount),
551
+ num(o.exceptionAmount),
552
+ num(o.frozenAmount),
553
+ num(o.availableAmount),
554
+ ],
555
+ },
556
+ ],
557
+ };
558
+ },
559
+ },
560
+ // 图表1 资金流趋势(折线,近30天)
561
+ {
562
+ id: "fin.fund_trend",
563
+ cabin: "finance",
564
+ title: "资金流趋势",
565
+ kind: "line",
566
+ endpointId: "finance.fund-trend",
567
+ purpose: "需求图表1:每日流入与流出双线对比,识别资金拐点。",
568
+ defaultPreset: "last_30d",
569
+ extract: (p) => ({
570
+ labels: labelsFromPoints(p),
571
+ series: [
572
+ seriesFromPoints(p, "流入", "value", "money"),
573
+ seriesFromPoints(p, "流出", "secondaryValue", "money"),
574
+ ],
575
+ }),
576
+ },
577
+ // 图表2 账户余额趋势(折线,近30天)
578
+ {
579
+ id: "fin.balance_trend",
580
+ cabin: "finance",
581
+ title: "账户余额趋势",
582
+ kind: "line",
583
+ endpointId: "finance.balance-trend",
584
+ purpose: "需求图表2:每日余额快照。⚠️ 副线是**冻结资金**,不是流出。",
585
+ defaultPreset: "last_30d",
586
+ extract: (p) => ({
587
+ labels: labelsFromPoints(p),
588
+ series: [
589
+ seriesFromPoints(p, "余额", "value", "money"),
590
+ seriesFromPoints(p, "冻结", "secondaryValue", "money"),
591
+ ],
592
+ }),
593
+ },
594
+ // 图表3 业务资金分布(柱状)
595
+ {
596
+ id: "fin.fund_distribution",
597
+ cabin: "finance",
598
+ title: "业务资金分布",
599
+ kind: "bar",
600
+ endpointId: "finance.fund-distribution",
601
+ purpose: "需求图表3:各业务的**流入 vs 流出**分组对比。⚠️ 接口给的是双向流量(In/Out),不是「资金规模」。",
602
+ extract: (p) => ({
603
+ labels: labelsFromItems(p),
604
+ series: [
605
+ seriesFromItems(p, "流入", "value", "money"),
606
+ seriesFromItems(p, "流出", "secondaryValue", "money"),
607
+ ],
608
+ }),
609
+ },
610
+ // 图表4 账户资金分布(柱状)
611
+ {
612
+ id: "fin.balance_distribution",
613
+ cabin: "finance",
614
+ title: "账户资金分布",
615
+ kind: "bar",
616
+ endpointId: "finance.balance-distribution",
617
+ purpose: "需求图表4:按账户类型的余额与冻结资金对比。",
618
+ extract: (p) => ({
619
+ labels: labelsFromItems(p),
620
+ series: [
621
+ seriesFromItems(p, "余额", "value", "money"),
622
+ seriesFromItems(p, "冻结", "secondaryValue", "money"),
623
+ ],
624
+ }),
625
+ },
626
+ // 图表5 结算趋势(折线,近30天)
627
+ {
628
+ id: "fin.settlement_trend",
629
+ cabin: "finance",
630
+ title: "结算趋势",
631
+ kind: "line",
632
+ endpointId: "finance.settlement-trend",
633
+ purpose: "需求图表5:每日结算金额与成功结算金额走势,看结算节奏与达成率。",
634
+ defaultPreset: "last_30d",
635
+ extract: (p) => ({
636
+ labels: labelsFromPoints(p),
637
+ series: [
638
+ seriesFromPoints(p, "结算金额", "value", "money"),
639
+ seriesFromPoints(p, "成功结算", "secondaryValue", "money"),
640
+ ],
641
+ }),
642
+ },
643
+ // 图表5 的加强版:柱=结算金额,线=成功结算率
644
+ {
645
+ id: "fin.settlement_combo",
646
+ cabin: "finance",
647
+ title: "结算金额与达成率",
648
+ kind: "combo",
649
+ endpointId: "finance.settlement-trend",
650
+ purpose: "需求图表5 的加强视图:柱=结算金额,线=成功结算金额,同时看规模与达成情况。",
651
+ defaultPreset: "last_30d",
652
+ extract: (p) => ({
653
+ labels: labelsFromPoints(p),
654
+ series: [
655
+ { ...seriesFromPoints(p, "结算金额", "value", "money"), kind: "bar", axis: "left" },
656
+ { ...seriesFromPoints(p, "成功结算", "secondaryValue", "money"), kind: "line", axis: "right" },
657
+ ],
658
+ }),
659
+ },
660
+ // 图表6 对账结果分析(环形)
661
+ {
662
+ id: "fin.reconciliation",
663
+ cabin: "finance",
664
+ title: "对账结果分析",
665
+ kind: "donut",
666
+ endpointId: "finance.reconciliation-analysis",
667
+ purpose: "需求图表6:对账结果分布。按需求口径 `COUNT GROUP BY result_status` 取**笔数**(接口 share 亦按笔数)。",
668
+ extract: (p) => ({ labels: labelsFromItems(p), series: [seriesFromItems(p, "对账笔数", "count", "number")] }),
669
+ },
670
+ // 图表7 账户资金占比(环形)
671
+ {
672
+ id: "fin.balance_share",
673
+ cabin: "finance",
674
+ title: "账户资金占比",
675
+ kind: "donut",
676
+ endpointId: "finance.balance-distribution",
677
+ purpose: "需求图表7:按账户类型的余额占比。",
678
+ extract: (p) => ({ labels: labelsFromItems(p), series: [seriesFromItems(p, "余额", "value", "money")] }),
679
+ },
680
+ // 图表8 TOP10商户资金余额(排行榜)
681
+ {
682
+ id: "fin.top_merchant_balances",
683
+ cabin: "finance",
684
+ title: "TOP10 商户资金余额",
685
+ kind: "hbar",
686
+ endpointId: "finance.top-merchant-balances",
687
+ purpose: "需求图表8:余额最集中的商户,看资金集中度风险。",
688
+ extract: (p) => ({ labels: merchantLabels(p), series: [seriesFromItems(p, "余额", "value", "money")] }),
689
+ },
690
+ // 图表9 TOP10对账异常商户(排行榜)
691
+ {
692
+ id: "fin.top_reconciliation_merchants",
693
+ cabin: "finance",
694
+ title: "TOP10 对账异常商户",
695
+ kind: "hbar",
696
+ endpointId: "finance.top-reconciliation-merchants",
697
+ purpose: "需求图表9:对账异常金额最集中的商户。",
698
+ extract: (p) => ({ labels: merchantLabels(p), series: [seriesFromItems(p, "异常金额", "value", "money")] }),
699
+ },
700
+ ];
701
+ const BY_ID = new Map(CHART_SPECS.map((c) => [c.id, c]));
702
+ export function getChartSpec(id) {
703
+ return BY_ID.get(id);
704
+ }
705
+ export function chartsByCabin(cabin) {
706
+ return CHART_SPECS.filter((c) => c.cabin === cabin);
707
+ }
708
+ //# sourceMappingURL=specs.js.map