@@ -22,6 +22,12 @@ function numOrNull(v) {
2222 return Number . isFinite ( n ) ? n : null ;
2323}
2424
25+ // 成本渠道匹配别名:用于渠道配置采用容器域名/IP、与监控地址不一致的场景。
26+ function sanitizeCostAliases ( input ) {
27+ const rows = Array . isArray ( input ) ? input : String ( input || "" ) . split ( / [ \n , ] / ) ;
28+ return [ ...new Set ( rows . map ( ( v ) => String ( v || "" ) . trim ( ) ) . filter ( Boolean ) ) ] ;
29+ }
30+
2531// 固定成本付费记录:[{amount(¥), days, startDate|null}],无效行直接丢弃
2632function sanitizePurchases ( input ) {
2733 if ( ! Array . isArray ( input ) ) return null ;
@@ -92,6 +98,8 @@ export class Store {
9298 for ( const s of this . data . stations ) {
9399 // v2.1:旧站点没有续费计划字段,默认保持原有的正常重复提醒策略。
94100 s . noRenewal = ! ! s . noRenewal ;
101+ s . costAliases = sanitizeCostAliases ( s . costAliases ) ;
102+ s . costGateway = [ "sub2api" , "sub2api-password" ] . includes ( s . type ) && ! ! s . costGateway ;
95103 if ( ! Array . isArray ( s . fixedPurchases ) ) {
96104 s . fixedPurchases = [ ] ;
97105 const amount = s . fixedCostCny ?? s . fixedMonthlyCny ;
@@ -280,6 +288,10 @@ export class Store {
280288 lowBalanceUsd : numOrNull ( input . lowBalanceUsd ) ,
281289 // 充值折算汇率:站点 $1 折合人民币(¥);null 按 1:1 展示
282290 cnyPerUsd : numOrNull ( input . cnyPerUsd ) ,
291+ // 自有站渠道可能使用容器域名、内网 IP 等地址;别名参与利润成本归属匹配。
292+ costAliases : sanitizeCostAliases ( input . costAliases ) ,
293+ // 成本汇总站:其 actual_cost 已覆盖后面的负载均衡上游,利润中只计这一层。
294+ costGateway : [ "sub2api" , "sub2api-password" ] . includes ( input . type ) && ! ! input . costGateway ,
283295 // 我自己的中转站:启用「我的站点」下游用量分析(需管理员令牌)
284296 isOwn : ! ! input . isOwn ,
285297 // 不再续费:余额低于阈值时仅提醒一次,不再发送耗尽/ETA/持续余额提醒
@@ -294,6 +306,9 @@ export class Store {
294306 alertState : null , // 告警去重状态(含不再续费站点的一次性低余额提醒时间)
295307 balance : null , // 最近一次查询结果
296308 } ;
309+ if ( station . costGateway ) {
310+ for ( const s of this . data . stations ) s . costGateway = false ;
311+ }
297312 this . data . stations . push ( station ) ;
298313 await this . save ( ) ;
299314 return station ;
@@ -311,6 +326,13 @@ export class Store {
311326 if ( "password" in patch ) s . password = String ( patch . password ?? "" ) ;
312327 if ( "lowBalanceUsd" in patch ) s . lowBalanceUsd = numOrNull ( patch . lowBalanceUsd ) ;
313328 if ( "cnyPerUsd" in patch ) s . cnyPerUsd = numOrNull ( patch . cnyPerUsd ) ;
329+ if ( "costAliases" in patch ) s . costAliases = sanitizeCostAliases ( patch . costAliases ) ;
330+ if ( "costGateway" in patch || ! [ "sub2api" , "sub2api-password" ] . includes ( s . type ) ) {
331+ s . costGateway = [ "sub2api" , "sub2api-password" ] . includes ( s . type ) && ! ! patch . costGateway ;
332+ if ( s . costGateway ) {
333+ for ( const other of this . data . stations ) if ( other . id !== s . id ) other . costGateway = false ;
334+ }
335+ }
314336 if ( "isOwn" in patch ) s . isOwn = ! ! patch . isOwn ;
315337 if ( "noRenewal" in patch || s . type === "fixed" ) {
316338 const noRenewal = s . type !== "fixed" && ! ! patch . noRenewal ;
0 commit comments