@@ -5,7 +5,7 @@ import { readFile } from "node:fs/promises";
55
66import { Store } from "./lib/store.js" ;
77import {
8- queryStation , queryStationUsage , queryOwnData , queryOwnChannels ,
8+ queryStation , queryStationUsage , queryOwnData , queryOwnChannels , queryOwnUsers ,
99 dateStrInTz , parseDateLabel , dailyFixedCny , STATION_TYPES ,
1010} from "./lib/providers.js" ;
1111import { forecastDaily } from "./lib/forecast.js" ;
@@ -182,6 +182,19 @@ mock.get("/newapi/:acc/api/data/users", (req, res) => {
182182 if ( ! needAuth ( req , res ) ) return ;
183183 res . json ( { success : true , message : "" , data : mockOwnRows ( Number ( req . query . start_timestamp ) || 0 , Number ( req . query . end_timestamp ) || 0 , "user" ) } ) ;
184184} ) ;
185+ mock . get ( "/newapi/:acc/api/user/" , ( req , res ) => {
186+ if ( ! needAuth ( req , res ) ) return ;
187+ res . json ( {
188+ success : true , message : "" ,
189+ data : { items : [
190+ { id : 1 , username : "root" , display_name : "Root" , role : 100 , status : 1 , quota : 99500000 , used_quota : 4200000 } ,
191+ { id : 2 , username : "alice" , display_name : "" , role : 1 , status : 1 , quota : 5250000 , used_quota : 61500000 } ,
192+ { id : 3 , username : "bob" , display_name : "" , role : 1 , status : 1 , quota : 1200000 , used_quota : 38200000 } ,
193+ { id : 4 , username : "carol" , display_name : "" , role : 1 , status : 1 , quota : 0 , used_quota : 17800000 } ,
194+ { id : 5 , username : "dave" , display_name : "" , role : 10 , status : 1 , quota : 800000 , used_quota : 9200000 } ,
195+ ] , total : 5 } ,
196+ } ) ;
197+ } ) ;
185198mock . get ( "/newapi/:acc/api/channel/" , ( req , res ) => {
186199 if ( ! needAuth ( req , res ) ) return ;
187200 const local = `http://${ HOST } :${ PORT } /mock` ;
@@ -534,16 +547,33 @@ app.get("/api/own/analytics", async (req, res) => {
534547 daily . push ( { t, cost : Math . round ( ( dmap . get ( t ) || 0 ) * 10000 ) / 10000 } ) ;
535548 }
536549 }
537- const byUser = aggBy ( userRows , "user" ) ;
550+ // 用户列表:识别管理员(role >= 10)并取各用户余额
551+ let ownUsers = null ;
552+ try {
553+ ownUsers = await getOwnUsers ( own ) ;
554+ } catch { /* 拿不到就退化为不区分管理员 */ }
555+ const adminSet = new Set ( ( ownUsers || [ ] ) . filter ( ( u ) => u . role >= 10 ) . map ( ( u ) => u . username ) ) ;
556+
557+ const byUser = aggBy ( userRows , "user" ) . map ( ( u ) => ( { ...u , isAdmin : adminSet . has ( u . user ) } ) ) ;
558+ // 收入 = 普通用户的期内消费;管理员/root 自己用不产生收入,但上游成本照付
559+ const incomeUsd = byUser . filter ( ( u ) => ! u . isAdmin ) . reduce ( ( a , u ) => a + u . cost , 0 ) ;
560+ const adminUsageUsd = byUser . filter ( ( u ) => u . isAdmin ) . reduce ( ( a , u ) => a + u . cost , 0 ) ;
561+
538562 const payload = {
539563 range, tz, startMs, endMs : now ,
540564 station : { id : own . id , name : own . name , cnyPerUsd : own . cnyPerUsd ?? null } ,
541565 byModel : aggBy ( winModel , "model" ) ,
542566 byUser,
567+ userBalances : ownUsers
568+ ? ownUsers
569+ . filter ( ( u ) => u . role < 10 )
570+ . map ( ( u ) => ( { user : u . username , balanceUsd : Math . round ( u . quotaUsd * 10000 ) / 10000 , usedUsd : Math . round ( u . usedUsd * 100 ) / 100 , status : u . status } ) )
571+ . sort ( ( a , b ) => b . balanceUsd - a . balanceUsd )
572+ : null ,
543573 trend : [ ...tmap . values ( ) ] . sort ( ( a , b ) => a . t - b . t ) ,
544574 daily : daily . slice ( - 14 ) ,
545575 forecast : forecastDaily ( daily , 7 ) ,
546- profit : await computeProfit ( own , payloadIncome ( winModel ) , { startMs, now, tz, range } ) ,
576+ profit : await computeProfit ( own , incomeUsd , adminUsageUsd , { startMs, now, tz, range } ) ,
547577 generatedAt : new Date ( ) . toISOString ( ) ,
548578 } ;
549579 ownCache . set ( cacheKey , { at : Date . now ( ) , payload } ) ;
@@ -553,8 +583,13 @@ app.get("/api/own/analytics", async (req, res) => {
553583 }
554584} ) ;
555585
556- function payloadIncome ( winModelRows ) {
557- return winModelRows . reduce ( ( a , r ) => a + r . cost , 0 ) ;
586+ // 用户列表拉取开销不小,缓存 10 分钟
587+ let ownUsersCache = { at : 0 , stationId : null , list : null } ;
588+ async function getOwnUsers ( own ) {
589+ if ( ! ownUsersCache . list || ownUsersCache . stationId !== own . id || Date . now ( ) - ownUsersCache . at > 600000 ) {
590+ ownUsersCache = { at : Date . now ( ) , stationId : own . id , list : await queryOwnUsers ( own ) } ;
591+ }
592+ return ownUsersCache . list ;
558593}
559594
560595// 渠道列表拉取开销不小,缓存 10 分钟
@@ -566,7 +601,7 @@ let ownChannelsCache = { at: 0, stationId: null, list: null };
566601 * 否则按上游用量接口的实际扣费 × 充值汇率;用量接口不可用时退回余额下降推算。
567602 * 渠道按 base_url 与监控站点匹配(忽略协议/末尾斜杠/是否带 /api)。
568603 */
569- async function computeProfit ( own , incomeUsd , { startMs, now, tz, range } ) {
604+ async function computeProfit ( own , incomeUsd , adminUsageUsd , { startMs, now, tz, range } ) {
570605 const r2 = ( v ) => Math . round ( v * 100 ) / 100 ;
571606 try {
572607 if ( ! ownChannelsCache . list || ownChannelsCache . stationId !== own . id || Date . now ( ) - ownChannelsCache . at > 600000 ) {
@@ -646,6 +681,7 @@ async function computeProfit(own, incomeUsd, { startMs, now, tz, range }) {
646681 const totalCostCny = r2 ( costs . reduce ( ( a , c ) => a + c . cny , 0 ) ) ;
647682 return {
648683 incomeCny, totalCostCny,
684+ adminUsageCny : r2 ( ( adminUsageUsd || 0 ) * ownRate ) ,
649685 profitCny : r2 ( incomeCny - totalCostCny ) ,
650686 marginPct : incomeCny > 0 ? Math . round ( ( ( incomeCny - totalCostCny ) / incomeCny ) * 1000 ) / 10 : null ,
651687 costs : costs . sort ( ( a , b ) => b . cny - a . cny ) ,
0 commit comments