2026-06-02 13:14:10 +08:00
const express = require ( "express" ) ;
const bcrypt = require ( "bcryptjs" ) ;
const {
requireAuth ,
requireAdmin ,
requireEnterpriseAdmin ,
requireManagementAccess ,
login ,
generateToken ,
startUserSession ,
getUserContextById ,
isSystemAdmin ,
generateUniqueEnterpriseCode ,
} = require ( "../auth" ) ;
const keyManager = require ( "../keyManager" ) ;
const {
calculateCost ,
calculateCostMills ,
listModelPrices ,
normalizeModelPriceRow ,
getAverageCostCents ,
loadPriceCache ,
} = require ( "../pricing" ) ;
const {
deductForApiCall ,
deductImageGenerationCredits ,
creditBalance ,
creditUserBalance ,
activatePackage ,
distributeCredits ,
getEnterpriseFinancials ,
getUserEnterpriseId ,
getEnterpriseName ,
preauthorizeCall ,
} = require ( "../billing" ) ;
const wechatPay = require ( "../paymentWechat" ) ;
const alipay = require ( "../paymentAlipay" ) ;
const crypto = require ( "node:crypto" ) ;
const { pool , withTransaction } = require ( "../db" ) ;
const {
computeNextRevision ,
normalizeRevisionValue ,
shouldRejectStaleRevision ,
} = require ( "../projectRevisionLogic" ) ;
const { loadBetaInviteCodes } = require ( "../betaInviteCodes" ) ;
const USERNAME _PATTERN = /^[a-zA-Z0-9_\u4e00-\u9fa5]+$/ ;
const PRICE _CATEGORIES = new Set ( [ "text" , "image" , "video" ] ) ;
const PRICE _TYPES = new Set ( [ "token" , "flat" ] ) ;
const PHONE _PATTERN = /^\+?[0-9]{6,20}$/ ;
const EMAIL _PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ ;
const SMS _PURPOSES = new Set ( [ "register" , "login" ] ) ;
const SMS _CODE _TTL _MINUTES = Math . max ( 1 , Number ( process . env . SMS _CODE _TTL _MINUTES ) || 10 ) ;
const SMS _CODE _COOLDOWN _SECONDS = Math . max ( 10 , Number ( process . env . SMS _CODE _COOLDOWN _SECONDS ) || 60 ) ;
const SMS _CODE _MAX _ATTEMPTS = Math . max ( 1 , Number ( process . env . SMS _CODE _MAX _ATTEMPTS ) || 5 ) ;
2026-06-04 18:58:45 +08:00
const EMAIL _PURPOSES = new Set ( [ "register" , "login" , "reset" ] ) ;
const EMAIL _CODE _TTL _MINUTES = Math . max ( 1 , Number ( process . env . EMAIL _CODE _TTL _MINUTES ) || 10 ) ;
const EMAIL _CODE _COOLDOWN _SECONDS = Math . max ( 10 , Number ( process . env . EMAIL _CODE _COOLDOWN _SECONDS ) || 60 ) ;
const EMAIL _CODE _MAX _ATTEMPTS = Math . max ( 1 , Number ( process . env . EMAIL _CODE _MAX _ATTEMPTS ) || 5 ) ;
2026-06-02 13:14:10 +08:00
function validateUsername ( username ) {
if ( ! username ) return "缺少用户名" ;
if ( username . length < 2 || username . length > 30 ) return "用户名长度必须在 2 到 30 之间" ;
if ( ! USERNAME _PATTERN . test ( username ) ) return "用户名只能包含字母、数字、下划线或中文" ;
return null ;
}
function validatePassword ( password ) {
if ( ! password ) return "缺少密码" ;
if ( password . length < 6 ) return "密码至少 6 位" ;
return null ;
}
function normalizePhone ( phone ) {
return String ( phone || "" )
. trim ( )
. replace ( /[\s-]/g , "" ) ;
}
function validatePhone ( phone ) {
const normalized = normalizePhone ( phone ) ;
if ( ! normalized ) return "缺少手机号" ;
if ( ! PHONE _PATTERN . test ( normalized ) ) return "手机号格式不正确" ;
return null ;
}
function normalizeEmail ( email ) {
return String ( email || "" ) . trim ( ) . toLowerCase ( ) ;
}
function validateEmail ( email ) {
const normalized = normalizeEmail ( email ) ;
if ( ! normalized ) return "缺少邮箱" ;
if ( normalized . length > 200 || ! EMAIL _PATTERN . test ( normalized ) ) return "邮箱格式不正确" ;
return null ;
}
function hashSmsCode ( phone , code ) {
const secret = process . env . SMS _CODE _SECRET || process . env . JWT _SECRET || "omniai-dev-sms-secret" ;
return crypto . createHash ( "sha256" ) . update ( ` ${ phone } : ${ code } : ${ secret } ` ) . digest ( "hex" ) ;
}
function generateSmsCode ( ) {
return String ( Math . floor ( 100000 + Math . random ( ) * 900000 ) ) ;
}
async function sendSmsCode ( phone , code , purpose ) {
const provider = String ( process . env . SMS _PROVIDER || "mock" )
. trim ( )
. toLowerCase ( ) ;
if ( provider === "http" ) {
const endpoint = process . env . SMS _HTTP _ENDPOINT ;
if ( ! endpoint ) throw new Error ( "SMS_HTTP_ENDPOINT 未配置" ) ;
const response = await fetch ( endpoint , {
method : "POST" ,
headers : {
"Content-Type" : "application/json" ,
... ( process . env . SMS _HTTP _TOKEN
? { Authorization : ` Bearer ${ process . env . SMS _HTTP _TOKEN } ` }
: { } ) ,
} ,
body : JSON . stringify ( { phone , code , purpose } ) ,
} ) ;
if ( ! response . ok ) {
throw new Error ( ` 短信平台返回 HTTP ${ response . status } ` ) ;
}
return { provider } ;
}
console . log ( ` [sms: ${ purpose } ] ${ phone } verification sent (mock provider) ` ) ;
return {
provider : "mock" ,
devCode : process . env . SMS _DEV _RETURN _CODE === "1" ? code : undefined ,
} ;
}
async function createLoginResultForUserId ( userId , req ) {
const user = await getUserContextById ( userId ) ;
if ( ! user ? . enabled ) return null ;
const userAgent = req ? . headers ? . [ "user-agent" ] || null ;
const sessionId = await startUserSession ( user . id , userAgent ) ;
const userWithSession = {
... user ,
sessionId ,
sessionStartedAt : new Date ( ) . toISOString ( ) ,
} ;
return {
token : generateToken ( userWithSession , sessionId ) ,
user : userWithSession ,
} ;
}
function sanitizeUsernameSeed ( seed , fallback ) {
const normalized = String ( seed || "" )
. trim ( )
. replace ( /[^\w\u4e00-\u9fa5]/g , "_" )
. replace ( /_+/g , "_" )
. replace ( /^_+|_+$/g , "" ) ;
const safe = normalized || fallback ;
return safe . length > 24 ? safe . slice ( 0 , 24 ) : safe ;
}
async function generateUniqueUsername ( seed , fallback ) {
const base = sanitizeUsernameSeed ( seed , fallback ) ;
for ( let attempt = 0 ; attempt < 10 ; attempt ++ ) {
const suffix = crypto . randomBytes ( 3 ) . toString ( "hex" ) ;
const username = ` ${ base } _ ${ suffix } ` . slice ( 0 , 30 ) ;
const { rows } = await pool . query ( "SELECT 1 FROM users WHERE username = $1" , [ username ] ) ;
if ( rows . length === 0 ) return username ;
}
return ` ${ fallback } _ ${ Date . now ( ) . toString ( 36 ) } ` . slice ( 0 , 30 ) ;
}
async function consumeSmsCode ( phone , code , purpose ) {
const { rows } = await pool . query (
`
SELECT id, code_hash, attempts
FROM sms_verification_codes
WHERE phone = $ 1
AND purpose = $ 2
AND consumed_at IS NULL
AND expires_at > NOW()
ORDER BY created_at DESC
LIMIT 1
` ,
[ phone , purpose ] ,
) ;
const row = rows [ 0 ] ;
if ( ! row ) return false ;
if ( Number ( row . attempts || 0 ) >= SMS _CODE _MAX _ATTEMPTS ) {
return false ;
}
const expectedHash = hashSmsCode ( phone , String ( code || "" ) . trim ( ) ) ;
if ( row . code _hash !== expectedHash ) {
await pool . query ( "UPDATE sms_verification_codes SET attempts = attempts + 1 WHERE id = $1" , [
row . id ,
] ) ;
return false ;
}
await pool . query ( "UPDATE sms_verification_codes SET consumed_at = NOW() WHERE id = $1" , [ row . id ] ) ;
return true ;
}
2026-06-04 18:58:45 +08:00
function hashEmailCode ( email , code ) {
const secret = process . env . EMAIL _CODE _SECRET || process . env . JWT _SECRET || "omniai-dev-email-secret" ;
return crypto . createHash ( "sha256" ) . update ( email + ":" + code + ":" + secret ) . digest ( "hex" ) ;
}
async function sendEmailCode ( email , code , purpose ) {
const provider = String ( process . env . EMAIL _PROVIDER || "mock" ) . trim ( ) . toLowerCase ( ) ;
if ( provider === "smtp" ) {
const nodemailer = require ( "nodemailer" ) ;
const transporter = nodemailer . createTransport ( {
host : process . env . SMTP _HOST ,
port : Number ( process . env . SMTP _PORT ) || 587 ,
secure : process . env . SMTP _SECURE === "1" ,
auth : {
user : process . env . SMTP _USER ,
pass : process . env . SMTP _PASS ,
} ,
} ) ;
const purposeText = purpose === "register" ? "注册" : purpose === "login" ? "登录" : "重置密码" ;
await transporter . sendMail ( {
from : process . env . SMTP _FROM || process . env . SMTP _USER ,
to : email ,
subject : "[OmniAI] \u90ae\u7bb1\u9a8c\u8bc1\u7801" ,
text : "\u60a8\u7684\u9a8c\u8bc1\u7801\u662f\uff1a" + code + "\n\u7528\u9014\uff1a" + purposeText + "\n\u6709\u6548\u671f\uff1a" + String ( process . env . EMAIL _CODE _TTL _MINUTES || 10 ) + " \u5206\u949f\n\u5982\u679c\u4e0d\u662f\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u6b64\u90ae\u4ef6\u3002" ,
html : "<div style=\"font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px\"><h2 style=\"color:#333\">OmniAI \u90ae\u7bb1\u9a8c\u8bc1</h2><p style=\"font-size:16px;color:#555\">\u60a8\u7684\u9a8c\u8bc1\u7801\u662f\uff1a</p><p style=\"font-size:32px;font-weight:bold;letter-spacing:6px;color:#1677ff;margin:16px 0\">" + code + "</p><p style=\"color:#888\">\u7528\u9014\uff1a" + purposeText + "</p><p style=\"color:#888\">\u6709\u6548\u671f\uff1a" + String ( process . env . EMAIL _CODE _TTL _MINUTES || 10 ) + " \u5206\u949f</p><hr style=\"border:none;border-top:1px solid #eee;margin:24px 0\"><p style=\"color:#aaa;font-size:13px\">\u5982\u679c\u4e0d\u662f\u60a8\u672c\u4eba\u64cd\u4f5c\uff0c\u8bf7\u5ffd\u7565\u6b64\u90ae\u4ef6\u3002</p></div>" ,
} ) ;
return { provider : "smtp" } ;
}
console . log ( "[email:" + purpose + "] " + email + " verification code: " + code + " (mock provider)" ) ;
return {
provider : "mock" ,
devCode : process . env . EMAIL _DEV _RETURN _CODE === "1" ? code : undefined ,
} ;
}
async function consumeEmailCode ( email , code , purpose ) {
const { rows } = await pool . query (
"SELECT id, code_hash, attempts FROM email_verification_codes WHERE email = $1 AND purpose = $2 AND consumed_at IS NULL AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1" ,
[ email , purpose ]
) ;
const row = rows [ 0 ] ;
if ( ! row ) return false ;
if ( Number ( row . attempts || 0 ) >= EMAIL _CODE _MAX _ATTEMPTS ) {
return false ;
}
const expectedHash = hashEmailCode ( email , String ( code || "" ) . trim ( ) ) ;
if ( row . code _hash !== expectedHash ) {
await pool . query ( "UPDATE email_verification_codes SET attempts = attempts + 1 WHERE id = $1" , [ row . id ] ) ;
return false ;
}
await pool . query ( "UPDATE email_verification_codes SET consumed_at = NOW() WHERE id = $1" , [ row . id ] ) ;
return true ;
}
2026-06-02 13:14:10 +08:00
function getWechatLoginConfig ( ) {
const appId = process . env . WECHAT _LOGIN _APP _ID || process . env . WECHAT _APP _ID || "" ;
const appSecret = process . env . WECHAT _LOGIN _APP _SECRET || process . env . WECHAT _APP _SECRET || "" ;
const redirectUri = process . env . WECHAT _LOGIN _REDIRECT _URI || "" ;
return { appId , appSecret , redirectUri } ;
}
async function fetchWechatJson ( url ) {
const response = await fetch ( url ) ;
const payload = await response . json ( ) ;
if ( ! response . ok || payload . errcode ) {
throw new Error ( payload . errmsg || ` 微信接口返回 HTTP ${ response . status } ` ) ;
}
return payload ;
}
async function exchangeWechatCode ( code ) {
const { appId , appSecret } = getWechatLoginConfig ( ) ;
if ( ! appId || ! appSecret ) {
throw new Error ( "微信开放平台 AppID/AppSecret 未配置" ) ;
}
const tokenUrl = new URL ( "https://api.weixin.qq.com/sns/oauth2/access_token" ) ;
tokenUrl . searchParams . set ( "appid" , appId ) ;
tokenUrl . searchParams . set ( "secret" , appSecret ) ;
tokenUrl . searchParams . set ( "code" , code ) ;
tokenUrl . searchParams . set ( "grant_type" , "authorization_code" ) ;
const tokenPayload = await fetchWechatJson ( tokenUrl . toString ( ) ) ;
const accessToken = tokenPayload . access _token ;
const openid = tokenPayload . openid ;
if ( ! accessToken || ! openid ) {
throw new Error ( "微信登录未返回 openid" ) ;
}
let profile = { } ;
try {
const userInfoUrl = new URL ( "https://api.weixin.qq.com/sns/userinfo" ) ;
userInfoUrl . searchParams . set ( "access_token" , accessToken ) ;
userInfoUrl . searchParams . set ( "openid" , openid ) ;
userInfoUrl . searchParams . set ( "lang" , "zh_CN" ) ;
profile = await fetchWechatJson ( userInfoUrl . toString ( ) ) ;
} catch ( error ) {
console . warn (
"[auth/wechat] userinfo failed" ,
error instanceof Error ? error . message : String ( error ) ,
) ;
}
return {
openid ,
unionid : profile . unionid || tokenPayload . unionid || null ,
nickname : profile . nickname || null ,
} ;
}
async function findOrCreateWechatUser ( wechatUser ) {
const { rows : existingRows } = await pool . query (
"SELECT id, enabled FROM users WHERE wechat_openid = $1 LIMIT 1" ,
[ wechatUser . openid ] ,
) ;
if ( existingRows . length > 0 ) {
if ( ! existingRows [ 0 ] . enabled ) {
const error = new Error ( "账号已禁用" ) ;
error . status = 403 ;
throw error ;
}
return existingRows [ 0 ] . id ;
}
if ( loadBetaInviteCodes ( ) . size > 0 ) {
const error = new Error ( "内测阶段请先使用内测码注册账号后再使用微信登录" ) ;
error . status = 403 ;
throw error ;
}
const username = await generateUniqueUsername (
wechatUser . nickname || ` wx ${ wechatUser . openid . slice ( - 6 ) } ` ,
"wechat" ,
) ;
const randomPasswordHash = await bcrypt . hash ( crypto . randomBytes ( 32 ) . toString ( "hex" ) , 10 ) ;
const { rows } = await pool . query (
`
INSERT INTO users (username, password_hash, wechat_openid, wechat_unionid, auth_provider, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents)
VALUES ( $ 1, $ 2, $ 3, $ 4, 'wechat', 'user', 30, null, 0, 0)
RETURNING id
` ,
[ username , randomPasswordHash , wechatUser . openid , wechatUser . unionid ] ,
) ;
return rows [ 0 ] . id ;
}
function validateEnterpriseName ( name ) {
if ( ! name ) return "缺少企业名称" ;
if ( name . trim ( ) . length < 2 || name . trim ( ) . length > 80 ) return "企业名称长度必须在 2 到 80 之间" ;
return null ;
}
function parseNumericValue ( value , fieldLabel , { allowNull = true } = { } ) {
if ( value === undefined ) return { ok : true , value : undefined } ;
if ( value === null || value === "" ) {
return allowNull ? { ok : true , value : null } : { ok : false , error : ` ${ fieldLabel } 不能为空 ` } ;
}
const numeric = Number ( value ) ;
if ( ! Number . isFinite ( numeric ) || numeric < 0 )
return { ok : false , error : ` ${ fieldLabel } 必须是非负数字 ` } ;
return { ok : true , value : numeric } ;
}
async function ensureEnterpriseExists ( enterpriseId ) {
if ( enterpriseId == null ) return null ;
const { rows } = await pool . query ( "SELECT id, name FROM enterprises WHERE id = $1" , [
enterpriseId ,
] ) ;
return rows [ 0 ] || null ;
}
function formatUserRow ( row ) {
return {
id : Number ( row . id ) ,
username : row . username ,
role : row . role ,
avatarUrl : row . avatar _url || null ,
maxConcurrency : Number ( row . max _concurrency || 0 ) ,
enabled : ! ! row . enabled ,
enterpriseId : row . enterprise _id == null ? null : Number ( row . enterprise _id ) ,
enterpriseName : row . enterprise _name || null ,
isEnterpriseAdmin : ! ! row . is _enterprise _admin ,
balanceCents : row . balance _cents != null ? Number ( row . balance _cents ) : 0 ,
billingMode : row . billing _mode || "credits" ,
betaExpiresAt : row . beta _expires _at || null ,
createdAt : row . created _at ,
} ;
}
function normalizeOssRegion ( region ) {
const trimmed = String ( region || "" ) . trim ( ) ;
return trimmed . startsWith ( "oss-" ) ? trimmed . slice ( 4 ) : trimmed ;
}
function buildOssPublicUrl ( ossKey ) {
const publicBaseUrl = String ( process . env . OSS _PUBLIC _BASE _URL || "" )
. trim ( )
. replace ( /\/+$/ , "" ) ;
if ( publicBaseUrl ) {
return ` ${ publicBaseUrl } / ${ ossKey } ` ;
}
const bucket = String ( process . env . OSS _BUCKET || "" ) . trim ( ) ;
const region = normalizeOssRegion ( process . env . OSS _REGION || "" ) ;
if ( ! bucket || ! region ) {
throw new Error ( "OSS bucket or region is not configured" ) ;
}
return ` https:// ${ bucket } .oss- ${ region } .aliyuncs.com/ ${ ossKey } ` ;
}
function normalizeAvatarOssKey ( value , userId ) {
if ( value === undefined ) return { value : undefined } ;
if ( value === null ) return { value : null } ;
const safeUserId = String ( userId ) . replace ( /[^a-zA-Z0-9_-]/g , "" ) ;
const ossKey = String ( value || "" )
. trim ( )
. replace ( /^\/+/ , "" ) ;
if ( ! ossKey ) return { value : null } ;
const expectedPrefix = ` users/ ${ safeUserId } /profile/avatar/ ` ;
const allowedPattern = new RegExp (
` ^users/ ${ safeUserId } /profile/avatar/avatar \\ .(jpg|jpeg|png|webp) $ ` ,
"i" ,
) ;
if ( ! ossKey . startsWith ( expectedPrefix ) || ! allowedPattern . test ( ossKey ) ) {
return { error : "Invalid avatar OSS key" } ;
}
return { value : ossKey } ;
}
function normalizeProfileMediaUrl ( value ) {
if ( value === undefined ) return { value : undefined } ;
if ( value === null || value === "" ) return { value : null } ;
const url = String ( value || "" ) . trim ( ) ;
if ( ! url ) return { value : null } ;
if ( url . length > 2000 ) return { error : "资料图片地址过长" } ;
if ( url . startsWith ( "data:" ) ) return { error : "资料图片请先上传到 OSS" } ;
try {
const parsed = new URL ( url ) ;
if ( parsed . protocol !== "https:" && parsed . protocol !== "http:" ) {
return { error : "资料图片地址格式不正确" } ;
}
} catch {
return { error : "资料图片地址格式不正确" } ;
}
return { value : url } ;
}
function normalizeProjectOssKey ( value , userId , projectId ) {
const safeUserId = String ( userId ) . replace ( /[^a-zA-Z0-9_-]/g , "" ) ;
const safeProjectId = String ( projectId || "" )
. trim ( )
. replace ( /[^a-zA-Z0-9_-]/g , "" ) ;
const ossKey = String ( value || "" )
. trim ( )
. replace ( /^\/+/ , "" ) ;
if ( ! safeUserId || ! safeProjectId || safeProjectId !== String ( projectId || "" ) . trim ( ) ) {
return { error : "Invalid project OSS key scope" } ;
}
const expectedKey = ` users/ ${ safeUserId } /projects/ ${ safeProjectId } /current/project.json ` ;
if ( ossKey !== expectedKey ) {
return { error : "Invalid project OSS key scope" } ;
}
return { value : ossKey } ;
}
function getManagementEnterpriseId ( user ) {
if ( ! user || isSystemAdmin ( user ) ) return null ;
return user . enterpriseId || null ;
}
function appendEnterpriseScope ( whereClauses , params , user , expression , paramIdx ) {
const enterpriseId = getManagementEnterpriseId ( user ) ;
if ( enterpriseId != null ) {
whereClauses . push ( ` ${ expression } = $ ${ paramIdx } ` ) ;
params . push ( enterpriseId ) ;
return paramIdx + 1 ;
}
return paramIdx ;
}
function readModelPricePayload ( body , existing = null ) {
const modelKey = String ( body . modelKey ? ? existing ? . modelKey ? ? "" ) . trim ( ) ;
const displayName = String ( body . displayName ? ? existing ? . displayName ? ? "" ) . trim ( ) ;
const category = String ( body . category ? ? existing ? . category ? ? "text" ) . trim ( ) ;
const pricingType = String ( body . pricingType ? ? existing ? . pricingType ? ? "token" ) . trim ( ) ;
const currency = String ( body . currency ? ? existing ? . currency ? ? "CNY" ) . trim ( ) || "CNY" ;
const enabled = body . enabled === undefined ? ( existing ? . enabled ? ? true ) : ! ! body . enabled ;
if ( ! modelKey ) return { error : "缺少模型标识" } ;
if ( ! displayName ) return { error : "缺少显示名称" } ;
if ( ! PRICE _CATEGORIES . has ( category ) ) return { error : "模型分类无效" } ;
if ( ! PRICE _TYPES . has ( pricingType ) ) return { error : "计费类型无效" } ;
const inputPriceMills = parseNumericValue ( body . inputPriceMills , "输入价格(厘)" ) ;
if ( ! inputPriceMills . ok ) return { error : inputPriceMills . error } ;
const outputPriceMills = parseNumericValue ( body . outputPriceMills , "输出价格(厘)" ) ;
if ( ! outputPriceMills . ok ) return { error : outputPriceMills . error } ;
const flatPriceMills = parseNumericValue ( body . flatPriceMills , "固定价格(厘)" ) ;
if ( ! flatPriceMills . ok ) return { error : flatPriceMills . error } ;
const merged = {
modelKey ,
displayName ,
category ,
pricingType ,
currency ,
enabled ,
inputPriceMills :
inputPriceMills . value !== undefined
? inputPriceMills . value
: ( existing ? . inputPriceMills ? ? null ) ,
outputPriceMills :
outputPriceMills . value !== undefined
? outputPriceMills . value
: ( existing ? . outputPriceMills ? ? null ) ,
flatPriceMills :
flatPriceMills . value !== undefined
? flatPriceMills . value
: ( existing ? . flatPriceMills ? ? null ) ,
} ;
if ( pricingType === "token" ) {
if ( merged . inputPriceMills == null || merged . outputPriceMills == null )
return { error : "按 Token 计费时必须提供输入和输出价格(厘)" } ;
merged . flatPriceMills = null ;
} else {
if ( merged . flatPriceMills == null ) return { error : "固定计费时必须提供固定价格(厘)" } ;
merged . inputPriceMills = null ;
merged . outputPriceMills = null ;
}
return { value : merged } ;
}
async function getModelPriceById ( id ) {
const { rows } = await pool . query ( "SELECT * FROM model_prices WHERE id = $1" , [ id ] ) ;
return normalizeModelPriceRow ( rows [ 0 ] ) ;
}
function getPeriodStart ( period ) {
switch ( period ) {
case "7d" :
return "NOW() - INTERVAL '7 days'" ;
case "30d" :
return "NOW() - INTERVAL '30 days'" ;
case "all" :
return null ;
default :
return "NOW() - INTERVAL '7 days'" ;
}
}
// Fills a SQL day-aggregation result into a continuous 7-day series ending
// today, padding missing days with zeros so the trend chart has no gaps.
function buildDailyTrend ( rows , days = 7 ) {
const byDay = new Map ( ) ;
for ( const row of rows || [ ] ) {
byDay . set ( String ( row . day ) , {
usedCents : Number ( row . used _cents || 0 ) ,
taskCount : Number ( row . task _count || 0 ) ,
} ) ;
}
const series = [ ] ;
const today = new Date ( ) ;
for ( let i = days - 1 ; i >= 0 ; i -= 1 ) {
const d = new Date ( today ) ;
d . setDate ( today . getDate ( ) - i ) ;
const key = d . toISOString ( ) . slice ( 0 , 10 ) ;
const hit = byDay . get ( key ) || { usedCents : 0 , taskCount : 0 } ;
series . push ( { date : key , usedCents : hit . usedCents , taskCount : hit . taskCount } ) ;
}
return series ;
}
function clampPositiveInteger ( value , fallback , max ) {
const numeric = Number ( value ) ;
if ( ! Number . isFinite ( numeric ) || numeric <= 0 ) return fallback ;
return Math . min ( Math . trunc ( numeric ) , max ) ;
}
function clampNonNegativeInteger ( value , fallback , max ) {
const numeric = Number ( value ) ;
if ( ! Number . isFinite ( numeric ) || numeric < 0 ) return fallback ;
return Math . min ( Math . trunc ( numeric ) , max ) ;
}
function generateOrderNo ( ) {
const timestamp = Date . now ( ) . toString ( 36 ) . toUpperCase ( ) ;
const random = crypto . randomBytes ( 4 ) . toString ( "hex" ) . toUpperCase ( ) ;
return ` ORD ${ timestamp } ${ random } ` ;
}
const GENERATION _TASK _STATUSES = new Set ( [
"pending" ,
"running" ,
"completed" ,
"failed" ,
"cancelled" ,
] ) ;
const GENERATION _TASK _TYPES = new Set ( [ "image" , "video" ] ) ;
function clampTaskProgress ( value ) {
const numeric = Number ( value ) ;
if ( ! Number . isFinite ( numeric ) ) return 0 ;
return Math . max ( 0 , Math . min ( 100 , Math . trunc ( numeric ) ) ) ;
}
function serializeTaskParams ( value ) {
if ( ! value || typeof value !== "object" ) return "{}" ;
return JSON . stringify ( value ) ;
}
function parseTaskParams ( value ) {
if ( ! value || typeof value !== "string" ) return { } ;
try {
return JSON . parse ( value ) ;
} catch {
return { } ;
}
}
function formatGenerationTaskRow ( row ) {
return {
id : Number ( row . id ) ,
projectId : row . project _id ,
clientQueueId : row . client _queue _id ,
type : row . type ,
status : row . status ,
providerTaskId : row . provider _task _id || null ,
params : parseTaskParams ( row . params _json ) ,
resultUrl : row . result _url || null ,
progress : Number ( row . progress || 0 ) ,
error : row . error || null ,
dedupeKey : row . dedupe _key || null ,
sourceDeviceId : row . source _device _id || null ,
createdAt : row . created _at ,
updatedAt : row . updated _at ,
completedAt : row . completed _at || null ,
} ;
}
function normalizeGenerationTaskPayload ( body ) {
const clientQueueId = String ( body . clientQueueId || body . client _queue _id || "" )
. trim ( )
. slice ( 0 , 128 ) ;
const type = String ( body . type || "" ) . trim ( ) ;
const status = String ( body . status || "pending" ) . trim ( ) ;
if ( ! clientQueueId ) return { error : "Missing clientQueueId" } ;
if ( ! GENERATION _TASK _TYPES . has ( type ) ) return { error : "Invalid task type" } ;
if ( ! GENERATION _TASK _STATUSES . has ( status ) ) return { error : "Invalid task status" } ;
return {
value : {
clientQueueId ,
type ,
status ,
providerTaskId : body . providerTaskId || body . provider _task _id || null ,
paramsJson : serializeTaskParams ( body . params || body . paramsJson || body . params _json ) ,
resultUrl : body . resultUrl || body . result _url || null ,
progress : clampTaskProgress ( body . progress ) ,
error : body . error || null ,
dedupeKey : body . dedupeKey || body . dedupe _key || null ,
sourceDeviceId : body . sourceDeviceId || body . source _device _id || null ,
createdAt : body . createdAt || body . created _at || null ,
completedAt : body . completedAt || body . completed _at || null ,
} ,
} ;
}
async function requireOwnedProject ( client , userId , projectId ) {
const { rows } = await client . query ( "SELECT id FROM projects WHERE id = $1 AND user_id = $2" , [
projectId ,
userId ,
] ) ;
return rows . length > 0 ;
}
async function upsertGenerationTask ( client , userId , projectId , payload ) {
const {
rows : [ row ] ,
} = await client . query (
`
INSERT INTO generation_tasks (
user_id,
project_id,
client_queue_id,
type,
status,
provider_task_id,
params_json,
result_url,
progress,
error,
dedupe_key,
source_device_id,
created_at,
updated_at,
completed_at
)
VALUES (
$ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, $ 10, $ 11, $ 12,
COALESCE( $ 13::timestamptz, NOW()),
NOW(),
$ 14::timestamptz
)
ON CONFLICT (project_id, client_queue_id) WHERE project_id IS NOT NULL DO UPDATE SET
type = EXCLUDED.type,
status = EXCLUDED.status,
provider_task_id = EXCLUDED.provider_task_id,
params_json = EXCLUDED.params_json,
result_url = EXCLUDED.result_url,
progress = EXCLUDED.progress,
error = EXCLUDED.error,
dedupe_key = EXCLUDED.dedupe_key,
source_device_id = EXCLUDED.source_device_id,
updated_at = NOW(),
completed_at = EXCLUDED.completed_at
RETURNING *
` ,
[
userId ,
projectId ,
payload . clientQueueId ,
payload . type ,
payload . status ,
payload . providerTaskId ,
payload . paramsJson ,
payload . resultUrl ,
payload . progress ,
payload . error ,
payload . dedupeKey ,
payload . sourceDeviceId ,
payload . createdAt ,
payload . completedAt ,
] ,
) ;
return row ;
}
module . exports = {
express ,
bcrypt ,
requireAuth ,
requireAdmin ,
requireEnterpriseAdmin ,
requireManagementAccess ,
login ,
generateToken ,
startUserSession ,
getUserContextById ,
isSystemAdmin ,
generateUniqueEnterpriseCode ,
keyManager ,
calculateCost ,
calculateCostMills ,
listModelPrices ,
normalizeModelPriceRow ,
getAverageCostCents ,
loadPriceCache ,
deductForApiCall ,
deductImageGenerationCredits ,
creditBalance ,
creditUserBalance ,
activatePackage ,
distributeCredits ,
getEnterpriseFinancials ,
getUserEnterpriseId ,
getEnterpriseName ,
preauthorizeCall ,
wechatPay ,
alipay ,
crypto ,
pool ,
withTransaction ,
computeNextRevision ,
normalizeRevisionValue ,
shouldRejectStaleRevision ,
USERNAME _PATTERN ,
PRICE _CATEGORIES ,
PRICE _TYPES ,
PHONE _PATTERN ,
EMAIL _PATTERN ,
2026-06-04 18:58:45 +08:00
EMAIL _PURPOSES ,
EMAIL _CODE _TTL _MINUTES ,
EMAIL _CODE _COOLDOWN _SECONDS ,
EMAIL _CODE _MAX _ATTEMPTS ,
2026-06-02 13:14:10 +08:00
SMS _PURPOSES ,
SMS _CODE _TTL _MINUTES ,
SMS _CODE _COOLDOWN _SECONDS ,
SMS _CODE _MAX _ATTEMPTS ,
validateUsername ,
validatePassword ,
normalizePhone ,
validatePhone ,
normalizeEmail ,
validateEmail ,
hashSmsCode ,
generateSmsCode ,
sendSmsCode ,
2026-06-04 18:58:45 +08:00
hashEmailCode ,
sendEmailCode ,
consumeEmailCode ,
2026-06-02 13:14:10 +08:00
createLoginResultForUserId ,
sanitizeUsernameSeed ,
generateUniqueUsername ,
consumeSmsCode ,
getWechatLoginConfig ,
fetchWechatJson ,
exchangeWechatCode ,
findOrCreateWechatUser ,
validateEnterpriseName ,
parseNumericValue ,
ensureEnterpriseExists ,
formatUserRow ,
normalizeOssRegion ,
buildOssPublicUrl ,
normalizeAvatarOssKey ,
normalizeProfileMediaUrl ,
normalizeProjectOssKey ,
getManagementEnterpriseId ,
appendEnterpriseScope ,
readModelPricePayload ,
getModelPriceById ,
getPeriodStart ,
buildDailyTrend ,
clampPositiveInteger ,
clampNonNegativeInteger ,
generateOrderNo ,
GENERATION _TASK _STATUSES ,
GENERATION _TASK _TYPES ,
clampTaskProgress ,
serializeTaskParams ,
parseTaskParams ,
formatGenerationTaskRow ,
normalizeGenerationTaskPayload ,
requireOwnedProject ,
upsertGenerationTask ,
} ;