koishi-plugin-prism 0.1.23 → 0.1.27

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/README.md CHANGED
@@ -56,6 +56,8 @@ pricingConfigIds: [pricing-mahjong-a]
56
56
  * `login` / `入场` - 开启当前玩家的计费场次。
57
57
  * `logout` - 结算当前玩家的计费场次。
58
58
  玩家在未产生任何费用时退场,机器人会简洁显示“本次未产生费用”和当前余额;存在收费或优惠明细时仍显示完整结算账单。
59
+ 结账成功回执中的余额为后端已经完成扣款后的余额;只有 `/billing` 预览会显示当前余额与预计结账后余额。
60
+ 方案内区间封顶直接计入对应计时费用;全局封顶按日期和时段逐条列出,不合并、不截断,并直接形成计费总价,不作为优惠。只作用于整次结账的资产优惠直接列在计费总价下方,不显示额外标题或 emoji,也不会误归属到最后一个 session。
59
61
  结账成功后,机器人会向 `staffUserIds` 与 `logoutNotifyUserIds` 中的用户私聊同一份账单,账单会明确显示结账玩家身份。
60
62
  同一玩家在前一次退场结账尚未完成时重复发送 `/logout` 或 `/退场`,机器人会复用同一次结账请求与账单,避免重复扣款。
61
63
  账单和管理员代操作回执均使用“平台昵称(QQ:号码)”称呼玩家;平台暂时无法提供昵称时显示“未知昵称(QQ:号码)”,不会显示内部玩家 ID。
package/lib/index.js CHANGED
@@ -510,22 +510,33 @@ class PrismKoishiService {
510
510
  const result = (await this.client.confirmCheckoutByIdentity(this.identity(sender), false));
511
511
  const settlement = result?.playerSettlement ?? result?.settlement ?? {};
512
512
  const records = result?.settlements ?? [];
513
+ const checkoutAdjustments = (result?.checkoutAdjustments ?? []);
514
+ const pricingCapAdjustments = (result?.pricingCapAdjustments ?? []);
515
+ const checkoutAdjustmentKeys = new Set(checkoutAdjustments.map(adjustmentKey));
516
+ const pricingCapAdjustmentKeys = new Set(pricingCapAdjustments.map(adjustmentKey));
513
517
  const sessionPreviews = records.map((rec) => {
514
518
  const s = rec?.settlement ?? {};
519
+ const sessionAdjustments = (rec?.adjustments ?? []).filter((adjustment) => {
520
+ const key = adjustmentKey(adjustment);
521
+ return !checkoutAdjustmentKeys.has(key) &&
522
+ !pricingCapAdjustmentKeys.has(key) &&
523
+ !isPricingCapAdjustment(adjustment);
524
+ });
525
+ const sessionSubtotal = toNumber(s.subtotal ?? 0);
515
526
  return {
516
527
  sessionId: s.sessionId,
517
528
  label: s.label,
518
529
  startedAt: s.startedAt,
519
530
  endedAt: s.endedAt ?? s.settledAt,
520
531
  status: "closed",
521
- subtotal: s.subtotal ?? 0,
522
- total: s.total ?? 0,
532
+ subtotal: sessionSubtotal,
533
+ total: Math.max(0, sessionSubtotal + sessionAdjustments.reduce((sum, adjustment) => sum + toNumber(adjustment?.amount ?? 0), 0)),
523
534
  chargeItems: rec?.chargeItems ?? [],
524
- adjustments: rec?.adjustments ?? [],
535
+ adjustments: sessionAdjustments,
525
536
  };
526
537
  });
527
538
  const synthetic = {
528
- settlementPreview: {
539
+ settlement: {
529
540
  playerId: settlement.playerId,
530
541
  subtotal: settlement.subtotal ?? 0,
531
542
  total: settlement.total ?? 0,
@@ -533,6 +544,9 @@ class PrismKoishiService {
533
544
  sessionPreviews,
534
545
  chargeItems: result?.chargeItems ?? [],
535
546
  adjustments: result?.adjustments ?? [],
547
+ checkoutAdjustments,
548
+ pricingCapAdjustments,
549
+ globalCapWindows: result?.globalCapWindows ?? [],
536
550
  assetHoldings: result?.assetHoldings ?? [],
537
551
  };
538
552
  const receipt = await this.formatCheckoutPreview(synthetic, sender, "✅ 退场成功 · 结算账单");
@@ -892,7 +906,10 @@ class PrismKoishiService {
892
906
  },
893
907
  ];
894
908
  }
895
- const adjustments = result?.adjustments ?? [];
909
+ const adjustments = (result?.adjustments ?? []);
910
+ const pricingCapAdjustments = (result?.pricingCapAdjustments ?? adjustments.filter(isPricingCapAdjustment));
911
+ const sessionAdjustmentKeys = new Set(sessionPreviews.flatMap((session) => (session?.adjustments ?? []).map(adjustmentKey)));
912
+ const checkoutAdjustments = (result?.checkoutAdjustments ?? adjustments.filter((adjustment) => !isPricingCapAdjustment(adjustment) && !sessionAdjustmentKeys.has(adjustmentKey(adjustment))));
896
913
  const assetHoldings = result?.assetHoldings ?? [];
897
914
  const lines = [];
898
915
  lines.push(title);
@@ -907,7 +924,7 @@ class PrismKoishiService {
907
924
  }
908
925
  }
909
926
  const hasNonZeroSessionTotal = sessionPreviews.some((session) => toNumber(session?.total ?? 0) !== 0);
910
- const hasNonZeroAdjustment = hasAdjustmentEntries(adjustments, sessionPreviews);
927
+ const hasNonZeroAdjustment = hasAdjustmentEntries(checkoutAdjustments, sessionPreviews);
911
928
  if (!hasNonZeroSessionTotal && !hasNonZeroAdjustment) {
912
929
  lines.push("");
913
930
  lines.push("本次未产生费用");
@@ -947,10 +964,51 @@ class PrismKoishiService {
947
964
  lines.push(` └ ${adjLabel}:${formatNumber(amount)}${currency}`);
948
965
  }
949
966
  }
967
+ const cappedWindows = (result?.globalCapWindows ?? []).filter((window) => toNumber(window?.currentAmount) !== toNumber(window?.amountApplied));
968
+ const appliedCapAdjustments = pricingCapAdjustments.filter((adjustment) => toNumber(adjustment?.amount ?? 0) !== 0);
969
+ if (cappedWindows.length > 0 || appliedCapAdjustments.length > 0) {
970
+ lines.push("");
971
+ lines.push("封顶:");
972
+ if (cappedWindows.length > 0) {
973
+ for (const window of cappedWindows) {
974
+ const label = firstDefined(window, "ruleLabel", "label", "name") ?? "封顶时段";
975
+ const startedAt = parseDateTime(window?.windowStartedAt);
976
+ const datedLabel = startedAt ? `${formatMD(startedAt)} ${label}` : label;
977
+ const currentAmount = toNumber(window?.currentAmount);
978
+ const amountApplied = toNumber(window?.amountApplied);
979
+ const priceCap = toNumber(window?.priceCap);
980
+ const paidBefore = toNumber(window?.paidBefore);
981
+ const details = [`上限${formatNumber(priceCap)}`];
982
+ if (paidBefore > 0)
983
+ details.push(`已计${formatNumber(paidBefore)}`);
984
+ lines.push(`- ${datedLabel}:${formatNumber(currentAmount)} → ${formatNumber(amountApplied)}${currency}(${details.join(",")})`);
985
+ }
986
+ }
987
+ else {
988
+ for (const adjustment of appliedCapAdjustments) {
989
+ const label = firstDefined(adjustment, "label", "name", "source") ?? "封顶时段";
990
+ lines.push(`- ${label}:计费调整 ${formatNumber(adjustment?.amount ?? 0)}${currency}`);
991
+ }
992
+ }
993
+ }
994
+ const visibleCheckoutAdjustments = checkoutAdjustments.filter((adjustment) => toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0)) !== 0);
950
995
  lines.push("");
951
- lines.push(`计费总价:${formatNumber(subtotal)}${currency}`);
952
- if (hasNonZeroAdjustment)
953
- lines.push(`优惠后价格:${formatNumber(total)}${currency}`);
996
+ const cappedTotal = Math.max(0, toNumber(subtotal) + pricingCapAdjustments.reduce((sum, adjustment) => sum + toNumber(adjustment?.amount ?? 0), 0));
997
+ lines.push(`计费总价:${formatNumber(cappedTotal)}${currency}`);
998
+ const hasManualAdjustment = visibleCheckoutAdjustments.some((adjustment) => cleanText(adjustment?.source).startsWith("staff.override:"));
999
+ if (visibleCheckoutAdjustments.length > 0) {
1000
+ lines.push("");
1001
+ for (const adjustment of visibleCheckoutAdjustments) {
1002
+ const amount = toNumber(firstDefined(adjustment ?? {}, "amount", "saved", 0));
1003
+ const label = firstDefined(adjustment ?? {}, "label", "name", "source") ?? "优惠";
1004
+ lines.push(`${label}:${formatNumber(amount)}${currency}`);
1005
+ }
1006
+ }
1007
+ if (hasNonZeroAdjustment) {
1008
+ if (visibleCheckoutAdjustments.length > 0)
1009
+ lines.push("");
1010
+ lines.push(`${hasManualAdjustment ? "调整后价格" : "优惠后价格"}:${formatNumber(total)}${currency}`);
1011
+ }
954
1012
  if (hasBalance) {
955
1013
  const isPreview = result?.settlementPreview != null && result?.settlement == null;
956
1014
  if (isPreview) {
@@ -1137,6 +1195,21 @@ function formatPlayerReference(sender, provider = "qq") {
1137
1195
  const name = sender.name && sender.name !== sender.id ? sender.name : "未知昵称";
1138
1196
  return `${name}(${provider.toUpperCase()}:${sender.id})`;
1139
1197
  }
1198
+ function adjustmentKey(adjustment) {
1199
+ const id = cleanText(adjustment?.id);
1200
+ if (id)
1201
+ return `id:${id}`;
1202
+ return JSON.stringify([
1203
+ adjustment?.source ?? "",
1204
+ adjustment?.label ?? "",
1205
+ toNumber(adjustment?.amount ?? 0),
1206
+ ]);
1207
+ }
1208
+ function isPricingCapAdjustment(adjustment) {
1209
+ return adjustment?.pricingCapHistory != null ||
1210
+ cleanText(adjustment?.source).startsWith("time.cap:") ||
1211
+ cleanText(adjustment?.id).startsWith("time-cap:");
1212
+ }
1140
1213
  function toNumber(value) {
1141
1214
  if (value == null)
1142
1215
  return 0;
@@ -1197,6 +1270,10 @@ function formatHM(dt) {
1197
1270
  const pad = (n) => String(n).padStart(2, "0");
1198
1271
  return `${pad(dt.getHours())}:${pad(dt.getMinutes())}`;
1199
1272
  }
1273
+ function formatMD(dt) {
1274
+ const pad = (n) => String(n).padStart(2, "0");
1275
+ return `${pad(dt.getMonth() + 1)}-${pad(dt.getDate())}`;
1276
+ }
1200
1277
  function formatDateTime(value) {
1201
1278
  const dt = parseDateTime(value);
1202
1279
  if (!dt)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-prism",
3
- "version": "0.1.23",
3
+ "version": "0.1.27",
4
4
  "description": "PRiSM Next 计费与设备管理系统的 Koishi 机器人集成插件",
5
5
  "main": "./lib/index.js",
6
6
  "exports": {