// admin-routes.mjs // // Back-office HTTP route factories for the standalone memind_adm service. // // Route logic lives here exactly once. The public server (server.mjs) no longer // serves these surfaces; admin-server.mjs is the sole consumer. Services are // injected so the same router can be wired against any container (tests, future // read-replica pool, etc.). // // createAdminApi -> /admin-api/* platform super-admin (role === 'admin') // createOpsApi -> /api/ops/v1/* plaza operations console (ops_role) import express from 'express'; import { sendData, sendError } from './api-response.mjs'; import { PLAN_CATALOG, tokensToCallsApprox } from './billing-subscription.mjs'; import { hasOpsRole } from './plaza-ops.mjs'; import { mapPlazaError } from './plaza-posts.mjs'; function plazaRouteError(res, req, error) { const status = mapPlazaError(error); const code = error?.code ?? 'internal_error'; const message = error instanceof Error ? error.message : 'Plaza 请求失败'; return sendError(res, req, status, code, message, error?.details); } /** * Platform super-admin API (role === 'admin'). * @param {object} deps * @param {import('express').RequestHandler} deps.jsonBody JSON body parser middleware. * @param {(req: import('express').Request) => (string|undefined)} deps.getToken Extract the user session token from a request. * @param {Promise} [deps.ready] Optional bootstrap gate awaited before the first request resolves auth. * @param {object} deps.userAuth * @param {object|null} deps.llmProviderService * @param {object|null} deps.memoryV2ConfigService * @param {object|null} deps.skillRuntimeConfigService * @param {object|null} deps.adminSystemTestService * @param {object|null} deps.plazaPosts * @param {object|null} deps.plazaOps * @param {object|null} deps.wechatAdmin */ export function createAdminApi({ jsonBody, getToken, ready, userAuth, llmProviderService, assetGatewayConfigService, imageMakeAdminConfigService, memoryV2ConfigService, mindSearchConfigService, skillRuntimeConfigService, adminSystemTestService, plazaPosts, plazaOps, wechatAdmin, subscriptionService, }) { function requireAdmin(req, res, next) { if (!req.currentUser || req.currentUser.role !== 'admin') { res.status(403).json({ message: '需要管理员权限' }); return; } next(); } const adminApi = express.Router(); adminApi.use(jsonBody); adminApi.use(async (req, res, next) => { if (ready) await ready; if (!userAuth) return res.status(503).json({ message: '未启用用户系统' }); const me = await userAuth.getMe(getToken(req)); if (!me) return res.status(401).json({ message: '未登录' }); req.currentUser = me; next(); }); adminApi.get('/users', requireAdmin, async (req, res) => { const { page, pageSize, search, role, status } = req.query; const result = await userAuth.listUsers({ page, pageSize, search, role, status }); res.json(result); }); adminApi.get('/summary', requireAdmin, async (_req, res) => { const summary = await userAuth.getAdminSummary(); let llm = null; if (llmProviderService) { const keys = await llmProviderService.listKeys(); const global = await llmProviderService.getGlobalSettings(); const selected = keys.find((key) => key.isSelected); llm = { keyCount: keys.length, selectedKeyName: selected?.name ?? null, globalModel: global?.model ?? null, }; } res.json({ summary: { ...summary, llm } }); }); adminApi.get('/asset-gateway/config', requireAdmin, async (_req, res) => { if (!assetGatewayConfigService?.getConfig) { return res.status(503).json({ message: '资产能力配置服务未启用' }); } return res.json(await assetGatewayConfigService.getConfig()); }); adminApi.put('/asset-gateway/config', requireAdmin, async (req, res) => { if (!assetGatewayConfigService?.updateGlobalConfig) { return res.status(503).json({ message: '资产能力配置服务未启用' }); } return res.json(await assetGatewayConfigService.updateGlobalConfig(req.body ?? {}, { updatedBy: req.currentUser.id, })); }); adminApi.put('/asset-gateway/plugins/:pluginId', requireAdmin, async (req, res) => { if (!assetGatewayConfigService?.updatePluginConfig) { return res.status(503).json({ message: '资产能力配置服务未启用' }); } const result = await assetGatewayConfigService.updatePluginConfig(req.params.pluginId, req.body ?? {}, { updatedBy: req.currentUser.id, }); if (!result.ok) return res.status(400).json({ message: result.message }); return res.json(result); }); adminApi.get('/image-make/config', requireAdmin, async (_req, res) => { if (!imageMakeAdminConfigService?.getAdminConfig) { return res.status(503).json({ message: 'image_make 配置服务未启用' }); } return res.json(await imageMakeAdminConfigService.getAdminConfig()); }); const updateImageMakeConfig = async (req, res) => { if (!imageMakeAdminConfigService?.updateAdminConfig) { return res.status(503).json({ message: 'image_make 配置服务未启用' }); } const result = await imageMakeAdminConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id, }); if (result.ok === false) return res.status(400).json({ message: result.message }); return res.json(result); }; adminApi.put('/image-make/config', requireAdmin, updateImageMakeConfig); adminApi.patch('/image-make/config', requireAdmin, updateImageMakeConfig); adminApi.get('/image-make/runtime', requireAdmin, async (_req, res) => { if (!imageMakeAdminConfigService?.getRuntimeConfig) { return res.status(503).json({ message: 'image_make 配置服务未启用' }); } const runtime = await imageMakeAdminConfigService.getRuntimeConfig(); if (!runtime.ok) return res.status(503).json({ message: runtime.message ?? 'image_make 运行时配置无效' }); const { providers, ...rest } = runtime; return res.json({ ...rest, providers: { mock: { enabled: providers.mock.enabled }, aliyun_bailian: providers.aliyun_bailian.enabled ? { enabled: true, model: providers.aliyun_bailian.model, apiBase: providers.aliyun_bailian.apiBase, apiKeyConfigured: Boolean(providers.aliyun_bailian.apiKey), } : { enabled: false }, comfyui: providers.comfyui.enabled ? { enabled: true, ...providers.comfyui } : { enabled: false }, }, }); }); adminApi.get('/memory-v2/config', requireAdmin, async (_req, res) => { if (!memoryV2ConfigService?.getAdminConfig) { return res.status(503).json({ message: 'Memory V2 配置服务未启用' }); } const result = await memoryV2ConfigService.getAdminConfig(); return res.json(result); }); const updateMemoryV2Config = async (req, res) => { if (!memoryV2ConfigService?.updateAdminConfig) { return res.status(503).json({ message: 'Memory V2 配置服务未启用' }); } const result = await memoryV2ConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id, }); return res.json(result); }; adminApi.put('/memory-v2/config', requireAdmin, updateMemoryV2Config); adminApi.patch('/memory-v2/config', requireAdmin, updateMemoryV2Config); adminApi.get('/mindsearch/config', requireAdmin, async (_req, res) => { if (!mindSearchConfigService?.getAdminConfig) return res.status(503).json({ message: 'MindSearch 配置服务未启用' }); return res.json(await mindSearchConfigService.getAdminConfig()); }); const updateMindSearchConfig = async (req, res) => { if (!mindSearchConfigService?.updateAdminConfig) return res.status(503).json({ message: 'MindSearch 配置服务未启用' }); return res.json(await mindSearchConfigService.updateAdminConfig(req.body ?? {}, { updatedBy: req.currentUser.id })); }; adminApi.put('/mindsearch/config', requireAdmin, updateMindSearchConfig); adminApi.patch('/mindsearch/config', requireAdmin, updateMindSearchConfig); adminApi.get('/mindsearch/runtime', requireAdmin, async (_req, res) => { if (!mindSearchConfigService?.getRuntimeState) return res.status(503).json({ message: 'MindSearch 配置服务未启用' }); return res.json(await mindSearchConfigService.getRuntimeState()); }); adminApi.get('/memory-v2/runtime', requireAdmin, async (_req, res) => { if (!memoryV2ConfigService?.getRuntimeState) { return res.status(503).json({ message: 'Memory V2 配置服务未启用' }); } const result = await memoryV2ConfigService.getRuntimeState(); return res.json(result); }); adminApi.get('/skill-runtime/config', requireAdmin, async (_req, res) => { if (!skillRuntimeConfigService?.getAdminConfig) { return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' }); } return res.json(await skillRuntimeConfigService.getAdminConfig()); }); const updateSkillRuntimeConfig = async (req, res) => { if (!skillRuntimeConfigService?.updateAdminConfig) { return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' }); } const result = await skillRuntimeConfigService.updateAdminConfig(req.body?.config ?? req.body ?? {}, { updatedBy: req.currentUser.id, }); return res.json(result); }; adminApi.put('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig); adminApi.patch('/skill-runtime/config', requireAdmin, updateSkillRuntimeConfig); adminApi.get('/skill-runtime/catalog', requireAdmin, async (_req, res) => { if (!skillRuntimeConfigService?.listCatalogSummary) { return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' }); } return res.json({ catalog: await skillRuntimeConfigService.listCatalogSummary() }); }); adminApi.get('/skill-runtime/runtime', requireAdmin, async (_req, res) => { if (!skillRuntimeConfigService?.getPublicRuntimeConfig) { return res.status(503).json({ message: 'Skill Runtime 配置服务未启用' }); } return res.json(await skillRuntimeConfigService.getPublicRuntimeConfig()); }); adminApi.post('/system-tests/skill-validation', requireAdmin, async (req, res) => { if (!adminSystemTestService?.runSkillValidation) { return res.status(503).json({ message: '系统测试服务未启用' }); } const username = String(req.body?.username ?? '').trim(); const password = String(req.body?.password ?? ''); const skillName = String(req.body?.skillName ?? 'service-integration-smoke').trim(); if (!username || !password) { return res.status(400).json({ message: '请输入测试账号和密码' }); } const result = await adminSystemTestService.runSkillValidation({ username, password, skillName }); return res.json(result); }); adminApi.post('/users', requireAdmin, async (req, res) => { const result = await userAuth.createUser(req.body ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.status(201).json({ user: result.user }); }); adminApi.patch('/users/:userId', requireAdmin, async (req, res) => { const result = await userAuth.updateUser(req.params.userId, req.body ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.json({ user: result.user }); }); adminApi.post('/users/:userId/recharge', requireAdmin, async (req, res) => { const amountCents = Number(req.body?.amountCents); const note = typeof req.body?.note === 'string' ? req.body.note : ''; const result = await userAuth.recharge( req.params.userId, amountCents, req.currentUser.id, note, ); if (!result.ok) return res.status(400).json({ message: result.message }); res.json({ user: result.user }); }); adminApi.get('/usage', requireAdmin, async (req, res) => { const userId = typeof req.query.userId === 'string' ? req.query.userId : null; const { page, pageSize } = req.query; const result = await userAuth.listUsageRecords({ userId, page, pageSize }); res.json(result); }); adminApi.get('/capabilities/catalog', requireAdmin, (_req, res) => { res.json({ catalog: userAuth.capabilityCatalog }); }); adminApi.get('/capabilities/role/:role', requireAdmin, async (req, res) => { const role = req.params.role === 'admin' ? 'admin' : 'user'; if (role === 'admin') { return res.json({ role, capabilities: Object.fromEntries( userAuth.capabilityCatalog.map((item) => [item.key, true]), ), unrestricted: true, }); } res.json(await userAuth.getRoleCapabilities('user')); }); adminApi.put('/capabilities/role/:role', requireAdmin, async (req, res) => { const role = req.params.role === 'admin' ? 'admin' : 'user'; const result = await userAuth.setRoleCapabilities(role, req.body?.capabilities ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.get('/users/:userId/capabilities', requireAdmin, async (req, res) => { const result = await userAuth.getUserCapabilities(req.params.userId); if (!result.ok) return res.status(404).json({ message: result.message }); res.json(result); }); adminApi.put('/users/:userId/capabilities', requireAdmin, async (req, res) => { const result = await userAuth.setUserCapabilities( req.params.userId, req.body?.capabilities ?? {}, ); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.delete('/users/:userId/capabilities', requireAdmin, async (req, res) => { const result = await userAuth.clearUserCapabilityOverrides(req.params.userId); if (!result.ok) return res.status(404).json({ message: result.message }); res.json(result); }); adminApi.get('/policies/catalog', requireAdmin, (_req, res) => { res.json({ catalog: userAuth.policyCatalog }); }); adminApi.get('/policies/role/:role', requireAdmin, async (req, res) => { const role = req.params.role === 'admin' ? 'admin' : 'user'; if (role === 'admin') { return res.json({ role, policies: {}, unrestricted: true }); } res.json(await userAuth.getRolePolicies('user')); }); adminApi.put('/policies/role/:role', requireAdmin, async (req, res) => { const role = req.params.role === 'admin' ? 'admin' : 'user'; const result = await userAuth.setRolePolicies(role, req.body?.policies ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.get('/users/:userId/policies', requireAdmin, async (req, res) => { const result = await userAuth.getUserPolicies(req.params.userId); if (!result.ok) return res.status(404).json({ message: result.message }); res.json(result); }); adminApi.put('/users/:userId/policies', requireAdmin, async (req, res) => { const result = await userAuth.setUserPolicies(req.params.userId, req.body?.policies ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.delete('/users/:userId/policies', requireAdmin, async (req, res) => { const result = await userAuth.clearUserPolicyOverrides(req.params.userId); if (!result.ok) return res.status(404).json({ message: result.message }); res.json(result); }); adminApi.get('/skills/catalog', requireAdmin, (_req, res) => { res.json({ catalog: userAuth.skillCatalog }); }); adminApi.get('/skills/role/:role', requireAdmin, async (req, res) => { const role = req.params.role === 'admin' ? 'admin' : 'user'; if (role === 'admin') { return res.json({ role, skills: Object.fromEntries(userAuth.skillCatalog.map((item) => [item.name, true])), }); } res.json(await userAuth.getRoleSkills('user')); }); adminApi.put('/skills/role/:role', requireAdmin, async (req, res) => { const role = req.params.role === 'admin' ? 'admin' : 'user'; const result = await userAuth.setRoleSkills(role, req.body?.skills ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.get('/users/:userId/skills', requireAdmin, async (req, res) => { const result = await userAuth.getUserSkills(req.params.userId); if (!result.ok) return res.status(404).json({ message: result.message }); res.json(result); }); adminApi.put('/users/:userId/skills', requireAdmin, async (req, res) => { const result = await userAuth.setUserSkills(req.params.userId, req.body?.skills ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.delete('/users/:userId/skills', requireAdmin, async (req, res) => { const result = await userAuth.clearUserSkillOverrides(req.params.userId); if (!result.ok) return res.status(404).json({ message: result.message }); res.json(result); }); adminApi.get('/ledger', requireAdmin, async (req, res) => { const userId = typeof req.query.userId === 'string' ? req.query.userId : null; const { page, pageSize } = req.query; const result = await userAuth.listBillingLedger({ userId, page, pageSize }); res.json(result); }); adminApi.get('/wechat/summary', requireAdmin, async (_req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); res.json(await wechatAdmin.getSummary()); }); adminApi.get('/wechat/bindings', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); res.json(await wechatAdmin.listBindings(req.query)); }); adminApi.get('/wechat/messages', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); res.json(await wechatAdmin.listMessages(req.query)); }); adminApi.get('/wechat/digests', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); res.json(await wechatAdmin.listDigests(req.query)); }); adminApi.get('/wechat/deliveries', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); res.json(await wechatAdmin.listDeliveries(req.query)); }); adminApi.get('/wechat/balance-alerts', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); res.json(await wechatAdmin.listBalanceAlerts(req.query)); }); adminApi.post('/wechat/balance-alerts', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); const result = await wechatAdmin.createBalanceAlert(req.body ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.status(201).json(result); }); adminApi.post('/wechat/balance-alerts/:id/cancel', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); const result = await wechatAdmin.cancelBalanceAlert(req.params.id); if (!result.ok) return res.status(404).json({ message: result.message ?? '取消失败' }); res.json(result); }); adminApi.post('/wechat/balance-alerts/:id/resume', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); const result = await wechatAdmin.resumeBalanceAlert(req.params.id); if (!result.ok) return res.status(404).json({ message: result.message ?? '恢复失败' }); res.json(result); }); adminApi.get('/wechat/web-notifications', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); res.json(await wechatAdmin.listWebNotifications(req.query)); }); adminApi.post('/wechat/web-notifications', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); const result = await wechatAdmin.createWebNotification(req.body ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.status(201).json(result); }); adminApi.post('/wechat/users/:userId/route/clear', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); const result = await wechatAdmin.clearRouteForUser(req.params.userId); if (!result.ok) return res.status(404).json({ message: result.message ?? '清除失败' }); res.json(result); }); adminApi.post('/wechat/digests/:id/cancel', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); const result = await wechatAdmin.cancelDigest(req.params.id); if (!result.ok) return res.status(404).json({ message: '订阅不存在或已暂停' }); res.json(result); }); adminApi.post('/wechat/digests/:id/resume', requireAdmin, async (req, res) => { if (!wechatAdmin) return res.status(503).json({ message: '服务号管理未启用' }); const result = await wechatAdmin.resumeDigest(req.params.id); if (!result.ok) return res.status(404).json({ message: result.message ?? '恢复失败' }); res.json(result); }); adminApi.get('/llm-providers/catalog', requireAdmin, (_req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); res.json({ catalog: llmProviderService.catalog }); }); adminApi.get('/llm-providers/keys', requireAdmin, async (_req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); const keys = await llmProviderService.listKeys(); res.json({ keys }); }); adminApi.post('/llm-providers/keys', requireAdmin, async (req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); const result = await llmProviderService.createKey(req.body ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.status(201).json({ key: result.key }); }); adminApi.patch('/llm-providers/keys/:keyId', requireAdmin, async (req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); const result = await llmProviderService.updateKey(req.params.keyId, req.body ?? {}); if (!result.ok) return res.status(400).json({ message: result.message }); res.json({ key: result.key }); }); adminApi.post('/llm-providers/keys/:keyId/select', requireAdmin, async (req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); const result = await llmProviderService.selectKey(req.params.keyId); if (!result.ok) return res.status(400).json({ message: result.message }); res.json({ key: result.key }); }); adminApi.delete('/llm-providers/keys/:keyId', requireAdmin, async (req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); const result = await llmProviderService.deleteKey(req.params.keyId); if (!result.ok) return res.status(400).json({ message: result.message }); res.json({ ok: true }); }); adminApi.post('/llm-providers/sync', requireAdmin, async (_req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); try { const result = await llmProviderService.syncSelectedToGoosed(); res.json(result); } catch (err) { res.status(500).json({ message: err instanceof Error ? err.message : '同步失败', }); } }); adminApi.get('/llm-providers/global', requireAdmin, async (_req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); const global = await llmProviderService.getGlobalSettings(); res.json({ global }); }); adminApi.put('/llm-providers/global', requireAdmin, async (req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); const result = await llmProviderService.setGlobalModel(req.body?.model); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.post('/llm-providers/test', requireAdmin, async (req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); try { const result = await llmProviderService.testDraft(req.body ?? {}); res.json(result); } catch (err) { res.status(500).json({ ok: false, message: err instanceof Error ? err.message : '联通测试失败', }); } }); adminApi.post('/llm-providers/keys/:keyId/test', requireAdmin, async (req, res) => { if (!llmProviderService) return res.status(503).json({ message: '未启用 LLM 配置' }); try { const result = await llmProviderService.testKey(req.params.keyId, req.body?.model); res.json(result); } catch (err) { res.status(500).json({ ok: false, message: err instanceof Error ? err.message : '联通测试失败', }); } }); adminApi.get('/plaza/pending', requireAdmin, async (_req, res) => { if (!plazaPosts) return res.status(503).json({ message: 'Plaza 未启用' }); const posts = await plazaPosts.listPendingPosts(); res.json({ data: { posts } }); }); adminApi.post('/plaza/posts/:id/review', requireAdmin, async (req, res) => { if (!plazaPosts) return res.status(503).json({ message: 'Plaza 未启用' }); try { const action = String(req.body?.action ?? 'approve'); const reason = req.body?.reason ?? null; const result = plazaOps && req.currentUser?.id ? await plazaOps.reviewPostAsOps(req.currentUser.id, req.params.id, action, { reason }) : await plazaPosts.reviewPost(req.params.id, action, { reason }); res.json({ data: result }); } catch (error) { const status = mapPlazaError(error); res.status(status).json({ error: { code: error?.code ?? 'review_failed', message: error.message }, }); } }); // ── Subscription management ─────────────────────────────────────────────── adminApi.get('/subscriptions', requireAdmin, async (req, res) => { if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' }); const { userId, status, page, pageSize } = req.query; const result = await subscriptionService.listSubscriptions({ userId, status, page, pageSize }); res.json(result); }); adminApi.get('/subscriptions/plans', requireAdmin, (_req, res) => { res.json( Object.entries(PLAN_CATALOG).map(([planType, def]) => ({ planType, ...def, approxCalls: tokensToCallsApprox(def.periodTokens), })), ); }); adminApi.get('/users/:userId/subscription', requireAdmin, async (req, res) => { if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' }); const sub = await subscriptionService.getActiveSubscription(req.params.userId); res.json({ subscription: sub }); }); adminApi.post('/users/:userId/subscription', requireAdmin, async (req, res) => { if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' }); const { planType, durationDays, note } = req.body ?? {}; if (!planType) return res.status(400).json({ message: '缺少 planType' }); const result = await subscriptionService.grantSubscription( req.params.userId, planType, durationDays ? Number(durationDays) : undefined, req.currentUser.id, note, ); if (!result.ok) return res.status(400).json({ message: result.message }); res.json(result); }); adminApi.delete('/users/:userId/subscription', requireAdmin, async (req, res) => { if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' }); const result = await subscriptionService.cancelSubscription( req.params.userId, req.currentUser.id, ); res.json(result); }); adminApi.post('/subscriptions/expire-stale', requireAdmin, async (_req, res) => { if (!subscriptionService) return res.status(503).json({ message: '订阅服务未启用' }); const count = await subscriptionService.expireStaleSubscriptions(); res.json({ expiredCount: count }); }); return adminApi; } /** * Plaza operations console API (ops_role: reviewer < editor < ops_admin). * @param {object} deps * @param {import('express').RequestHandler} deps.jsonBody * @param {object|null} deps.plazaOps */ export function createOpsApi({ jsonBody, plazaOps }) { function makeRequireOps(minRole = 'reviewer') { return async (req, res, next) => { if (!plazaOps) return sendError(res, req, 503, 'plaza_unavailable', 'Plaza 未启用'); if (!req.currentUser) return sendError(res, req, 401, 'unauthorized', '未授权,请重新登录'); const role = await plazaOps.loadOperatorRole(req.currentUser.id); if (!hasOpsRole(role, minRole)) { return sendError(res, req, 403, 'OPS_PERMISSION_DENIED', '无运营权限'); } req.opsRole = role; return next(); }; } const opsApi = express.Router(); opsApi.use(jsonBody); opsApi.use(makeRequireOps('reviewer')); opsApi.get('/categories', async (req, res) => { try { return sendData(res, req, { categories: await plazaOps.listCategories() }); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.get('/review/queue', async (req, res) => { try { const queue = await plazaOps.listReviewQueue({ status: req.query.status ?? 'pending_review', cursor: req.query.cursor ?? null, limit: req.query.limit, keyword: req.query.keyword ?? null, category: req.query.category ?? null, }); return sendData(res, req, queue); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.post('/review/posts/:id', async (req, res) => { try { const result = await plazaOps.reviewPostAsOps(req.currentUser.id, req.params.id, req.body?.action, { reason: req.body?.reason ?? null, }); return sendData(res, req, { post: result }); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.post('/review/batch', makeRequireOps('editor'), async (req, res) => { try { const result = await plazaOps.batchReviewPosts(req.currentUser.id, req.body ?? {}); return sendData(res, req, result); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.get('/reports', async (req, res) => { try { return sendData(res, req, await plazaOps.listReports({ status: req.query.status ?? 'pending' })); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.post('/reports/:id/process', async (req, res) => { try { const result = await plazaOps.processReport(req.currentUser.id, req.params.id, req.body ?? {}); return sendData(res, req, { report: result }); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.get('/featured', makeRequireOps('editor'), async (req, res) => { try { return sendData(res, req, await plazaOps.listFeatured()); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.post('/featured', makeRequireOps('editor'), async (req, res) => { try { const result = await plazaOps.setFeatured(req.currentUser.id, req.body ?? {}); return sendData(res, req, { featured: result }, 201); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.delete('/featured/:id', makeRequireOps('editor'), async (req, res) => { try { const result = await plazaOps.removeFeatured(req.currentUser.id, req.params.id); return sendData(res, req, { featured: result }); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.get('/analytics/overview', makeRequireOps('ops_admin'), async (req, res) => { try { return sendData(res, req, await plazaOps.getAnalyticsOverview()); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.get('/creators', async (req, res) => { try { return sendData(res, req, await plazaOps.listCreators({ keyword: req.query.keyword ?? null })); } catch (error) { return plazaRouteError(res, req, error); } }); opsApi.patch('/creators/:userId', async (req, res) => { try { const result = await plazaOps.updateCreator(req.currentUser.id, req.params.userId, req.body ?? {}); return sendData(res, req, { creator: result }); } catch (error) { return plazaRouteError(res, req, error); } }); return opsApi; }