Files
omniai-server/src/routes/enterprise.js
T

502 lines
17 KiB
JavaScript
Raw Normal View History

2026-06-02 13:14:10 +08:00
const {
requireAuth,
requireEnterpriseAdmin,
distributeCredits,
2026-06-08 15:46:28 +08:00
creditsToCreditUnits,
2026-06-02 13:14:10 +08:00
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) => {
2026-06-08 15:46:28 +08:00
const { userId, amountCredits, amountCents, distributions } = req.body;
2026-06-02 13:14:10 +08:00
try {
if (distributions && Array.isArray(distributions)) {
for (const d of distributions) {
2026-06-08 15:46:28 +08:00
const creditUnits =
d.amountCredits !== undefined && d.amountCredits !== null && d.amountCredits !== ""
? creditsToCreditUnits(d.amountCredits)
: Number(d.amountCents);
if (!d.userId || !creditUnits || creditUnits <= 0) {
2026-06-02 13:14:10 +08:00
return res
.status(400)
2026-06-08 15:46:28 +08:00
.json({ error: "每条分发记录必须包含有效的 userId 和 amountCredits" });
2026-06-02 13:14:10 +08:00
}
2026-06-08 15:46:28 +08:00
await distributeCredits(req.user.enterpriseId, d.userId, creditUnits, req.user.id);
2026-06-02 13:14:10 +08:00
}
res.json({ success: true, count: distributions.length });
2026-06-08 15:46:28 +08:00
} 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" });
2026-06-02 13:14:10 +08:00
const result = await distributeCredits(
req.user.enterpriseId,
userId,
2026-06-08 15:46:28 +08:00
creditUnits,
2026-06-02 13:14:10 +08:00
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,
2026-06-08 15:46:28 +08:00
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,
2026-06-02 13:14:10 +08:00
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,
};