Skip to content

Commit 25de12b

Browse files
committed
固定成本支持购买日期:按生效区间摊销,到期归零 (v1.6.1)
- 固定成本渠道新增「购买日期」(新建默认今天,可改成实际购买日) - 利润计算只对 [购买日, 购买日+覆盖天数] 与统计窗口的重叠天数摊销: 期中购买按实际生效天数计,已到期归零并标注「已到期」; 未填日期保持全窗口摊销(适合自动续费的常驻成本) - 站点卡片显示生效信息:起始日期 · 剩 N 天(≤3 天转黄), 到期后日均归零、红字提示「续费请更新日期」 - 利润成本明细行带生效/到期标注 - 「我的站点」页移除数据来源提示(上版遗留确认)
1 parent 30434d2 commit 25de12b

6 files changed

Lines changed: 50 additions & 14 deletions

File tree

lib/store.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,11 @@ export class Store {
190190
cnyPerUsd: numOrNull(input.cnyPerUsd),
191191
// 我自己的中转站:启用「我的站点」下游用量分析(需管理员令牌)
192192
isOwn: !!input.isOwn,
193-
// 固定成本:每次付费金额(¥)+ 覆盖天数,日均摊销 = 金额 ÷ 天数
193+
// 固定成本:每次付费金额(¥)+ 覆盖天数 + 购买日期,日均摊销 = 金额 ÷ 天数,
194+
// 只在 [购买日, 购买日+天数] 区间内计成本,到期归零
194195
fixedCostCny: numOrNull(input.fixedCostCny),
195196
fixedDays: numOrNull(input.fixedDays),
197+
fixedStartDate: /^\d{4}-\d{2}-\d{2}$/.test(input.fixedStartDate || "") ? input.fixedStartDate : null,
196198
demo: !!input.demo,
197199
createdAt: new Date().toISOString(),
198200
s2Tokens: null, // Sub2API 密码模式的令牌缓存 {accessToken, refreshToken, expiresAt}
@@ -219,6 +221,9 @@ export class Store {
219221
if ("isOwn" in patch) s.isOwn = !!patch.isOwn;
220222
if ("fixedCostCny" in patch) s.fixedCostCny = numOrNull(patch.fixedCostCny);
221223
if ("fixedDays" in patch) s.fixedDays = numOrNull(patch.fixedDays);
224+
if ("fixedStartDate" in patch) {
225+
s.fixedStartDate = /^\d{4}-\d{2}-\d{2}$/.test(patch.fixedStartDate || "") ? patch.fixedStartDate : null;
226+
}
222227
// 凭证或站点实际变化才作废令牌缓存(前端编辑总会带上 type/email 原值,
223228
// 无脑作废会导致每次改名都触发一次完整重登录)
224229
const credsChanged =

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "relay-monitor",
3-
"version": "1.6.0",
3+
"version": "1.6.1",
44
"private": true,
55
"description": "监控 sub2api / new-api 中转站余额的网页面板",
66
"type": "module",

public/app.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,17 +190,28 @@ function stationRow(s) {
190190
// 固定成本渠道:不访问接口,只展示摊销信息
191191
if (s.type === "fixed") {
192192
const daily = s.fixedCostCny > 0 && s.fixedDays > 0 ? s.fixedCostCny / s.fixedDays : 0;
193+
const pieces = [`<span>日均摊销 ${cny(daily)}</span>`, `<span>每次 ${cny(s.fixedCostCny || 0)}${s.fixedDays || 0} 天</span>`];
194+
let expired = false;
195+
if (s.fixedStartDate && s.fixedDays > 0) {
196+
const start = new Date(s.fixedStartDate + "T00:00:00");
197+
const end = start.getTime() + s.fixedDays * 86400000;
198+
const remain = Math.ceil((end - Date.now()) / 86400000);
199+
expired = remain <= 0;
200+
pieces.push(expired
201+
? `<span class="danger">已于 ${fmtClock(end).split(" ")[0]} 到期,续费请更新日期</span>`
202+
: `<span${remain <= 3 ? ' class="warn"' : ""}>${esc(s.fixedStartDate)} 起 · 剩 ${remain} 天</span>`);
203+
}
193204
return `
194205
<div class="st-row" data-id="${s.id}">
195206
<div class="st-plate">¥</div>
196207
<div class="st-main" style="cursor:default">
197208
<div class="st-name">${esc(s.name)}<span class="demo-tag">固定成本</span></div>
198209
<div class="st-meta">${s.baseUrl ? esc(s.baseUrl) + " · " : ""}不访问接口 · 仅计入利润成本</div>
199-
<div class="st-predict"><span>日均摊销 ${cny(daily)}</span><span>·</span><span>每次 ${cny(s.fixedCostCny || 0)}${s.fixedDays || 0} 天</span></div>
210+
<div class="st-predict">${pieces.join("<span>·</span>")}</div>
200211
</div>
201212
<div class="st-balance">
202-
<div class="amt">${cny(daily)}</div>
203-
<div class="sub">每天</div>
213+
<div class="amt${expired ? " danger" : ""}">${cny(expired ? 0 : daily)}</div>
214+
<div class="sub">${expired ? "已到期" : "每天"}</div>
204215
</div>
205216
<div class="st-actions">
206217
<button class="icon-btn" data-act="edit" title="编辑"><svg viewBox="0 0 24 24"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg></button>
@@ -989,7 +1000,9 @@ function profitSection(p) {
9891000
const costRows = p.costs.map((c) => `
9901001
<div class="st-row profit-row">
9911002
<div class="st-main" style="cursor:default">
992-
<div class="st-name">${esc(c.name)} <span class="demo-tag">${MODE_LABEL[c.mode] || c.mode}</span></div>
1003+
<div class="st-name">${esc(c.name)} <span class="demo-tag">${MODE_LABEL[c.mode] || c.mode}</span>${
1004+
c.note ? ` <span class="demo-tag${c.note === "已到期" ? " tag-warn" : ""}">${esc(c.note)}</span>` : ""
1005+
}</div>
9931006
<div class="st-meta">渠道:${esc(c.channels.join("、"))}</div>
9941007
</div>
9951008
<div class="st-balance"><div class="amt">${cny(c.cny)}</div><div class="sub">期内成本</div></div>
@@ -1230,6 +1243,8 @@ function openModal(station) {
12301243
$("#f-cnyRate").value = station?.cnyPerUsd ?? "";
12311244
$("#f-fixedCost").value = station?.fixedCostCny ?? "";
12321245
$("#f-fixedDays").value = station?.fixedDays ?? "";
1246+
// 新建默认今天;编辑回显已存日期(本地时区的 YYYY-MM-DD)
1247+
$("#f-fixedStart").value = station ? (station.fixedStartDate || "") : new Date().toLocaleDateString("en-CA");
12331248
$("#f-own").checked = !!station?.isOwn;
12341249
if (station) {
12351250
$("#f-accessToken").placeholder = station.hasAccessToken ? "已配置,留空保持不变" : "令牌 / JWT";
@@ -1281,6 +1296,7 @@ $("#modalSave").onclick = async () => {
12811296
cnyPerUsd: $("#f-cnyRate").value.trim(),
12821297
fixedCostCny: $("#f-fixedCost").value.trim(),
12831298
fixedDays: $("#f-fixedDays").value.trim(),
1299+
fixedStartDate: $("#f-fixedStart").value,
12841300
isOwn: $("#f-type").value === "newapi" && $("#f-own").checked,
12851301
};
12861302
const at = $("#f-accessToken").value.trim();

public/index.html

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,13 +154,14 @@ <h2 id="modalTitle">添加中转站</h2>
154154
<div class="hint">面板金额将按此汇率折算成人民币展示;余额告警仍按站点余额判断。</div>
155155
</div>
156156
<div class="form-field" id="f-fixedWrap">
157-
<label>固定成本(每次付费金额 + 覆盖天数)</label>
157+
<label>固定成本(每次付费金额 + 覆盖天数 + 购买日期</label>
158158
<div class="field-inline">
159-
<input class="input" id="f-fixedCost" placeholder="每次付多少(¥),如 199" style="flex:1" />
160-
<input class="input" id="f-fixedDays" placeholder="管多少天,如 30" style="width:110px" />
159+
<input class="input" id="f-fixedCost" placeholder="每次付多少(¥)" style="flex:1" />
160+
<input class="input" id="f-fixedDays" placeholder="管多少天" style="width:96px" />
161+
<input class="input" type="date" id="f-fixedStart" style="width:150px" />
161162
</div>
162-
<div class="hint">不访问任何接口;日均成本 = 金额 ÷ 天数,均摊计入利润
163-
站点地址可留空;填上游地址可与「我的站点」渠道自动匹配。</div>
163+
<div class="hint">日均成本 = 金额 ÷ 天数,从购买日起摊销,到期后不再计成本(续费改日期即可)
164+
不访问任何接口;站点地址可留空,填上游地址可与「我的站点」渠道自动匹配。</div>
164165
</div>
165166
<div class="form-field" id="f-own-wrap">
166167
<label class="chk-line"><input type="checkbox" id="f-own" /> 这是我自己的中转站</label>

public/styles.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ button { font-family: inherit; cursor: pointer; }
124124
.stat-card .value.danger { color: var(--text-danger); }
125125
.stat-card .value.good { color: var(--text-success); }
126126
.profit-row { padding: 10px 16px; }
127+
.demo-tag.tag-warn { color: var(--text-warning); border-color: color-mix(in srgb, var(--text-warning) 30%, var(--bg-surface)); }
128+
.st-balance .amt.danger { color: var(--text-danger); }
127129
.stat-card .stat-sub { font-size: 11px; color: var(--text-tertiary); margin-top: 3px; font-variant-numeric: tabular-nums; }
128130

129131
.section-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }

server.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -599,15 +599,27 @@ async function computeProfit(own, incomeUsd, { startMs, now, tz, range }) {
599599
const windowDays = (now - startMs) / 86400000;
600600
const rateOf = (s) => (s.cnyPerUsd != null && s.cnyPerUsd > 0 ? s.cnyPerUsd : 1);
601601

602-
// 固定成本:无论是否匹配到渠道都计入(服务器租金这类可以完全不填地址)
602+
// 固定成本:无论是否匹配到渠道都计入(服务器租金这类可以完全不填地址)。
603+
// 配置了购买日期的,只对 [购买日, 购买日+天数] 与展示窗口的重叠部分摊销,到期归零
603604
const costs = [];
604605
for (const s of upstreams) {
605606
const df = dailyFixedCny(s);
606607
if (df == null) continue;
608+
let activeDays = windowDays;
609+
let note = null;
610+
if (s.fixedStartDate) {
611+
const st = parseDateLabel(s.fixedStartDate, tz);
612+
if (st != null) {
613+
const end = st + s.fixedDays * 86400000;
614+
activeDays = Math.max(0, Math.min(now, end) - Math.max(startMs, st)) / 86400000;
615+
if (end <= now) note = "已到期";
616+
else if (st > startMs) note = `${s.fixedStartDate} 起`;
617+
}
618+
}
607619
costs.push({
608-
stationId: s.id, name: s.name, mode: "fixed",
620+
stationId: s.id, name: s.name, mode: "fixed", note,
609621
channels: matched.get(s.id)?.channels || ["未匹配渠道 · 通用成本"],
610-
cny: r2(df * windowDays),
622+
cny: r2(df * activeDays),
611623
});
612624
}
613625
// 按用量:匹配到渠道且未配置固定成本的上游

0 commit comments

Comments
 (0)