const { requireAuth, requireEnterpriseAdmin, distributeCredits, creditsToCreditUnits, getEnterpriseFinancials, getEnterpriseName, pool, getPeriodStart, buildDailyTrend, generateOrderNo, clampPositiveInteger, clampNonNegativeInteger, } = require("./context"); function registerEnterpriseRoutes(router) { // ── Enterprise: Dashboard & Financials ──────────────────────────────── router.get("/enterprise/dashboard", requireAuth, requireEnterpriseAdmin, async (req, res) => { const financials = await getEnterpriseFinancials(req.user.enterpriseId); const { rows: [countRow], } = await pool.query( "SELECT COUNT(*) AS count FROM users WHERE enterprise_id = $1 AND enabled = 1 AND is_enterprise_admin = 0", [req.user.enterpriseId], ); res.json({ enterpriseName: req.user.enterpriseName, enterpriseCode: req.user.enterpriseCode, balanceCents: financials.balanceCents, activePackages: financials.activePackages, subAccountCount: Number(countRow?.count || 0), }); }); router.get("/enterprise/financials", requireAuth, requireEnterpriseAdmin, async (req, res) => { const financials = await getEnterpriseFinancials(req.user.enterpriseId); res.json({ balanceCents: financials.balanceCents, activePackages: financials.activePackages.map((p) => ({ id: p.id, packageName: p.package_name, remainingImage: p.remaining_image, remainingVideo: p.remaining_video, remainingText: p.remaining_text, expiresAt: p.expires_at, activatedAt: p.activated_at, })), recentTransactions: financials.recentTransactions.map((t) => ({ id: t.id, type: t.type, amountCents: t.amount_cents, balanceAfterCents: t.balance_after_cents, description: t.description, createdAt: t.created_at, })), }); }); router.get("/enterprise/usage/summary", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { period = "30d" } = req.query; const periodStart = getPeriodStart(period); const ledgerDateJoin = periodStart ? `AND cl.created_at >= ${periodStart}` : ""; const ledgerDateWhere = periodStart ? `AND created_at >= ${periodStart}` : ""; const recordsLimit = clampPositiveInteger(req.query.limit, 50, 200); try { const { rows: [enterprise], } = await pool.query( "SELECT id, name, balance_cents FROM enterprises WHERE id = $1 AND enabled = 1 LIMIT 1", [req.user.enterpriseId], ); if (!enterprise) return res.status(404).json({ error: "企业不存在或已禁用" }); const { rows: members } = await pool.query( ` SELECT u.id AS user_id, u.username, COALESCE(em.role, CASE WHEN u.is_enterprise_admin = 1 THEN 'admin' ELSE 'employee' END) AS member_role, COALESCE(SUM(CASE WHEN cl.status IN ('reserved', 'charged') THEN cl.amount_cents ELSE 0 END), 0) AS used_cents, COUNT(CASE WHEN cl.status IN ('reserved', 'charged') THEN 1 END) AS task_count, MAX(cl.created_at) AS last_used_at FROM users u LEFT JOIN enterprise_members em ON em.enterprise_id = u.enterprise_id AND em.user_id = u.id LEFT JOIN credit_ledger cl ON cl.enterprise_id = u.enterprise_id AND cl.user_id = u.id ${ledgerDateJoin} WHERE u.enterprise_id = $1 AND u.enabled = 1 GROUP BY u.id, u.username, em.role, u.is_enterprise_admin ORDER BY used_cents DESC, u.id ASC `, [req.user.enterpriseId], ); const { rows: modelBreakdown } = await pool.query( ` SELECT COALESCE(model, 'unknown') AS model, COALESCE(SUM(amount_cents), 0) AS used_cents, COUNT(*) AS task_count FROM credit_ledger WHERE enterprise_id = $1 AND status IN ('reserved', 'charged') ${ledgerDateWhere} GROUP BY COALESCE(model, 'unknown') ORDER BY used_cents DESC LIMIT 50 `, [req.user.enterpriseId], ); const { rows: [totalRow], } = await pool.query( ` SELECT COALESCE(SUM(amount_cents), 0) AS total_used_cents FROM credit_ledger WHERE enterprise_id = $1 AND status IN ('reserved', 'charged') ${ledgerDateWhere} `, [req.user.enterpriseId], ); const { rows: records } = await pool.query( ` SELECT cl.*, u.username, gt.params_json AS task_params_json FROM credit_ledger cl LEFT JOIN users u ON u.id = cl.user_id LEFT JOIN generation_tasks gt ON gt.id = cl.task_id WHERE cl.enterprise_id = $1 AND cl.status IN ('reserved', 'charged') ${periodStart ? `AND cl.created_at >= ${periodStart}` : ""} ORDER BY cl.created_at DESC LIMIT $2 `, [req.user.enterpriseId, recordsLimit], ); const { rows: trendRows } = await pool.query( ` SELECT to_char(date_trunc('day', created_at), 'YYYY-MM-DD') AS day, COUNT(*) AS task_count, COALESCE(SUM(amount_cents), 0) AS used_cents FROM credit_ledger WHERE enterprise_id = $1 AND status IN ('reserved', 'charged') AND created_at >= NOW() - INTERVAL '6 days' GROUP BY day ORDER BY day ASC `, [req.user.enterpriseId], ); const dailyTrend = buildDailyTrend(trendRows); res.json({ enterpriseId: String(enterprise.id), enterpriseName: enterprise.name, balanceCents: Number(enterprise.balance_cents || 0), totalUsedCents: Number(totalRow?.total_used_cents || 0), members: members.map((row) => ({ userId: Number(row.user_id), username: row.username, role: row.member_role || "employee", usedCents: Number(row.used_cents || 0), taskCount: Number(row.task_count || 0), lastUsedAt: row.last_used_at || null, })), modelBreakdown: modelBreakdown.map((row) => ({ model: row.model, usedCents: Number(row.used_cents || 0), taskCount: Number(row.task_count || 0), })), records: records.map((row) => { let prompt = ""; try { const params = row.task_params_json ? JSON.parse(row.task_params_json) : {}; prompt = params.prompt || ""; } catch {} return { id: String(row.id), userId: row.user_id == null ? "" : Number(row.user_id), username: row.username || "", model: row.model || "", taskType: row.task_type, resolution: row.resolution || null, durationSeconds: row.duration_seconds == null ? null : Number(row.duration_seconds), amountCents: Number(row.amount_cents || 0), prompt, status: row.status, createdAt: row.created_at, }; }), }); } catch (error) { console.error("[enterprise/usage/summary] failed", error); res.status(500).json({ error: "企业用量汇总加载失败" }); } }); router.get("/enterprise/usage/records", requireAuth, requireEnterpriseAdmin, async (req, res) => { const limit = clampPositiveInteger(req.query.limit, 50, 200); const offset = clampNonNegativeInteger(req.query.offset, 0, 100000); const userId = req.query.userId || req.query.user_id; const model = String(req.query.model || "").trim(); const dateFrom = req.query.from || req.query.date_from; const dateTo = req.query.to || req.query.date_to; const where = ["cl.enterprise_id = $1"]; const params = [req.user.enterpriseId]; if (userId) { params.push(userId); where.push(`cl.user_id = $${params.length}`); } if (model) { params.push(model); where.push(`cl.model = $${params.length}`); } if (dateFrom) { params.push(`${dateFrom}T00:00:00.000Z`); where.push(`cl.created_at >= $${params.length}`); } if (dateTo) { params.push(`${dateTo}T23:59:59.999Z`); where.push(`cl.created_at <= $${params.length}`); } const whereSql = `WHERE ${where.join(" AND ")}`; try { const { rows: [countRow], } = await pool.query(`SELECT COUNT(*) AS total FROM credit_ledger cl ${whereSql}`, params); const { rows } = await pool.query( ` SELECT cl.*, u.username, gt.params_json AS task_params_json FROM credit_ledger cl LEFT JOIN users u ON u.id = cl.user_id LEFT JOIN generation_tasks gt ON gt.id = cl.task_id ${whereSql} ORDER BY cl.created_at DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2} `, [...params, limit, offset], ); res.json({ items: rows.map((row) => { let prompt = ""; try { const params = row.task_params_json ? JSON.parse(row.task_params_json) : {}; prompt = params.prompt || ""; } catch {} return { id: String(row.id), userId: row.user_id == null ? "" : Number(row.user_id), username: row.username || "", model: row.model || "", taskType: row.task_type, resolution: row.resolution || null, durationSeconds: row.duration_seconds == null ? null : Number(row.duration_seconds), amountCents: Number(row.amount_cents || 0), prompt, status: row.status, createdAt: row.created_at, }; }), total: Number(countRow?.total || 0), limit, offset, }); } catch (error) { console.error("[enterprise/usage/records] failed", error); res.status(500).json({ error: "企业用量记录加载失败" }); } }); router.post("/enterprise/recharge", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { amountCents, paymentMethod = "wechat" } = req.body; if (!amountCents || amountCents <= 0) return res.status(400).json({ error: "充值金额必须大于0" }); const orderNo = generateOrderNo(); const enterpriseName = await getEnterpriseName(req.user.enterpriseId); try { await pool.query( ` INSERT INTO payment_orders (order_no, enterprise_id, enterprise_name, type, amount_cents, payment_method, status) VALUES ($1, $2, $3, 'recharge', $4, $5, 'pending') `, [orderNo, req.user.enterpriseId, enterpriseName, Number(amountCents), paymentMethod], ); res.json({ orderNo, amountCents, paymentMethod }); } catch (error) { console.error("[enterprise/recharge] failed", error); res.status(500).json({ error: "创建充值订单失败" }); } }); router.post("/enterprise/distribute", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { userId, amountCredits, amountCents, distributions } = req.body; try { if (distributions && Array.isArray(distributions)) { for (const d of distributions) { const creditUnits = d.amountCredits !== undefined && d.amountCredits !== null && d.amountCredits !== "" ? creditsToCreditUnits(d.amountCredits) : Number(d.amountCents); if (!d.userId || !creditUnits || creditUnits <= 0) { return res .status(400) .json({ error: "每条分发记录必须包含有效的 userId 和 amountCredits" }); } await distributeCredits(req.user.enterpriseId, d.userId, creditUnits, req.user.id); } res.json({ success: true, count: distributions.length }); } else if (userId && (amountCredits || amountCents)) { const creditUnits = amountCredits !== undefined && amountCredits !== null && amountCredits !== "" ? creditsToCreditUnits(amountCredits) : Number(amountCents); if (!creditUnits || creditUnits <= 0) return res.status(400).json({ error: "分发积分必须大于0" }); const result = await distributeCredits( req.user.enterpriseId, userId, creditUnits, req.user.id, ); res.json({ success: true, ...result }); } else { return res.status(400).json({ error: "缺少分发参数" }); } } catch (error) { console.error("[enterprise/distribute] failed", error); res.status(400).json({ error: "分发参数处理失败" }); } }); router.get( "/enterprise/employee-consumption", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { period = "30d" } = req.query; const periodStart = getPeriodStart(period); const whereClause = periodStart ? `AND acl.created_at >= ${periodStart}` : ""; const { rows } = await pool.query( ` SELECT u.id AS user_id, u.username, u.balance_cents AS current_balance_cents, COUNT(acl.id) AS total_calls, COALESCE(SUM(CASE WHEN acl.cost_estimate IS NOT NULL THEN CAST(ROUND((acl.cost_estimate * 10000)::numeric) AS INTEGER) ELSE 0 END), 0) AS total_cost_cents, MAX(acl.created_at) AS last_active FROM users u LEFT JOIN api_call_logs acl ON acl.user_id = u.id AND acl.status = 'success' WHERE u.enterprise_id = $1 AND u.enabled = 1 AND u.is_enterprise_admin = 0 ${whereClause} GROUP BY u.id, u.username, u.balance_cents ORDER BY total_cost_cents DESC `, [req.user.enterpriseId], ); res.json( rows.map((r) => ({ userId: Number(r.user_id), username: r.username, currentBalanceCents: Number(r.current_balance_cents), totalCalls: Number(r.total_calls), totalCostCents: Number(r.total_cost_cents), lastActive: r.last_active, })), ); }, ); router.post( "/enterprise/purchase-package", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { packageId, paymentMethod = "wechat" } = req.body; if (!packageId) return res.status(400).json({ error: "缺少套餐ID" }); const { rows: [pkg], } = await pool.query("SELECT * FROM packages WHERE id = $1 AND enabled = 1", [packageId]); if (!pkg) return res.status(404).json({ error: "套餐不存在或已下架" }); const orderNo = generateOrderNo(); const enterpriseName = await getEnterpriseName(req.user.enterpriseId); try { await pool.query( ` INSERT INTO payment_orders (order_no, enterprise_id, enterprise_name, type, amount_cents, package_id, payment_method, status) VALUES ($1, $2, $3, 'package', $4, $5, $6, 'pending') `, [ orderNo, req.user.enterpriseId, enterpriseName, pkg.price_cents, packageId, paymentMethod, ], ); res.json({ orderNo, amountCents: pkg.price_cents, packageId, paymentMethod }); } catch (error) { console.error("[enterprise/purchase-package] failed", error); res.status(500).json({ error: "创建套餐订单失败" }); } }, ); // ── Enterprise: Invoices ────────────────────────────────────────────── router.post( "/enterprise/invoice-apply", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { paymentOrderId, type = "general", title, taxNo } = req.body; if (!title) return res.status(400).json({ error: "缺少发票抬头" }); let amountCents = 0; if (paymentOrderId) { const { rows: [order], } = await pool.query( "SELECT * FROM payment_orders WHERE id = $1 AND enterprise_id = $2 AND status = $3", [paymentOrderId, req.user.enterpriseId, "paid"], ); if (!order) return res.status(404).json({ error: "支付订单不存在或未支付" }); amountCents = order.amount_cents; } const enterpriseName = await getEnterpriseName(req.user.enterpriseId); try { const { rows: [row], } = await pool.query( ` INSERT INTO invoices (enterprise_id, enterprise_name, payment_order_id, type, title, tax_no, amount_cents, status) VALUES ($1, $2, $3, $4, $5, $6, $7, 'pending') RETURNING id `, [ req.user.enterpriseId, enterpriseName, paymentOrderId || null, type, title, taxNo || null, amountCents, ], ); res.json({ id: row.id, success: true }); } catch (error) { console.error("[enterprise/invoice-apply] failed", error); res.status(500).json({ error: "申请发票失败" }); } }, ); router.get("/enterprise/invoices", requireAuth, requireEnterpriseAdmin, async (req, res) => { const { rows } = await pool.query( "SELECT * FROM invoices WHERE enterprise_id = $1 ORDER BY id DESC", [req.user.enterpriseId], ); res.json( rows.map((row) => ({ id: Number(row.id), 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, })), ); }); } module.exports = { registerEnterpriseRoutes, };