const { bcrypt, requireAuth, requireAdmin, requireEnterpriseAdmin, listModelPrices, loadPriceCache, creditUserBalance, creditsToCreditUnits, formatCreditsFromCents, pool, validateUsername, validatePassword, validateEnterpriseName, ensureEnterpriseExists, formatUserRow, readModelPricePayload, getModelPriceById, } = require("./context"); function registerAdminRoutes(router) { // ── Admin: Users ───────────────────────────────────────────────────── router.get("/admin/users", requireAuth, requireAdmin, async (_req, res) => { const { rows } = await pool.query(` SELECT u.id, u.username, u.role, u.max_concurrency, u.enabled, u.avatar_url, u.enterprise_id, u.is_enterprise_admin, u.balance_cents, u.billing_mode, u.beta_expires_at, u.created_at, e.name AS enterprise_name FROM users u LEFT JOIN enterprises e ON e.id = u.enterprise_id ORDER BY u.id `); res.json(rows.map(formatUserRow)); }); router.post("/admin/users", requireAuth, requireAdmin, async (req, res) => { const { username, password, role = "user", maxConcurrency = 30, enterpriseId = null, isEnterpriseAdmin = false, } = req.body; const usernameError = validateUsername(username); if (usernameError) return res.status(400).json({ error: usernameError }); const passwordError = validatePassword(password); if (passwordError) return res.status(400).json({ error: passwordError }); if (enterpriseId != null && !(await ensureEnterpriseExists(enterpriseId))) { return res.status(400).json({ error: "企业不存在" }); } try { const hash = await bcrypt.hash(password, 10); const { rows: [row], } = await pool.query( ` INSERT INTO users (username, password_hash, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents) VALUES ($1, $2, $3, $4, $5, $6, 0) RETURNING id `, [ username, hash, role, Number(maxConcurrency) || 30, enterpriseId, isEnterpriseAdmin ? 1 : 0, ], ); const { rows: [userRow], } = await pool.query( ` SELECT u.*, e.name AS enterprise_name FROM users u LEFT JOIN enterprises e ON e.id = u.enterprise_id WHERE u.id = $1 `, [row.id], ); res.json(formatUserRow(userRow)); } catch (error) { console.error("[admin/users:create] failed", error); res.status(409).json({ error: "用户名已存在" }); } }); router.put("/admin/users/:id", requireAuth, requireAdmin, async (req, res) => { const { enabled, role, maxConcurrency, password, enterpriseId, isEnterpriseAdmin, billingMode, betaExpiresAt, } = req.body; const updates = []; const params = []; let idx = 1; if (enabled !== undefined) { updates.push(`enabled = $${idx++}`); params.push(enabled ? 1 : 0); } if (role) { updates.push(`role = $${idx++}`); params.push(role); } if (maxConcurrency !== undefined) { updates.push(`max_concurrency = $${idx++}`); params.push(Number(maxConcurrency) || 30); } if (password) { const passwordError = validatePassword(password); if (passwordError) return res.status(400).json({ error: passwordError }); updates.push(`password_hash = $${idx++}`); params.push(await bcrypt.hash(password, 10)); } if (enterpriseId !== undefined) { if (enterpriseId != null && !(await ensureEnterpriseExists(enterpriseId))) return res.status(400).json({ error: "企业不存在" }); updates.push(`enterprise_id = $${idx++}`); params.push(enterpriseId); } if (isEnterpriseAdmin !== undefined) { updates.push(`is_enterprise_admin = $${idx++}`); params.push(isEnterpriseAdmin ? 1 : 0); } if (billingMode !== undefined) { const mode = String(billingMode || "credits").trim(); if (!["credits", "beta_unlimited"].includes(mode)) { return res.status(400).json({ error: "billingMode 无效" }); } updates.push(`billing_mode = $${idx++}`); params.push(mode); } if (betaExpiresAt !== undefined) { updates.push(`beta_expires_at = $${idx++}`); params.push(betaExpiresAt || null); } if (updates.length === 0) return res.status(400).json({ error: "没有可更新内容" }); updates.push(`updated_at = NOW()`); params.push(req.params.id); await pool.query(`UPDATE users SET ${updates.join(", ")} WHERE id = $${idx}`, params); res.json({ success: true }); }); router.post("/admin/users/:id/credit", requireAuth, requireAdmin, async (req, res) => { const targetUserId = Number(req.params.id); const { amountCredits, amountCents } = req.body; const creditUnits = amountCredits !== undefined && amountCredits !== null && amountCredits !== "" ? creditsToCreditUnits(amountCredits) : Number(amountCents); if (!creditUnits || creditUnits <= 0) return res.status(400).json({ error: "积分必须大于 0" }); try { const newBalance = await creditUserBalance( targetUserId, creditUnits, `管理员 ${req.user.username} 发放 ${formatCreditsFromCents(creditUnits)} 积分`, ); res.json({ success: true, newBalanceCents: newBalance }); } catch (err) { res.status(400).json({ error: err.message || "发放积分失败" }); } }); // ── Admin: Sub-accounts (enterprise admin) ─────────────────────────── router.get("/admin/sub-accounts", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { rows } = await pool.query( ` SELECT u.id, u.username, u.role, u.max_concurrency, u.enabled, u.avatar_url, u.enterprise_id, u.is_enterprise_admin, u.balance_cents, u.billing_mode, u.beta_expires_at, u.created_at, e.name AS enterprise_name FROM users u LEFT JOIN enterprises e ON e.id = u.enterprise_id WHERE u.enterprise_id = $1 ORDER BY u.is_enterprise_admin DESC, u.id ASC `, [req.user.enterpriseId], ); res.json(rows.map(formatUserRow)); }); router.post("/admin/sub-accounts", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { username, password, maxConcurrency = 30 } = req.body; const usernameError = validateUsername(username); if (usernameError) return res.status(400).json({ error: usernameError }); const passwordError = validatePassword(password); if (passwordError) return res.status(400).json({ error: passwordError }); try { const hash = await bcrypt.hash(password, 10); const { rows: [row], } = await pool.query( ` INSERT INTO users (username, password_hash, role, max_concurrency, enterprise_id, is_enterprise_admin, balance_cents) VALUES ($1, $2, 'user', $3, $4, 0, 0) RETURNING id `, [username, hash, Number(maxConcurrency) || 30, req.user.enterpriseId], ); const { rows: [userRow], } = await pool.query( ` SELECT u.id, u.username, u.role, u.max_concurrency, u.enabled, u.avatar_url, u.enterprise_id, u.is_enterprise_admin, u.balance_cents, u.billing_mode, u.beta_expires_at, u.created_at, e.name AS enterprise_name FROM users u LEFT JOIN enterprises e ON e.id = u.enterprise_id WHERE u.id = $1 `, [row.id], ); res.json(formatUserRow(userRow)); } catch (error) { console.error("[admin/sub-accounts:create] failed", error); res.status(409).json({ error: "用户名已存在" }); } }); router.put("/admin/sub-accounts/:id", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { rows: [target], } = await pool.query("SELECT id, enterprise_id, is_enterprise_admin FROM users WHERE id = $1", [ req.params.id, ]); if (!target || Number(target.enterprise_id || 0) !== Number(req.user.enterpriseId || 0)) { return res.status(404).json({ error: "子账号不存在" }); } const { enabled, maxConcurrency, password } = req.body; const updates = []; const params = []; let idx = 1; if (enabled !== undefined) { if (Number(target.id) === Number(req.user.id) && !enabled) return res.status(400).json({ error: "不能禁用当前企业管理员账号" }); updates.push(`enabled = $${idx++}`); params.push(enabled ? 1 : 0); } if (maxConcurrency !== undefined) { updates.push(`max_concurrency = $${idx++}`); params.push(Number(maxConcurrency) || 30); } if (password) { const passwordError = validatePassword(password); if (passwordError) return res.status(400).json({ error: passwordError }); updates.push(`password_hash = $${idx++}`); params.push(await bcrypt.hash(password, 10)); } if (updates.length === 0) return res.status(400).json({ error: "没有可更新内容" }); updates.push("updated_at = NOW()"); params.push(req.params.id); await pool.query(`UPDATE users SET ${updates.join(", ")} WHERE id = $${idx}`, params); res.json({ success: true }); }); // ── Admin: Provider Health ──────────────────────────────────────────── router.get('/admin/providers/status', requireAuth, requireAdmin, async (_req, res) => { const { getProviderHealthCache } = require('../providerHealthMonitor'); const { pool } = require('./context'); const health = getProviderHealthCache(); const { rows: callStats } = await pool.query( 'SELECT provider, model, status, COUNT(*) AS count, AVG(duration_ms) AS avg_ms, SUM(cost_estimate) AS total_cost FROM api_call_logs WHERE created_at > NOW() - INTERVAL \$1 GROUP BY provider, model, status ORDER BY provider, model', ['1 hour'] ); const { rows: keyStats } = await pool.query( 'SELECT provider, COUNT(*) AS total_keys, SUM(CASE WHEN enabled = 1 THEN 1 ELSE 0 END) AS active_keys, SUM(active_count) AS current_load FROM api_keys GROUP BY provider ORDER BY provider' ); res.json({ health, callStats, keyStats, checkedAt: new Date().toISOString() }); }); // ── Admin: Prices ──────────────────────────────────────────────────── router.get("/admin/prices", requireAuth, requireAdmin, async (_req, res) => { const prices = await listModelPrices(); res.json(prices); }); router.post("/admin/prices", requireAuth, requireAdmin, async (req, res) => { const payload = readModelPricePayload(req.body); if (payload.error) return res.status(400).json({ error: payload.error }); try { const { rows: [row], } = await pool.query( ` INSERT INTO model_prices (model_key, display_name, category, pricing_type, input_price_mills, output_price_mills, flat_price_mills, currency, enabled) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING id `, [ payload.value.modelKey, payload.value.displayName, payload.value.category, payload.value.pricingType, payload.value.inputPriceMills, payload.value.outputPriceMills, payload.value.flatPriceMills, payload.value.currency, payload.value.enabled ? 1 : 0, ], ); await loadPriceCache(); res.json(await getModelPriceById(row.id)); } catch (error) { console.error("[admin/prices:create] failed", error); res.status(409).json({ error: "模型标识已存在" }); } }); router.put("/admin/prices/:id", requireAuth, requireAdmin, async (req, res) => { const existing = await getModelPriceById(req.params.id); if (!existing) return res.status(404).json({ error: "定价不存在" }); const payload = readModelPricePayload(req.body, existing); if (payload.error) return res.status(400).json({ error: payload.error }); try { await pool.query( ` UPDATE model_prices SET model_key = $1, display_name = $2, category = $3, pricing_type = $4, input_price_mills = $5, output_price_mills = $6, flat_price_mills = $7, currency = $8, enabled = $9 WHERE id = $10 `, [ payload.value.modelKey, payload.value.displayName, payload.value.category, payload.value.pricingType, payload.value.inputPriceMills, payload.value.outputPriceMills, payload.value.flatPriceMills, payload.value.currency, payload.value.enabled ? 1 : 0, req.params.id, ], ); await loadPriceCache(); res.json(await getModelPriceById(req.params.id)); } catch (error) { console.error("[admin/prices:update] failed", error); res.status(409).json({ error: "模型标识已存在" }); } }); // ── Admin: Keys ────────────────────────────────────────────────────── router.get("/admin/keys", requireAuth, requireAdmin, async (_req, res) => { const { rows } = await pool.query( "SELECT id, provider, label, max_concurrency, active_count, total_used, enabled, created_at FROM api_keys ORDER BY provider, id", ); res.json(rows); }); router.post("/admin/keys", requireAuth, requireAdmin, async (req, res) => { const { provider, api_key, label = "", max_concurrency = 10 } = req.body; if (!provider || !api_key) return res.status(400).json({ error: "缺少 provider 或 api_key" }); const { rows: [row], } = await pool.query( "INSERT INTO api_keys (provider, api_key, label, max_concurrency) VALUES ($1, $2, $3, $4) RETURNING id", [provider, api_key, label, Number(max_concurrency) || 10], ); res.json({ id: row.id, provider, label }); }); router.put("/admin/keys/:id", requireAuth, requireAdmin, async (req, res) => { const { enabled, label, max_concurrency, api_key } = req.body; const updates = []; const params = []; let idx = 1; if (enabled !== undefined) { updates.push(`enabled = $${idx++}`); params.push(enabled ? 1 : 0); } if (label !== undefined) { updates.push(`label = $${idx++}`); params.push(label); } if (max_concurrency !== undefined) { updates.push(`max_concurrency = $${idx++}`); params.push(Number(max_concurrency) || 10); } if (api_key) { updates.push(`api_key = $${idx++}`); params.push(api_key); } if (updates.length === 0) return res.status(400).json({ error: "没有可更新内容" }); params.push(req.params.id); await pool.query(`UPDATE api_keys SET ${updates.join(", ")} WHERE id = $${idx}`, params); res.json({ success: true }); }); router.delete("/admin/keys/:id", requireAuth, requireAdmin, async (req, res) => { await pool.query("DELETE FROM api_keys WHERE id = $1", [req.params.id]); res.json({ success: true }); }); // ── Admin: Enterprises ─────────────────────────────────────────────── router.get("/admin/enterprises", requireAuth, requireAdmin, async (_req, res) => { const { rows } = await pool.query(` SELECT e.*, COUNT(u.id) AS user_count FROM enterprises e LEFT JOIN users u ON u.enterprise_id = e.id AND u.enabled = 1 GROUP BY e.id ORDER BY e.id `); res.json( rows.map((row) => ({ id: Number(row.id), name: row.name, contactName: row.contact_name, contactPhone: row.contact_phone, taxId: row.tax_id, legalPersonName: row.legal_person_name, legalPersonPhone: row.legal_person_phone, enterpriseCode: row.enterprise_code, balanceCents: row.balance_cents, enabled: !!row.enabled, userCount: Number(row.user_count), createdAt: row.created_at, updatedAt: row.updated_at, })), ); }); router.get("/admin/enterprises/:id", requireAuth, requireAdmin, async (req, res) => { const { rows: [row], } = await pool.query("SELECT * FROM enterprises WHERE id = $1", [req.params.id]); if (!row) return res.status(404).json({ error: "企业不存在" }); res.json({ id: Number(row.id), name: row.name, contactName: row.contact_name, contactPhone: row.contact_phone, taxId: row.tax_id, legalPersonName: row.legal_person_name, legalPersonPhone: row.legal_person_phone, enterpriseCode: row.enterprise_code, balanceCents: row.balance_cents, enabled: !!row.enabled, createdAt: row.created_at, updatedAt: row.updated_at, }); }); router.put("/admin/enterprises/:id", requireAuth, requireAdmin, async (req, res) => { const { name, contactName, contactPhone, taxId, legalPersonName, legalPersonPhone, enabled } = req.body; const updates = []; const params = []; let idx = 1; if (name !== undefined) { const enterpriseError = validateEnterpriseName(name); if (enterpriseError) return res.status(400).json({ error: enterpriseError }); updates.push(`name = $${idx++}`); params.push(name.trim()); } if (contactName !== undefined) { updates.push(`contact_name = $${idx++}`); params.push(String(contactName || "").trim()); } if (contactPhone !== undefined) { updates.push(`contact_phone = $${idx++}`); params.push(String(contactPhone || "").trim()); } if (taxId !== undefined) { updates.push(`tax_id = $${idx++}`); params.push(String(taxId || "").trim() || null); } if (legalPersonName !== undefined) { updates.push(`legal_person_name = $${idx++}`); params.push(String(legalPersonName || "").trim() || null); } if (legalPersonPhone !== undefined) { updates.push(`legal_person_phone = $${idx++}`); params.push(String(legalPersonPhone || "").trim() || null); } if (enabled !== undefined) { updates.push(`enabled = $${idx++}`); params.push(enabled ? 1 : 0); } if (updates.length === 0) return res.status(400).json({ error: "没有可更新内容" }); updates.push("updated_at = NOW()"); params.push(req.params.id); await pool.query(`UPDATE enterprises SET ${updates.join(", ")} WHERE id = $${idx}`, params); res.json({ success: true }); }); // ── Admin: Packages ────────────────────────────────────────────────── router.get("/admin/packages", requireAuth, requireAdmin, async (_req, res) => { const { rows } = await pool.query("SELECT * FROM packages ORDER BY sort_order, id"); res.json( rows.map((row) => ({ id: Number(row.id), name: row.name, description: row.description, priceCents: row.price_cents, creditsCents: row.credits_cents, imageQuota: row.image_quota, videoQuota: row.video_quota, textQuota: row.text_quota, durationDays: row.duration_days, enabled: !!row.enabled, sortOrder: row.sort_order, createdAt: row.created_at, })), ); }); router.post("/admin/packages", requireAuth, requireAdmin, async (req, res) => { const { name, description = "", priceCents, credits, amountCredits, creditsCents = 0, imageQuota = 0, videoQuota = 0, textQuota = 0, durationDays = 365, enabled = true, sortOrder = 0, } = req.body; if (!name) return res.status(400).json({ error: "缺少套餐名称" }); if (priceCents == null || priceCents <= 0) return res.status(400).json({ error: "售价必须大于0" }); try { const { rows: [row], } = await pool.query( ` INSERT INTO packages (name, description, price_cents, credits_cents, image_quota, video_quota, text_quota, duration_days, enabled, sort_order) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id `, [ name, description, Number(priceCents), credits !== undefined || amountCredits !== undefined ? creditsToCreditUnits(credits ?? amountCredits) : Number(creditsCents || 0), Number(imageQuota || 0), Number(videoQuota || 0), Number(textQuota || 0), Number(durationDays || 365), enabled ? 1 : 0, Number(sortOrder || 0), ], ); res.json({ id: row.id, success: true }); } catch (error) { console.error("[admin/packages:create] failed", error); res.status(500).json({ error: "创建套餐失败" }); } }); router.put("/admin/packages/:id", requireAuth, requireAdmin, async (req, res) => { const { rows: [pkg], } = await pool.query("SELECT * FROM packages WHERE id = $1", [req.params.id]); if (!pkg) return res.status(404).json({ error: "套餐不存在" }); const { name, description, priceCents, credits, amountCredits, creditsCents, imageQuota, videoQuota, textQuota, durationDays, enabled, sortOrder, } = req.body; const updates = []; const params = []; let idx = 1; if (name !== undefined) { updates.push(`name = $${idx++}`); params.push(name); } if (description !== undefined) { updates.push(`description = $${idx++}`); params.push(description); } if (priceCents !== undefined) { updates.push(`price_cents = $${idx++}`); params.push(Number(priceCents)); } if (credits !== undefined || amountCredits !== undefined) { updates.push(`credits_cents = $${idx++}`); params.push(creditsToCreditUnits(credits ?? amountCredits)); } else if (creditsCents !== undefined) { updates.push(`credits_cents = $${idx++}`); params.push(Number(creditsCents)); } if (imageQuota !== undefined) { updates.push(`image_quota = $${idx++}`); params.push(Number(imageQuota)); } if (videoQuota !== undefined) { updates.push(`video_quota = $${idx++}`); params.push(Number(videoQuota)); } if (textQuota !== undefined) { updates.push(`text_quota = $${idx++}`); params.push(Number(textQuota)); } if (durationDays !== undefined) { updates.push(`duration_days = $${idx++}`); params.push(Number(durationDays)); } if (enabled !== undefined) { updates.push(`enabled = $${idx++}`); params.push(enabled ? 1 : 0); } if (sortOrder !== undefined) { updates.push(`sort_order = $${idx++}`); params.push(Number(sortOrder)); } if (updates.length === 0) return res.status(400).json({ error: "没有可更新内容" }); params.push(req.params.id); await pool.query(`UPDATE packages SET ${updates.join(", ")} WHERE id = $${idx}`, params); res.json({ success: true }); }); router.delete("/admin/packages/:id", requireAuth, requireAdmin, async (req, res) => { await pool.query("UPDATE packages SET enabled = 0 WHERE id = $1", [req.params.id]); res.json({ success: true }); }); } function registerAdminInvoiceRoutes(router) { // ── Admin: Invoices ────────────────────────────────────────────────── router.get("/admin/invoices", requireAuth, requireAdmin, async (_req, res) => { const { rows } = await pool.query(` SELECT i.*, e.name AS enterprise_name FROM invoices i LEFT JOIN enterprises e ON e.id = i.enterprise_id ORDER BY i.id DESC `); res.json( rows.map((row) => ({ id: Number(row.id), enterpriseId: Number(row.enterprise_id), enterpriseName: row.enterprise_name, type: row.type, title: row.title, taxNo: row.tax_no, amountCents: row.amount_cents, status: row.status, invoiceNo: row.invoice_no, invoiceUrl: row.invoice_url, issuedAt: row.issued_at, createdAt: row.created_at, })), ); }); router.put("/admin/invoices/:id", requireAuth, requireAdmin, async (req, res) => { const { invoiceNo, invoiceUrl, status } = req.body; const updates = []; const params = []; let idx = 1; if (invoiceNo !== undefined) { updates.push(`invoice_no = $${idx++}`); params.push(invoiceNo); } if (invoiceUrl !== undefined) { updates.push(`invoice_url = $${idx++}`); params.push(invoiceUrl); } if (status) { updates.push(`status = $${idx++}`); params.push(status); } if (status === "issued") { updates.push("issued_at = NOW()"); } if (updates.length === 0) return res.status(400).json({ error: "没有可更新内容" }); params.push(req.params.id); await pool.query(`UPDATE invoices SET ${updates.join(", ")} WHERE id = $${idx}`, params); res.json({ success: true }); }); } module.exports = { registerAdminRoutes, registerAdminInvoiceRoutes, };