729 lines
25 KiB
JavaScript
729 lines
25 KiB
JavaScript
const {
|
|
bcrypt,
|
|
requireAuth,
|
|
requireAdmin,
|
|
requireEnterpriseAdmin,
|
|
listModelPrices,
|
|
loadPriceCache,
|
|
creditUserBalance,
|
|
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 { amountCents } = req.body;
|
|
if (!amountCents || amountCents <= 0) return res.status(400).json({ error: "积分必须大于 0" });
|
|
|
|
try {
|
|
const newBalance = await creditUserBalance(
|
|
targetUserId,
|
|
amountCents,
|
|
`管理员 ${req.user.username} 发放 ${Math.floor(amountCents / 100)} 积分`,
|
|
);
|
|
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,
|
|
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),
|
|
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,
|
|
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 (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,
|
|
};
|