@@ -47,18 +47,7 @@ export function matchCostStation(upstreams, channelUrl) {
4747 return station ;
4848}
4949
50- // 单一成本汇总站代表完整的变量成本池:所有 New API 渠道归到它,后层上游不重复计费。
5150export function mapCostChannels ( upstreams , channels ) {
52- const gateways = upstreams . filter ( ( s ) => ! ! s . costGateway && [ "sub2api" , "sub2api-password" ] . includes ( s . type ) ) ;
53- if ( gateways . length === 1 ) {
54- const gateway = gateways [ 0 ] ;
55- return {
56- gateway,
57- matched : new Map ( [ [ gateway . id , { station : gateway , channels : channels . map ( ( ch ) => ch . name ) } ] ] ) ,
58- unmatched : new Map ( ) ,
59- } ;
60- }
61-
6251 const matched = new Map ( ) ;
6352 const unmatched = new Map ( ) ;
6453 for ( const ch of channels ) {
@@ -77,7 +66,16 @@ export function mapCostChannels(upstreams, channels) {
7766 unmatched . set ( label , entry ) ;
7867 }
7968 }
80- return { gateway : null , matched, unmatched } ;
69+ return { matched, unmatched } ;
70+ }
71+
72+ // 成本纳入与 New API 渠道匹配解耦:列表中的监控上游默认全部计入。
73+ export function selectCostUpstreams ( stations , ownId ) {
74+ const candidates = stations . filter ( ( s ) => s . id !== ownId ) ;
75+ return {
76+ included : candidates . filter ( ( s ) => s . includeInProfit !== false ) ,
77+ excluded : candidates . filter ( ( s ) => s . includeInProfit === false ) ,
78+ } ;
8179}
8280
8381// 部分 New API 部署会对 /api/data/self 返回成功空数组。余额明确下降时不能按零成本处理。
@@ -120,10 +118,9 @@ export async function computeResold(own, incomeUsd, adminUsageUsd, startMs, endM
120118}
121119
122120/**
123- * 利润 = 收入(下游消费 × 自有站售价汇率)− 成本(各匹配上游的期内成本)
124- * 成本口径:配置了每月固定成本的上游按天摊销(月费 ÷ 30 × 窗口天数);
125- * 否则按上游用量接口的实际扣费 × 充值汇率;用量接口不可用时退回余额下降推算。
126- * 渠道按 base_url 与监控站点匹配(忽略协议/末尾斜杠/是否带 /api)。
121+ * 利润 = 收入(下游消费 × 自有站售价汇率)− 全部纳入成本的监控上游期内成本。
122+ * 成本纳入不依赖 New API 渠道列表:固定付费按天摊销;其余按上游实际扣费 × 充值汇率,
123+ * 用量接口返回 0 或不可用时退回余额下降推算。渠道 URL 匹配仅用于展示流量归属。
127124 */
128125export async function computeProfit ( rt , own , incomeUsd , adminUsageUsd , { startMs, now, tz, range, resold } ) {
129126 const { store, history } = rt ;
@@ -135,9 +132,8 @@ export async function computeProfit(rt, own, incomeUsd, adminUsageUsd, { startMs
135132 rt . _ownChannelsCache = { at : Date . now ( ) , stationId : own . id , list : await queryOwnChannels ( own ) } ;
136133 }
137134 const channels = rt . _ownChannelsCache . list ;
138- const upstreams = store . list ( ) . filter ( ( s ) => s . id !== own . id ) ;
139-
140- const { gateway, matched, unmatched } = mapCostChannels ( upstreams , channels ) ;
135+ const { included : upstreams , excluded : excludedUpstreams } = selectCostUpstreams ( store . list ( ) , own . id ) ;
136+ const { matched, unmatched } = mapCostChannels ( upstreams , channels ) ;
141137
142138 const windowDays = ( now - startMs ) / 86400000 ;
143139 const rateOf = ( s ) => ( s . cnyPerUsd != null && s . cnyPerUsd > 0 ? s . cnyPerUsd : 1 ) ;
@@ -172,15 +168,17 @@ export async function computeProfit(rt, own, incomeUsd, adminUsageUsd, { startMs
172168 }
173169 costs . push ( {
174170 stationId : s . id , name : s . name , mode : "fixed" , note,
175- channels : matched . get ( s . id ) ?. channels || [ "未匹配渠道 · 通用成本 " ] ,
171+ channels : matched . get ( s . id ) ?. channels || [ "未直接出现在 New API 渠道 · 按固定成本计费 " ] ,
176172 cny : r2 ( cnySum ) ,
177173 } ) ;
178174 }
179- // 按用量:匹配到渠道且未配置固定成本的上游
175+ // 按用量:所有纳入成本、且未配置固定成本的监控上游。
176+ // 是否出现在 New API 渠道列表只影响归属说明,不影响成本纳入。
180177 const usageCosts = await Promise . all (
181- [ ...matched . values ( ) ]
182- . filter ( ( { station } ) => fixedPurchases ( station ) . length === 0 && station . type !== "fixed" )
183- . map ( async ( { station, channels : chNames } ) => {
178+ upstreams
179+ . filter ( ( station ) => fixedPurchases ( station ) . length === 0 && station . type !== "fixed" )
180+ . map ( async ( station ) => {
181+ const chNames = matched . get ( station . id ) ?. channels || [ "未直接出现在 New API 渠道 · 按监控数据计费" ] ;
184182 const item = { stationId : station . id , name : station . name , channels : chNames } ;
185183 try {
186184 const u = await queryStationUsage ( station , {
@@ -215,10 +213,10 @@ export async function computeProfit(rt, own, incomeUsd, adminUsageUsd, { startMs
215213 . filter ( ( s , i , all ) => ( s . cnyPerUsd == null || s . cnyPerUsd <= 0 ) && all . findIndex ( ( x ) => x . id === s . id ) === i )
216214 . map ( ( s ) => s . name ) ;
217215 const warnings = [ ] ;
218- if ( unmatchedEnabled ) warnings . push ( `${ unmatchedEnabled } 个启用渠道尚未匹配监控站,其成本未计入` ) ;
219- if ( gateway ) warnings . push ( `用量成本由 ${ gateway . name } 汇总,其负载均衡后的上游不重复计入` ) ;
216+ if ( unmatchedEnabled ) warnings . push ( `${ unmatchedEnabled } 个启用渠道未直接关联监控站;全部监控上游仍已独立计入成本` ) ;
220217 if ( estimatedCosts ) warnings . push ( `${ estimatedCosts } 个上游使用余额历史推算成本` ) ;
221218 if ( defaultRateStations . length ) warnings . push ( `${ defaultRateStations . join ( "、" ) } 未配置汇率,当前按 1:1 折算` ) ;
219+ if ( excludedUpstreams . length ) warnings . push ( `${ excludedUpstreams . map ( ( s ) => s . name ) . join ( "、" ) } 已设置为不计入利润成本` ) ;
222220 return {
223221 incomeCny, totalCostCny,
224222 adminUsageCny : r2 ( ( adminUsageUsd || 0 ) * ownRate ) ,
@@ -229,9 +227,9 @@ export async function computeProfit(rt, own, incomeUsd, adminUsageUsd, { startMs
229227 marginPct : incomeCny > 0 ? Math . round ( ( ( incomeCny - totalCostCny ) / incomeCny ) * 1000 ) / 10 : null ,
230228 costs : costs . sort ( ( a , b ) => b . cny - a . cny ) ,
231229 unmatched : unmatchedList ,
232- complete : unmatchedEnabled === 0 && defaultRateStations . length === 0 ,
230+ complete : defaultRateStations . length === 0 ,
233231 estimated : estimatedCosts > 0 ,
234- costGateway : gateway ? { stationId : gateway . id , name : gateway . name } : null ,
232+ excludedStationIds : excludedUpstreams . map ( ( s ) => s . id ) ,
235233 warnings,
236234 windowDays : Math . round ( windowDays * 10 ) / 10 ,
237235 } ;
0 commit comments