diff --git a/README.md b/README.md index c03d264b..cb3904ac 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - 租户后台能力:品牌、主题模板/草稿/发布、域名、公开设置、支付账户、登录配置、私密密钥掩码、活动内容、考试日期、题目反馈处理、用户站内通知查看、激活码、优惠券规则/核销报表、勋章管理/手动发放/签到积分反馈自动发放、成员权限、自定义角色模板、班级/教师/学生范围权限、学生批量导入、批量分班、学生备注、跟进任务、审计日志。 - 租户内容能力:可配置题库入口、任意深度分类树、考试意向标记、题目集合、顺序/随机/全真模拟蓝图、题目录入/更新、视频绑定、分数线、单词、知识手册、资料资源台账、题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 批量导入。 - 学生端能力:题库入口、分类树、题目集合、顺序/随机/模考 session 组卷快照、答题、错题本、收藏夹、背单词卡片学习/发音/收藏练习、个人中心、站内通知、勋章、考试倒计时、签到积分、积分活动任务、积分兑换、题目反馈、排行榜、分数线、AI 择校推荐、题目视频、订单详情/状态轮询、优惠券领取/抵扣、权益、激活码预检查/兑换、资料下载;签到、积分阈值、反馈解决和积分活动可返回自动获得勋章结果,反馈处理/奖励、勋章发放和积分兑换会写入用户站内通知。 -- 平台后台能力:租户管理、租户详情、账务资料维护、平台审计日志、SaaS 套餐、订阅、订阅账单候选预览/dry-run/批量生成、自动计费 worker、账单、服务费收款、逾期标记、内部催缴台账、用量记录、公共题库授权。 +- 平台后台能力:租户管理、租户详情、账务资料维护、平台审计日志查询和 CSV/JSON 导出、SaaS 套餐、订阅、订阅账单候选预览/dry-run/批量生成、自动计费 worker、账单、服务费收款、逾期标记、内部催缴台账、用量记录、公共题库授权。 - 公共题库商业化能力:租户可采纳平台授权题库为本租户副本,并可手动或由 worker 自动同步平台新增/更新题目;同步会保护租户自改题目,返回冲突而不覆盖,后台可查询冲突明细。 - 题库导出能力:租户内容编辑可按题目集合、内容入口或分类节点导出 JSON、`paper_json`、打印 payload、PDF、Word 和每日一练图片 ZIP 素材包,后端强制租户隔离、答案/解析开关、复合题子题脱敏、导出 job 和审计;PDF/Word/ZIP 由 exports worker 生成水印文件或运营素材并发布到 `content_assets`;`daily_practice` 支持每日一练九宫格 metadata、PDF/Word 版式、9 张 PNG/SVG 卡片和拼图包。 - 销售/代理/CRM 增长链路:邀请码、扫码/分享事件、首绑客资保护、销售统计、团队关系、CRM 配置、跟进分配策略和队列。 @@ -35,7 +35,7 @@ - 题库导出已完成服务端结构化 payload、PDF/Word 二进制 worker、每日一练基础导出和每日一练 ZIP 图片素材包;后续还要补更精细试卷模板、多模板排版和导出操作台体验。 - 优惠券复杂规则和核销报表已可联调,包含状态启停、活动分组、最低订单金额、优惠封顶、单用户限次、首单限制、适用套餐/地区、核销明细和活动报表;Taro 租户营销中心已接优惠券规则表单、筛选、核销明细和报表第一版。 - 勋章管理、手动发放、签到连续天数、积分阈值、反馈解决和积分活动任务自动发放已可联调;积分活动任务、积分兑换商品、兑换订单、优惠券兑换履约、租户后台配置和用户站内通知第一版已完成,Taro 学生个人中心已接积分任务/兑换/积分明细和消息中心第一版,租户营销中心已接积分任务/兑换操作台和用户通知查看第一版。后续还要补连续签到奖励规则、练习次数/单词掌握/模考成绩系统触发勋章、外部微信订阅消息/短信推送、积分风控报表、分佣真实打款 provider、发票、批量凭证上传、CRM 富卡片模板、失败告警、死信运营台、销售转化看板、公共题库版本通知和冲突处理操作台。 -- `apps/taro` 已建立 Taro 4 React 跨端前端地基,包含 H5 学生端、租户后台、平台后台三套构建入口、租户解析、统一 API client 和 Supabase Auth client 初始化;学生端第一批页面已接入登录、首页、题库、练习、背单词、知识手册、分数线、AI 择校推荐、资料和个人中心,已新增 `RichContent` 安全渲染组件用于题干、选项、解析、知识手册和逐题复盘,H5 端已用 KaTeX 渲染 `$...$`、`$$...$$`、`\(...\)`、`\[...\]` 公式,私有题图可用 `asset:`/`content_asset:` 资源引用走短期预览签名,已升级背单词为今日计划/单元学习/收藏练习、卡片翻转、发音、美/英音切换和本地位置恢复第一版,资料页已补齐预览/下载的短签名、水印 traceId 和强制水印容器第一版,个人中心已接学习报告、14 天趋势、题型表现、最近练习、7 日答题榜当前排名、积分任务/兑换/积分明细和消息中心第一版;租户后台第一批页面已接入工作台、数据看板、学生/班级、题库内容、营销中心、财务运营和租户设置,营销中心已接 CRM、分佣结算、优惠券规则/核销报表、积分任务/兑换操作台和用户通知查看第一版,财务运营已接退款状态机、官方账单任务、对账异常、差错工单和调整凭证第一版,设置页已接主题模板、草稿预览/发布、角色模板和成员绑定第一版;平台后台已接入工作台、租户管理、账务中心、公共题库授权,以及创建租户、租户详情、状态变更、账务资料维护、平台审计、订阅、订阅账单候选/dry-run/批量生成、自动计费 worker 生成结果查看、收款、逾期预览/催缴记录、用量和题库授权第一版写操作。 +- `apps/taro` 已建立 Taro 4 React 跨端前端地基,包含 H5 学生端、租户后台、平台后台三套构建入口、租户解析、统一 API client 和 Supabase Auth client 初始化;学生端第一批页面已接入登录、首页、题库、练习、背单词、知识手册、分数线、AI 择校推荐、资料和个人中心,已新增 `RichContent` 安全渲染组件用于题干、选项、解析、知识手册和逐题复盘,H5 端已用 KaTeX 渲染 `$...$`、`$$...$$`、`\(...\)`、`\[...\]` 公式,私有题图可用 `asset:`/`content_asset:` 资源引用走短期预览签名,已升级背单词为今日计划/单元学习/收藏练习、卡片翻转、发音、美/英音切换和本地位置恢复第一版,资料页已补齐预览/下载的短签名、水印 traceId 和强制水印容器第一版,个人中心已接学习报告、14 天趋势、题型表现、最近练习、7 日答题榜当前排名、积分任务/兑换/积分明细和消息中心第一版;租户后台第一批页面已接入工作台、数据看板、学生/班级、题库内容、营销中心、财务运营和租户设置,营销中心已接 CRM、分佣结算、优惠券规则/核销报表、积分任务/兑换操作台和用户通知查看第一版,财务运营已接退款状态机、官方账单任务、对账异常、差错工单和调整凭证第一版,设置页已接主题模板、草稿预览/发布、角色模板和成员绑定第一版;平台后台已接入工作台、租户管理、账务中心、公共题库授权,以及创建租户、租户详情、状态变更、账务资料维护、平台审计查询/CSV 导出、订阅、订阅账单候选/dry-run/批量生成、自动计费 worker 生成结果查看、收款、逾期预览/催缴记录、用量和题库授权第一版写操作。 - 根目录已清理为新 Supabase SaaS monorepo 编排层;旧 PocketBase/React 项目和旧构建产物仅保留在 `参考/` 目录作为迁移参考,不进入 Git 提交。 更完整的进度看这些文档: diff --git a/apps/api/src/features/platform-admin/index.ts b/apps/api/src/features/platform-admin/index.ts index 436b9e3f..09e997ce 100644 --- a/apps/api/src/features/platform-admin/index.ts +++ b/apps/api/src/features/platform-admin/index.ts @@ -7,6 +7,7 @@ import { createTenantInvoicesBatchFromSubscriptionsRoute, createTenantRoute, invoiceRemindersRoute, + platformAuditLogsExportRoute, platformAuditLogsRoute, platformOverviewRoute, platformPlansRoute, @@ -36,6 +37,7 @@ export const platformAdminRoutes: RouteDefinition[] = [ ['PATCH', '/api/platform-admin/tenants/status', updateTenantStatusRoute], ['PUT', '/api/platform-admin/tenants/billing-profile', upsertBillingProfileRoute], ['GET', '/api/platform-admin/audit-logs', platformAuditLogsRoute], + ['GET', '/api/platform-admin/audit-logs/export', platformAuditLogsExportRoute], ['POST', '/api/platform-admin/subscriptions', createSubscriptionRoute], ['GET', '/api/platform-admin/invoices', tenantInvoicesRoute], ['POST', '/api/platform-admin/invoices', createInvoiceRoute], diff --git a/apps/api/src/features/platform-admin/routes.ts b/apps/api/src/features/platform-admin/routes.ts index c153b084..9ad4385b 100644 --- a/apps/api/src/features/platform-admin/routes.ts +++ b/apps/api/src/features/platform-admin/routes.ts @@ -1,3 +1,4 @@ +import crypto from 'node:crypto'; import type pg from 'pg'; import { currentSessionFromContext } from '../../core/auth-context.js'; import { getHeader, HttpError, type RequestContext } from '../../core/http.js'; @@ -43,6 +44,67 @@ function optionalUuidArray(body: Record, key: string) { const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; const TENANT_INVOICE_STATUSES = new Set(['draft', 'issued', 'paid', 'void', 'overdue']); +function csvEscape(value: unknown) { + if (value === null || value === undefined) return ''; + const text = typeof value === 'object' ? JSON.stringify(value) : String(value); + if (!/[",\r\n]/.test(text)) return text; + return `"${text.replace(/"/g, '""')}"`; +} + +function csvLine(values: unknown[]) { + return values.map(csvEscape).join(','); +} + +function redactAuditExportValue(value: unknown, parentKey = '', depth = 0): unknown { + if (value === null || value === undefined) return value; + if (depth > 8) return '[REDACTED_DEPTH_LIMIT]'; + if ( + /(?:password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|app[_-]?secret|authorization|cookie|session|cert|signature|nonce)$/i + .test(parentKey) + ) { + return '[REDACTED]'; + } + if (Array.isArray(value)) return value.map(item => redactAuditExportValue(item, parentKey, depth + 1)); + if (typeof value === 'object') { + const output: Record = {}; + for (const [key, item] of Object.entries(value as Record)) { + output[key] = redactAuditExportValue(item, key, depth + 1); + } + return output; + } + return value; +} + +function contentBase64AndHash(content: string) { + const buffer = Buffer.from(content, 'utf8'); + return { + contentBase64: buffer.toString('base64'), + sha256: crypto.createHash('sha256').update(buffer).digest('hex'), + sizeBytes: buffer.length, + }; +} + +function platformExportTimestamp() { + return new Date().toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z'); +} + +function optionalDateFilter(ctx: RequestContext, name: string) { + const value = listQuery(ctx, name); + if (!value) return ''; + if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) { + throw new HttpError(400, `${name} must use YYYY-MM-DD format`, 'INVALID_DATE'); + } + return value; +} + +function auditExportFormat(value: string) { + const format = value || 'csv'; + if (!['csv', 'json'].includes(format)) { + throw new HttpError(400, 'format is invalid', 'INVALID_EXPORT_FORMAT'); + } + return format; +} + function optionalUuidList(value: unknown, key = 'ids', maxLength = 500) { const values = Array.isArray(value) ? value.map(item => String(item).trim()).filter(Boolean) @@ -97,6 +159,120 @@ function requestIp(ctx: RequestContext) { return forwarded || ctx.req.socket.remoteAddress || null; } +function auditLogQueryParams(ctx: RequestContext, defaultLimit = 100, maxLimit = 500) { + const tenantId = listQuery(ctx, 'tenantId'); + const action = listQuery(ctx, 'action'); + const targetType = listQuery(ctx, 'targetType'); + const actorUserId = listQuery(ctx, 'actorUserId'); + const q = listQuery(ctx, 'q'); + const startDate = optionalDateFilter(ctx, 'startDate'); + const endDate = optionalDateFilter(ctx, 'endDate'); + if (tenantId && !UUID_RE.test(tenantId)) throw new HttpError(400, 'tenantId is invalid', 'INVALID_UUID'); + if (actorUserId && !UUID_RE.test(actorUserId)) throw new HttpError(400, 'actorUserId is invalid', 'INVALID_UUID'); + if (startDate && endDate && startDate > endDate) throw new HttpError(400, 'startDate must be before endDate', 'INVALID_DATE_RANGE'); + + const limit = intParam(ctx, 'limit', defaultLimit, maxLimit); + const params: unknown[] = []; + const filters: string[] = []; + + if (tenantId) { + params.push(tenantId); + filters.push(`al.tenant_id = $${params.length}::uuid`); + } + if (action) { + params.push(`${action}%`); + filters.push(`al.action ilike $${params.length}`); + } + if (targetType) { + params.push(targetType); + filters.push(`al.target_type = $${params.length}`); + } + if (actorUserId) { + params.push(actorUserId); + filters.push(`al.actor_user_id = $${params.length}::uuid`); + } + if (startDate) { + params.push(startDate); + filters.push(`al.created_at >= $${params.length}::date`); + } + if (endDate) { + params.push(endDate); + filters.push(`al.created_at < ($${params.length}::date + interval '1 day')`); + } + if (q) { + params.push(`%${q}%`); + filters.push(`(al.action ilike $${params.length} or al.target_type ilike $${params.length} or al.target_id ilike $${params.length})`); + } + + return { + filters, + params, + limit, + summary: { tenantId, action, targetType, actorUserId, q, startDate, endDate }, + }; +} + +async function loadPlatformAuditLogs(input: { + filters: string[]; + params: unknown[]; + limit: number; +}) { + const params = [...input.params, input.limit]; + return query<{ + id: string; + tenantId: string | null; + tenantSlug: string | null; + tenantName: string | null; + actorUserId: string | null; + actorUsername: string | null; + actorName: string | null; + actorPhone: string | null; + action: string; + targetType: string | null; + targetId: string | null; + details: Record | null; + ipAddress: string | null; + userAgent: string | null; + createdAt: string; + }>( + ` + select al.id, al.tenant_id as "tenantId", t.slug::text as "tenantSlug", + t.name as "tenantName", al.actor_user_id as "actorUserId", + u.username as "actorUsername", u.name as "actorName", + u.phone as "actorPhone", al.action, + al.target_type as "targetType", al.target_id as "targetId", + al.details, al.ip_address as "ipAddress", al.user_agent as "userAgent", + al.created_at as "createdAt" + from public.audit_logs al + left join public.tenants t on t.id = al.tenant_id + left join public.platform_users u on u.id = al.actor_user_id + ${input.filters.length ? `where ${input.filters.join(' and ')}` : ''} + order by al.created_at desc + limit $${params.length} + `, + params, + ); +} + +function platformAuditExportItems(items: Awaited>) { + return items.map(item => ({ + id: item.id, + tenantId: item.tenantId, + tenantSlug: item.tenantSlug, + tenantName: item.tenantName, + actorUserId: item.actorUserId, + actorUsername: item.actorUsername, + actorName: item.actorName, + action: item.action, + targetType: item.targetType, + targetId: item.targetId, + details: redactAuditExportValue(item.details), + ipAddress: item.ipAddress, + userAgent: item.userAgent, + createdAt: item.createdAt, + })); +} + async function recordPlatformAudit( client: pg.PoolClient, ctx: RequestContext, @@ -577,60 +753,82 @@ export async function tenantDetailRoute(ctx: RequestContext) { export async function platformAuditLogsRoute(ctx: RequestContext) { await requirePlatformAdmin(ctx); - const tenantId = listQuery(ctx, 'tenantId'); - const action = listQuery(ctx, 'action'); - const targetType = listQuery(ctx, 'targetType'); - const actorUserId = listQuery(ctx, 'actorUserId'); - const q = listQuery(ctx, 'q'); - const limit = intParam(ctx, 'limit', 100, 500); - const params: unknown[] = []; - const filters: string[] = []; - - if (tenantId) { - params.push(tenantId); - filters.push(`al.tenant_id = $${params.length}::uuid`); - } - if (action) { - params.push(`${action}%`); - filters.push(`al.action ilike $${params.length}`); - } - if (targetType) { - params.push(targetType); - filters.push(`al.target_type = $${params.length}`); - } - if (actorUserId) { - params.push(actorUserId); - filters.push(`al.actor_user_id = $${params.length}::uuid`); - } - if (q) { - params.push(`%${q}%`); - filters.push(`(al.action ilike $${params.length} or al.target_type ilike $${params.length} or al.target_id ilike $${params.length})`); - } - - params.push(limit); - - const items = await query( - ` - select al.id, al.tenant_id as "tenantId", t.slug::text as "tenantSlug", - t.name as "tenantName", al.actor_user_id as "actorUserId", - u.username as "actorUsername", u.name as "actorName", - u.phone as "actorPhone", al.action, - al.target_type as "targetType", al.target_id as "targetId", - al.details, al.ip_address as "ipAddress", al.user_agent as "userAgent", - al.created_at as "createdAt" - from public.audit_logs al - left join public.tenants t on t.id = al.tenant_id - left join public.platform_users u on u.id = al.actor_user_id - ${filters.length ? `where ${filters.join(' and ')}` : ''} - order by al.created_at desc - limit $${params.length} - `, - params, - ); + const auditQuery = auditLogQueryParams(ctx, 100, 500); + const items = await loadPlatformAuditLogs(auditQuery); return { items }; } +export async function platformAuditLogsExportRoute(ctx: RequestContext) { + await requirePlatformAdmin(ctx); + + const format = auditExportFormat(listQuery(ctx, 'format')); + const auditQuery = auditLogQueryParams(ctx, 1000, 5000); + const items = await loadPlatformAuditLogs(auditQuery); + const exportItems = platformAuditExportItems(items); + const exportedAt = new Date().toISOString(); + const filename = `platform-audit-${platformExportTimestamp()}.${format}`; + const content = format === 'json' + ? JSON.stringify({ exportedAt, filters: auditQuery.summary, items: exportItems }, null, 2) + : [ + csvLine([ + 'id', + 'tenantId', + 'tenantSlug', + 'tenantName', + 'actorUserId', + 'actorUsername', + 'actorName', + 'action', + 'targetType', + 'targetId', + 'details', + 'ipAddress', + 'userAgent', + 'createdAt', + ]), + ...exportItems.map(item => csvLine([ + item.id, + item.tenantId, + item.tenantSlug, + item.tenantName, + item.actorUserId, + item.actorUsername, + item.actorName, + item.action, + item.targetType, + item.targetId, + item.details, + item.ipAddress, + item.userAgent, + item.createdAt, + ])), + ].join('\n'); + const encoded = contentBase64AndHash(content); + + await transaction(async client => { + await recordPlatformAudit(client, ctx, 'platform.audit.exported', 'audit_logs', null, { + format, + filename, + rowCount: items.length, + sha256: encoded.sha256, + filters: auditQuery.summary, + }); + }); + + return { + item: { + filename, + format, + mimeType: format === 'json' ? 'application/json' : 'text/csv', + rowCount: items.length, + exportedAt, + filters: auditQuery.summary, + ...encoded, + }, + }; +} + export async function createTenantRoute(ctx: RequestContext) { await requirePlatformAdmin(ctx); diff --git a/apps/taro/src/pages/platform-admin/workbench/index.tsx b/apps/taro/src/pages/platform-admin/workbench/index.tsx index 9a5fa7a2..cbea4023 100644 --- a/apps/taro/src/pages/platform-admin/workbench/index.tsx +++ b/apps/taro/src/pages/platform-admin/workbench/index.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import Taro from '@tarojs/taro'; import { Button, Text, View } from '@tarojs/components'; import { + exportPlatformAuditLogs, loadPlatformAuditLogs, loadPlatformInvoices, loadPlatformOverview, @@ -21,6 +22,17 @@ function money(cents: unknown) { return `¥${(Number(cents || 0) / 100).toFixed(2)}`; } +function downloadBase64File(filename: string, contentBase64: string, mimeType: string) { + if (typeof document === 'undefined') return false; + const link = document.createElement('a'); + link.href = `data:${mimeType};base64,${contentBase64}`; + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); + return true; +} + export default function PlatformWorkbenchPage() { const [overview, setOverview] = useState(null); const [tenants, setTenants] = useState([]); @@ -29,6 +41,7 @@ export default function PlatformWorkbenchPage() { const [banks, setBanks] = useState([]); const [grants, setGrants] = useState([]); const [error, setError] = useState(''); + const [busy, setBusy] = useState(''); useEffect(() => { Promise.all([ @@ -54,6 +67,23 @@ export default function PlatformWorkbenchPage() { { name: '公共题库', path: '/pages/platform-admin/question-banks/index', meta: '地区题库、授权、披露范围' }, ]; + async function exportAuditLogs() { + setBusy('audit-export'); + setError(''); + try { + const payload = await exportPlatformAuditLogs({ format: 'csv', limit: 1000 }); + const item = payload.item; + if (item?.contentBase64 && item.filename) { + const ok = downloadBase64File(item.filename, item.contentBase64, item.mimeType || 'text/csv'); + Taro.showToast({ title: ok ? '已导出' : '已生成', icon: 'success' }); + } + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '审计导出失败'); + } finally { + setBusy(''); + } + } + return ( @@ -112,6 +142,9 @@ export default function PlatformWorkbenchPage() { 最近平台审计 + + + {auditLogs.map(item => ( diff --git a/apps/taro/src/services/platformAdmin.ts b/apps/taro/src/services/platformAdmin.ts index 9ff97e42..f87dee69 100644 --- a/apps/taro/src/services/platformAdmin.ts +++ b/apps/taro/src/services/platformAdmin.ts @@ -223,6 +223,18 @@ export interface PlatformAuditLogItem { createdAt?: string | null; } +export interface PlatformAuditExportItem { + filename?: string | null; + format?: string | null; + mimeType?: string | null; + rowCount?: number | string | null; + exportedAt?: string | null; + sha256?: string | null; + sizeBytes?: number | string | null; + contentBase64?: string | null; + filters?: Record | null; +} + export interface CreatePlatformTenantInput { slug: string; name: string; @@ -342,6 +354,23 @@ export async function loadPlatformAuditLogs(query: { tenantId?: string; action?: }); } +export async function exportPlatformAuditLogs(query: { + tenantId?: string; + action?: string; + targetType?: string; + actorUserId?: string; + q?: string; + startDate?: string; + endDate?: string; + format?: 'csv' | 'json'; + limit?: number; +} = {}) { + return apiRequest<{ item?: PlatformAuditExportItem }>('/api/platform-admin/audit-logs/export', { + query: { format: 'csv', ...query, limit: query.limit || 1000 }, + tenantId: null, + }); +} + export async function loadPlatformInvoices(query: { tenantId?: string; status?: string; limit?: number } = {}) { return apiRequest<{ items?: PlatformInvoiceItem[] }>('/api/platform-admin/invoices', { query: { ...query, limit: query.limit || 80 }, diff --git a/docs/refactor/backend-capability-status.md b/docs/refactor/backend-capability-status.md index 66b26ffd..5252bfa4 100644 --- a/docs/refactor/backend-capability-status.md +++ b/docs/refactor/backend-capability-status.md @@ -145,7 +145,7 @@ | 班级/学生/教师管理 | 可联调 | `/api/tenant-admin/classes`、`classes/members`、`students`、`teachers`,支持班级范围权限和审计 | | 学生批量运营 | 可联调 | `/api/tenant-admin/students/bulk-upsert`、`students/status`、`classes/members/bulk-assign`、`students/notes`、`students/followups`;支持逐行结果、限量、防跨租户和教师范围校验 | | 用户站内通知查看 | 可联调 | `GET /api/tenant-admin/user-notifications`;需要 `notifications:read` 权限,支持按用户、状态、类型查询租户内通知和状态汇总,租户后台只读不直接代学生改状态 | -| 平台租户/详情/账务资料/审计/套餐/订阅/账单/用量 | 可联调 | `/api/platform-admin/*`;已支持租户列表、创建租户、租户详情、状态变更、账务资料维护、平台审计日志、SaaS 套餐、订阅、账单、订阅账单候选预览、dry-run、批量生成、自动计费 worker、重复开票保护、收款、逾期标记、内部催缴台账和用量;创建租户、状态变更、账务资料维护、订阅批量开票、自动开票和逾期催缴会写入审计 | +| 平台租户/详情/账务资料/审计/套餐/订阅/账单/用量 | 可联调 | `/api/platform-admin/*`;已支持租户列表、创建租户、租户详情、状态变更、账务资料维护、平台审计日志查询、CSV/JSON 审计导出、SaaS 套餐、订阅、账单、订阅账单候选预览、dry-run、批量生成、自动计费 worker、重复开票保护、收款、逾期标记、内部催缴台账和用量;审计导出会对 `details` 中的 token/secret/password/key 等敏感字段递归脱敏,并写入 `platform.audit.exported`;创建租户、状态变更、账务资料维护、订阅批量开票、自动开票和逾期催缴会写入审计 | | 数据看板聚合接口 | 可联调 | `GET /api/tenant-admin/dashboard`;支持 `7d/30d/90d`、地区筛选、学生/学习/内容/订单/激活码/反馈卡片、趋势、24h 活跃、题型分布、科目排行、地区统计、套餐销量和运营动态 | | 平台公共题库授权 | 可联调 | `/api/platform-admin/question-banks`、`question-bank-grants`;支持按 SaaS 套餐、指定租户或全部活跃租户披露平台公共题库 | | 租户采纳/同步公共题库 | 可联调 | `/api/tenant-content/public-question-banks`、`public-question-banks/adopt`、`public-question-banks/sync`、`public-question-banks/conflicts`、`public-question-banks/conflicts/resolve`、`public-question-banks/conflicts/resolve-batch`、`tenant-content/notifications`;租户只能看到自己订阅/授权范围内题库,采纳后生成租户自己的题库、入口、集合和题目快照,可直接进入练习;平台更新后可手动或由 worker 自动同步,新增/更新和冲突会生成租户内容通知;租户自改题目会标记冲突并跳过;后台可查询最近一次冲突明细,并可单条或批量选择“采纳平台版本”/“保留本地版本”,操作会重新校验授权并写入逐条审计,冲突全部处理后相关通知自动 resolved | diff --git a/docs/refactor/backend-handoff-roadmap.md b/docs/refactor/backend-handoff-roadmap.md index 6b6eaed2..9c30cb86 100644 --- a/docs/refactor/backend-handoff-roadmap.md +++ b/docs/refactor/backend-handoff-roadmap.md @@ -21,7 +21,7 @@ | 模块 | 当前状态 | 已经具备 | 上线前还要补 | | --- | --- | --- | --- | | 多租户底座 | 可联调 | 租户、域名、品牌、设置、RLS 基础、审计、Supabase JWT/API 身份映射;`npm run test:rls` 已提供本地动态租户隔离验收;`npm run smoke:auth:remote` 已提供真实云端 Supabase access token 回归脚本 | 真实云端 Auth/JWKS 回归需要在预生产/生产环境执行并留档,生产 RLS 深测继续执行 | -| 平台后台 | 基础完成 | 租户、租户详情、账务资料维护、平台审计日志、套餐、订阅、订阅账单候选预览、dry-run、批量生成、自动计费 worker、重复开票保护、服务费、人工收款、逾期标记、内部催缴台账、用量、公共题库授权、公共题库自动同步 worker、公共题库冲突单条/批量处理 API、公共题库同步通知第一版 | 外部催缴通知、平台在线收款、平台审计报表增强、更完整运营消息 | +| 平台后台 | 基础完成 | 租户、租户详情、账务资料维护、平台审计日志查询、平台审计 CSV/JSON 导出、套餐、订阅、订阅账单候选预览、dry-run、批量生成、自动计费 worker、重复开票保护、服务费、人工收款、逾期标记、内部催缴台账、用量、公共题库授权、公共题库自动同步 worker、公共题库冲突单条/批量处理 API、公共题库同步通知第一版 | 外部催缴通知、平台在线收款、平台审计告警、更完整运营消息 | | 租户后台 | 可联调 | 品牌、域名、支付账户、登录配置、密钥掩码、活动、兑换码、优惠券、勋章管理/手动发放/签到/积分/反馈/活动自动发放、积分任务、积分兑换、用户站内通知查看、成员权限、角色模板、菜单/模块/字段权限配置 API、班级/教师/学生范围权限;Taro 工作台已接权限驱动模块入口,学生运营页已接学生创建/更新、禁用/恢复、批量导入、批量分班、备注和跟进任务第一版,租户设置页已接角色模板和成员绑定操作台第一版,营销中心已接 CRM 配置/队列、分佣结算、优惠券规则/核销报表、积分任务/兑换操作台和用户通知查看第一版 | 更细的数据范围组合、成员批量运营、真实打款/导出/凭证和完整权限菜单 | | 题库与练习 | 可联调 | 内容入口、任意深度分类、题目集合、顺序/随机/全真模拟蓝图、组卷快照、客观题后端判分、主观题 `selfJudgedCorrect` 自评、阅读理解/案例分析 `subAnswers` 多小题判分、答题、错题、收藏、模考报告、排行榜、公共题库采纳快照、手动同步、自动同步 worker、冲突查询/单条和批量处理 API、公共题库同步通知、JSON/试卷 payload 导出、PDF/Word 异步导出 worker、水印和资料发布路径、每日一练九宫格 metadata、PDF/Word 运营版式和 ZIP 图片素材包 | 长题干/公式图片混排体验、导出模板精排、导出操作台、排行榜防刷/预聚合 | | 背单词 | 可联调 | 单元、单词、进度、收藏、统计、每日计划、JSON/CSV/Excel 导入、排行榜 | 更细复习参数 | @@ -35,7 +35,7 @@ | 内容导入 | 可联调 | 题目、单词、知识手册、分数线、视频 JSON/CSV/Excel preview/import、issue、job/detail、审计、幂等、`executionMode=async`、imports worker、导入后复检、模板下载、字段映射 API、字段映射覆盖白名单校验、PocketBase JSON dry-run 报告;Taro 租户内容页已接上传/粘贴预览、模板文件下载、字段别名编辑、同步/异步执行、异步轮询和复检详情第一版 | 真实数据 dry-run 执行验收、抽样校验和导入性能压测 | | 数据看板 | 可联调 | 租户 dashboard 聚合接口,收益、注册、学习、内容、激活码、反馈、趋势、24h 活跃、套餐销量和运营动态 | 预聚合 worker、缓存、慢 SQL 监控和销售转化看板 | | AI 择校推荐 | 可联调 | `ai_recommendation_reports`、SVIP 门禁、学生输入 schema、地区/分数线上下文、`local_rules` 稳定 JSON、报告列表/详情和 Taro 学生端基础页 | 真实 AI provider、prompt 版本管理、租户后台配置、报告 PDF 渲染和人工复核流程 | -| Taro 前端 | 地基已建 | `apps/taro` 已有 Taro 4 React 工程、H5 三入口、租户解析、统一 API client、Supabase Auth client 初始化;学生端、租户后台和平台后台均已有第一批真实 API 页面;学生端已接地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题作答、错题/收藏复习、题目反馈、视频解析、练习/模考报告、收银台、订单详情和售后入口第一版;平台后台已接关键写操作、租户详情、账务资料编辑、平台审计、订阅账单候选预览/dry-run/批量生成、自动计费生成结果查看、逾期预览和催缴记录第一版,租户工作台已接权限驱动模块入口,租户学生运营页已接创建/更新、禁用/恢复、批量导入、批量分班、备注和跟进任务第一版,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版;租户设置页已接角色模板和成员绑定操作台第一版;租户营销中心已接 CRM 配置保存、队列筛选、分佣规则、成员比例、订单明细、结算生成/审核/标记线下打款第一版 | 长题干/公式图片混排体验、更细数据范围 UI、平台审计报表增强、小程序兼容验证和端到端测试 | +| Taro 前端 | 地基已建 | `apps/taro` 已有 Taro 4 React 工程、H5 三入口、租户解析、统一 API client、Supabase Auth client 初始化;学生端、租户后台和平台后台均已有第一批真实 API 页面;学生端已接地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题作答、错题/收藏复习、题目反馈、视频解析、练习/模考报告、收银台、订单详情和售后入口第一版;平台后台已接关键写操作、租户详情、账务资料编辑、平台审计查询和 CSV 导出、订阅账单候选预览/dry-run/批量生成、自动计费生成结果查看、逾期预览和催缴记录第一版,租户工作台已接权限驱动模块入口,租户学生运营页已接创建/更新、禁用/恢复、批量导入、批量分班、备注和跟进任务第一版,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版;租户设置页已接角色模板和成员绑定操作台第一版;租户营销中心已接 CRM 配置保存、队列筛选、分佣规则、成员比例、订单明细、结算生成/审核/标记线下打款第一版 | 长题干/公式图片混排体验、更细数据范围 UI、平台审计告警、小程序兼容验证和端到端测试 | ## 前端接入建议 @@ -79,7 +79,7 @@ - 对象存储:上传/下载签名已接入阿里云 OSS、腾讯云 COS、Supabase Storage;上传确认、PDF/图片预览签名、动态水印上下文、assets worker 复检、内置安全扫描、外部 HTTP scanner 接入层和题库导出 PDF/Word/每日一练 ZIP worker 已完成,继续补视频播放防盗链、真实 AV/内容安全服务联调和转码/CDN 级水印。 - 真实数据 dry-run:导出 PocketBase 用户、题库、单词、知识手册、分数线、订单、权益,先跑 `npm run pb:import:dry-run -- --profile=production --json --fail-on-warnings`,确认 `migrationReadiness` 的必需集合和关键字段覆盖率通过,再跑迁移和校验报告。 - 生产环境配置:`.env.example` 和 `npm run readiness:production` / `npm run readiness:production:db` 已补;继续补数据库迁移流程、备份恢复、日志、告警和 API 容器部署说明。 -- Taro scaffold:`apps/taro` 地基已建立;学生端、租户后台、平台后台第一批 H5 页面已接真实 API,学生端已接地区选择、错题/收藏复习、阅读理解/案例分析多小题作答、题目反馈、视频解析、练习/模考报告、收银台、订单详情、售后入口、积分任务/兑换/积分明细和消息中心第一版;平台后台关键写操作、租户详情、账务资料编辑、最近平台审计、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、逾期预览和催缴记录第一版已接入,租户工作台已接权限驱动模块入口,租户学生运营页已接学生创建/更新、禁用/恢复、批量导入、批量分班、备注和跟进任务第一版,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版,租户设置页已接角色模板创建/编辑/停用、成员绑定模板和权限可见性配置第一版,租户营销中心已接 CRM 配置/队列、分佣结算、优惠券规则/核销报表、积分任务/兑换操作台和用户通知查看第一版;下一步补独立消息中心增强、公式图片混排、更细数据范围 UI、平台审计报表增强和小程序兼容验证。 +- Taro scaffold:`apps/taro` 地基已建立;学生端、租户后台、平台后台第一批 H5 页面已接真实 API,学生端已接地区选择、错题/收藏复习、阅读理解/案例分析多小题作答、题目反馈、视频解析、练习/模考报告、收银台、订单详情、售后入口、积分任务/兑换/积分明细和消息中心第一版;平台后台关键写操作、租户详情、账务资料编辑、最近平台审计查询/CSV 导出、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、逾期预览和催缴记录第一版已接入,租户工作台已接权限驱动模块入口,租户学生运营页已接学生创建/更新、禁用/恢复、批量导入、批量分班、备注和跟进任务第一版,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版,租户设置页已接角色模板创建/编辑/停用、成员绑定模板和权限可见性配置第一版,租户营销中心已接 CRM 配置/队列、分佣结算、优惠券规则/核销报表、积分任务/兑换操作台和用户通知查看第一版;下一步补独立消息中心增强、公式图片混排、更细数据范围 UI、平台审计告警和小程序兼容验证。 ### P1:商用收费和运营能力 diff --git a/docs/refactor/backend-progress.md b/docs/refactor/backend-progress.md index 63e9183f..1984e50c 100644 --- a/docs/refactor/backend-progress.md +++ b/docs/refactor/backend-progress.md @@ -14,12 +14,12 @@ - `video`:题目视频讲解、批量预加载、通用视频搜索。 - `commerce`:订单创建/列表/详情/状态轮询、支付确认、支付 provider/webhook、激活码预检查/兑换、优惠券领取/抵扣、规则复核、权益查询。 - `referral`:销售/代理邀请码、首绑客资保护、销售统计、团队关系、CRM 队列、分佣设置、佣金来源汇总、结算单和审核/打款状态。 - - `platform-admin`:平台方租户管理、租户详情、账务资料维护、平台审计日志、SaaS 套餐、订阅、订阅账单候选预览/dry-run/批量生成、自动计费 worker、服务费收款、逾期标记、内部催缴台账、使用量。 + - `platform-admin`:平台方租户管理、租户详情、账务资料维护、平台审计日志查询/导出、SaaS 套餐、订阅、订阅账单候选预览/dry-run/批量生成、自动计费 worker、服务费收款、逾期标记、内部催缴台账、使用量。 - `tenant-admin`:租户资料、品牌、公开设置、域名、支付账户、登录 provider、私密密钥掩码、活动内容、考试日期、题目反馈处理、用户站内通知查看、激活码批次、优惠券规则和核销报表、勋章管理/发放、成员管理、角色模板、班级/学生/教师范围权限、权限矩阵、审计查询。 - `tenant-content`:租户后台内容入口、任意深度分类树、考试意向标记、题目集合、练习蓝图、题目、视频、分数线、单词、知识手册、资料资源、题目/单词/知识手册/分数线/视频 JSON 导入维护。 - `tenant`:域名/租户解析。 - 鉴权上下文已支持 Supabase Auth JWT 和迁移期 `tk_` session 双入口,JWT 通过 `auth.users.id -> platform_users.auth_user_id -> tenant_memberships` 映射业务用户和租户;平台管理员 JWT 已可访问平台后台。 -- 平台后台租户运营第一版已补齐:`GET /api/platform-admin/tenants/detail` 返回租户、域名、订阅、账单、用量和账务资料;`PUT /api/platform-admin/tenants/billing-profile` 维护开票/联系/银行掩码资料;`GET /api/platform-admin/audit-logs` 支持按租户、动作、目标、操作者和关键词查询平台审计;`GET /api/platform-admin/invoices/subscription-candidates` 和 `POST /api/platform-admin/invoices/from-subscriptions-batch` 支持订阅账单候选预览、dry-run、批量生成、重复开票跳过和平台审计;`apps/worker --job platform-billing` 可自动为即将到期且未开票订阅生成服务费账单;`POST /api/platform-admin/invoices/process-overdue`、`GET /api/platform-admin/invoices/reminders` 和 `apps/worker --job platform-dunning` 可处理已逾期未结清服务费账单,写入内部催缴台账和审计。创建租户、状态变更、账务资料维护、订阅批量开票、自动计费和逾期催缴会写入审计日志,API/worker 集成测试已覆盖平台管理员可操作、学生越权拒绝、重复保护、非法输入拒绝和审计记录存在。 +- 平台后台租户运营第一版已补齐:`GET /api/platform-admin/tenants/detail` 返回租户、域名、订阅、账单、用量和账务资料;`PUT /api/platform-admin/tenants/billing-profile` 维护开票/联系/银行掩码资料;`GET /api/platform-admin/audit-logs` 支持按租户、动作、目标、操作者、日期和关键词查询平台审计;`GET /api/platform-admin/audit-logs/export` 支持平台管理员导出 CSV/JSON,返回 base64 内容、sha256、行数和筛选条件,并对 `details` 中的 token/secret/password/key 等敏感字段递归脱敏,同时写入 `platform.audit.exported` 审计;`GET /api/platform-admin/invoices/subscription-candidates` 和 `POST /api/platform-admin/invoices/from-subscriptions-batch` 支持订阅账单候选预览、dry-run、批量生成、重复开票跳过和平台审计;`apps/worker --job platform-billing` 可自动为即将到期且未开票订阅生成服务费账单;`POST /api/platform-admin/invoices/process-overdue`、`GET /api/platform-admin/invoices/reminders` 和 `apps/worker --job platform-dunning` 可处理已逾期未结清服务费账单,写入内部催缴台账和审计。创建租户、状态变更、账务资料维护、订阅批量开票、自动计费、逾期催缴和审计导出会写入审计日志,API/worker 集成测试已覆盖平台管理员可操作、学生越权拒绝、重复保护、非法输入拒绝和审计记录存在。 - 租户自定义角色模板已落库:`tenant_role_templates` 支持权限、菜单、模块、字段和数据范围配置,成员可通过 `role_template_id` 绑定模板。 - 班级与学生范围权限已落库:`tenant_classes`、`tenant_class_members` 支持教师/班主任/助教/学生分组,教师按负责班级查看学生,字段权限可脱敏学生手机号。 - 学生运营管理已落库:`tenant_student_notes`、`tenant_student_followups` 支持学生备注、家校/班主任/销售跟进任务、可见性、指派、完成状态和审计;批量学生 upsert、批量分班、禁用/恢复也已接入权限校验。 @@ -59,6 +59,7 @@ GET /api/platform-admin/tenants/detail PATCH /api/platform-admin/tenants/status PUT /api/platform-admin/tenants/billing-profile GET /api/platform-admin/audit-logs +GET /api/platform-admin/audit-logs/export POST /api/platform-admin/subscriptions GET /api/platform-admin/invoices POST /api/platform-admin/invoices @@ -280,7 +281,7 @@ GET /api/tenant-admin/audit-logs 3. 完成真实 OAuth provider 联调:微信网页、微信小程序、QQ,确认回调域名、开放平台账号和旧 PocketBase 身份映射策略。 4. 补真实生产账单格式验收、前端财务操作台和营销活动 UI;支付/退款补偿、退款查询确认、退款通知、微信/支付宝官方账单下载、资金对账导入比对、差错工单、异常订单运营台和优惠券核销报表主链路已完成。 5. 扩展 `apps/worker`:日报统计、CRM 死信告警、公共题库同步失败告警和更完整冲突处理运营台;公共题库同步 worker 和同步通知已具备基础闭环。 -6. 开始 Taro scaffold,把 `supabaseApi` 抽到跨端包或适配层。 +6. 继续 Taro H5/小程序兼容验证,重点看公式、题图资源字段化、支付容器和分享链路。 ## 测试命令 diff --git a/docs/refactor/frontend-handoff-index.md b/docs/refactor/frontend-handoff-index.md index 62957127..ddc3430f 100644 --- a/docs/refactor/frontend-handoff-index.md +++ b/docs/refactor/frontend-handoff-index.md @@ -31,7 +31,7 @@ - `apps/taro` 已经建立,且学生端第一批 H5 页面已经可构建:登录、首页、地区选择、题库、练习、错题/收藏、练习报告、视频解析、会员收银台、订单详情、背单词、知识手册、分数线、资料、个人中心。 - 租户后台第一批 H5 页面已经可构建:工作台、数据看板、学生/班级、题库内容、营销中心、财务运营、租户设置;工作台已接 `/api/tenant-admin/permissions` 做权限驱动模块入口;学生运营页已具备学生创建/更新、状态禁用/恢复、批量导入、批量分班、学生备注和跟进任务第一版;题库内容页已具备公共题库采纳/同步、同步通知、冲突查看、单条/批量采纳平台版本或保留本地版本、导入任务详情、异步轮询、导入问题查看、模板预览/下载、导入后复检详情、JSON/CSV/Excel 选择文件或粘贴内容、后端预览、字段别名覆盖和同步/异步执行导入的第一版操作能力;营销中心已具备 CRM 配置、CRM 队列查看、分佣规则、成员分佣比例、分佣订单、结算单生成/审核/标记打款、优惠券规则/核销报表和用户通知查看第一版;财务运营页已具备退款申请/审核/供应商提交与查询、官方账单下载任务、对账批次/异常明细、差错工单处理、人工调整凭证提交/复核和异常订单运营台第一版;租户设置页已具备主题模板、草稿预览/发布、角色模板新建、编辑、停用、成员搜索/新建、成员绑定模板、成员状态和额外权限覆盖第一版。 -- 平台后台第一批 H5 页面已经可构建:工作台、租户管理、账务中心、公共题库授权;租户管理页已接租户详情、账务资料编辑和最近平台审计,工作台已展示最近平台审计摘要,账务中心已接订阅账单候选预览、dry-run、批量生成、自动计费生成结果查看、逾期预览、内部催缴生成和催缴记录查看。 +- 平台后台第一批 H5 页面已经可构建:工作台、租户管理、账务中心、公共题库授权;租户管理页已接租户详情、账务资料编辑和最近平台审计,工作台已展示最近平台审计摘要并支持导出最近平台审计 CSV,账务中心已接订阅账单候选预览、dry-run、批量生成、自动计费生成结果查看、逾期预览、内部催缴生成和催缴记录查看。 - 可以继续复刻旧题库学生端主要视觉和交互:勋章展示、小程序端分享/支付体验、背单词更细统计和更完整复盘体验。地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题、题干/选项/解析 RichContent 安全渲染、视频解析、题目反馈、模考/练习报告逐题复盘、错题复习、收藏复习、背单词卡片学习/发音/收藏练习、商城收银台、订单详情和售后入口已经有第一版页面。 - 可以按新后端主模型接入内容导航: - `content_entries` @@ -106,9 +106,9 @@ | 页面 | 文件 | 已接接口 | | --- | --- | --- | -| 工作台 | `apps/taro/src/pages/platform-admin/workbench/index.tsx` | `platform-admin/overview`、`tenants`、`invoices`、`question-banks`、`question-bank-grants`、`audit-logs` | +| 工作台 | `apps/taro/src/pages/platform-admin/workbench/index.tsx` | `platform-admin/overview`、`tenants`、`invoices`、`question-banks`、`question-bank-grants`、`audit-logs`、`audit-logs/export` | | 租户管理 | `apps/taro/src/pages/platform-admin/tenants/index.tsx` | `platform-admin/tenants`、`POST tenants`、`tenants/detail`、`PATCH tenants/status`、`PUT tenants/billing-profile`、`audit-logs` | | 账务中心 | `apps/taro/src/pages/platform-admin/billing/index.tsx` | `platform-admin/plans`、`invoices`、`invoices/subscription-candidates`、`invoices/from-subscription`、`invoices/from-subscriptions-batch`、`invoices/payments/manual-confirm`、`usage`、`subscriptions`、`POST usage` | | 公共题库 | `apps/taro/src/pages/platform-admin/question-banks/index.tsx` | `platform-admin/question-banks`、`question-bank-grants`、`PUT question-bank-grants` | -当前平台后台已经具备第一批写操作台:创建租户、租户详情查看、状态变更、账务资料维护、最近平台审计、订阅开通、账单生成、订阅账单候选预览、dry-run、批量生成、自动计费生成结果查看、人工收款确认、逾期预览、内部催缴生成、催缴记录查看、用量录入、公共题库授权编辑;这些动作均经过前端基础校验和二次确认,后端继续执行真实权限、重复开票保护和审计。下一批继续补租户基础资料编辑增强、平台审计报表导出/告警、外部催缴通知、平台在线收款和更细平台权限点。 +当前平台后台已经具备第一批写操作台:创建租户、租户详情查看、状态变更、账务资料维护、最近平台审计查询、最近平台审计 CSV 导出、订阅开通、账单生成、订阅账单候选预览、dry-run、批量生成、自动计费生成结果查看、人工收款确认、逾期预览、内部催缴生成、催缴记录查看、用量录入、公共题库授权编辑;这些动作均经过前端基础校验和二次确认,后端继续执行真实权限、重复开票保护和审计。平台审计导出只开放给平台管理员,后端会对导出 `details` 中的 token/secret/password/key 等敏感字段脱敏,并返回 `contentBase64 + sha256`,H5 可直接下载,小程序端建议先展示“已生成,需在 H5 管理台下载”。下一批继续补租户基础资料编辑增强、平台审计告警、外部催缴通知、平台在线收款和更细平台权限点。 diff --git a/docs/refactor/legacy-feature-gap-matrix.md b/docs/refactor/legacy-feature-gap-matrix.md index bf83a234..05060219 100644 --- a/docs/refactor/legacy-feature-gap-matrix.md +++ b/docs/refactor/legacy-feature-gap-matrix.md @@ -73,13 +73,13 @@ | 功能 | 新后端状态 | 待补齐 | | --- | --- | --- | -| 创建/管理租户 | 已覆盖 | 平台后台租户列表、创建租户、租户详情、状态变更、账务资料维护、最近平台审计、订阅账单候选预览、dry-run、批量生成、自动计费 worker、逾期标记和内部催缴台账已接真实 API/worker;后续补外部催缴通知、租户基础资料编辑增强和更完整平台审计报表 | +| 创建/管理租户 | 已覆盖 | 平台后台租户列表、创建租户、租户详情、状态变更、账务资料维护、最近平台审计查询/导出、订阅账单候选预览、dry-run、批量生成、自动计费 worker、逾期标记和内部催缴台账已接真实 API/worker;后续补外部催缴通知、租户基础资料编辑增强和平台审计告警 | | SaaS 套餐 | 部分覆盖 | 已和公共题库授权打通;后续继续补地区数量、科目范围、存储/学生数等组合套餐限制 | | 年费/服务费账单 | 已覆盖 | 订阅账单候选、批量开票、自动计费、人工收款、逾期标记、租户 `past_due` 状态和内部催缴记录已覆盖;真实平台在线收款、外部短信/微信订阅消息/企业微信催缴和停用策略待补 | | 租户用量记录 | 已覆盖 | 自动采集 worker 待补 | | 公共题库/地区题库 | 部分覆盖 | 已有平台公共题库列表、授权编辑、租户可采纳列表、采纳快照复制、采纳后练习组卷、手动同步 API、自动同步 worker、同步通知、冲突查询 API、单条/批量冲突“采纳平台/保留本地”处理和平台后台页面;同步会重新校验授权、复制平台新增/更新题目,并对租户自改题目返回冲突不覆盖 | 缺生产定时调度、失败告警和更完整运营消息 | | 跨租户运营看板 | 部分覆盖 | overview 有基础;缺完整 BI 聚合 | -| 租户安全审计 | 部分覆盖 | 租户侧 audit logs 和平台侧 `/api/platform-admin/audit-logs` 第一版已有,平台后台工作台/租户详情页可查看最近审计;缺更完整筛选报表、导出和告警 | +| 租户安全审计 | 部分覆盖 | 租户侧 audit logs 和平台侧 `/api/platform-admin/audit-logs` 第一版已有,平台侧 `/api/platform-admin/audit-logs/export` 可按租户、动作、目标、操作者、日期和关键词导出 CSV/JSON,导出会递归脱敏敏感字段并写审计;平台后台工作台/租户详情页可查看最近审计,工作台可导出 CSV;缺更完整筛选 UI 和告警 | ## 旧功能中应重新设计的点 diff --git a/docs/refactor/next-development-todo.md b/docs/refactor/next-development-todo.md index 9145c970..19f34eab 100644 --- a/docs/refactor/next-development-todo.md +++ b/docs/refactor/next-development-todo.md @@ -11,7 +11,7 @@ - 学生端核心 API:题库、练习、答题、模考交卷报告、练习历史、学习统计、排行榜、错题复习计划、错题、收藏、背单词、知识手册、分数线、视频播放签名、资料、订单详情/状态轮询、优惠券领取/抵扣、激活码预检查/兑换、权益、个人中心、考试倒计时、签到积分、题目反馈、勋章、站内通知。 - 租户后台 API:品牌、域名、设置、支付账户、登录 provider、私密密钥、活动、考试日期、题目反馈处理、用户站内通知查看、激活码、优惠券、勋章管理/发放、成员权限、审计、内容管理、班级/教师/学生、学生批量导入、批量分班、学生备注、跟进任务。 - 租户主题系统:平台默认经典蓝、专注绿、高对比三套模板,租户可保存草稿、发布主题,公开租户解析只返回已发布主题,Taro 租户设置页已接第一版主题操作台。 -- 平台后台 API/worker:租户、租户详情、账务资料维护、平台审计日志、SaaS 套餐、订阅、订阅账单候选预览/dry-run/批量生成、自动计费 worker、服务费收款、逾期标记、内部催缴台账、用量。 +- 平台后台 API/worker:租户、租户详情、账务资料维护、平台审计日志查询/导出、SaaS 套餐、订阅、订阅账单候选预览/dry-run/批量生成、自动计费 worker、服务费收款、逾期标记、内部催缴台账、用量。 - 销售/代理/CRM 增长链路:邀请码、扫码事件、首绑保护、团队、统计、CRM 配置/队列、`none/direct/round_robin/referrer` 跟进分配策略、CRM worker、分佣规则、成员比例、订单/激活码归因、结算生成、审核、打款状态、结算导出和凭证复核;Taro 租户营销中心已接 CRM、分佣和优惠券规则/核销报表第一版操作台。 - 内容导航:`content_entries/content_nodes` 支持任意深度入口和分类。 - 练习组卷:`question_collections/practice_blueprints` 支持顺序、随机、全真模拟快照。 @@ -187,7 +187,7 @@ - H5 和小程序共用同一套业务 API client。 - 租户通过域名、小程序配置或启动参数解析。 - 页面主题、品牌、功能开关都从后端租户配置读取;学生端和后台只消费 `/api/tenant/resolve` 的已发布 `branding.theme/publicAssets`,租户后台草稿只通过 `/api/tenant-admin/theme` 查看。 -- 当前已完成 H5 学生端、租户后台、平台后台三套构建入口和统一 API client;学生端、租户后台、平台后台都有第一批真实 API 页面;学生端已补地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题、错题/收藏复习、题目反馈、视频解析、练习/模考报告、个人中心学习报告可视化、会员收银台、订单详情、售后入口、站内消息中心第一版、积分任务/兑换/积分明细第一版、题干/选项/解析/知识手册 RichContent 安全渲染、H5 KaTeX 公式渲染、私有资源 ID 题图短签名渲染、逐题复盘、背单词卡片学习/发音/收藏练习第一版;平台后台已接入创建租户、租户详情、状态变更、账务资料维护、平台审计、订阅、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、收款、用量和公共题库授权第一版写操作;租户后台已接权限驱动工作台、学生运营操作台、主题模板预览/发布、角色模板、成员绑定、CRM/分佣操作台、积分任务/兑换操作台和用户通知查看第一版;下一步补小程序公式真机验收、题图资源字段化、独立消息中心增强、状态管理、更细数据范围 UI、学生批量运营增强和小程序兼容验证。 +- 当前已完成 H5 学生端、租户后台、平台后台三套构建入口和统一 API client;学生端、租户后台、平台后台都有第一批真实 API 页面;学生端已补地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题、错题/收藏复习、题目反馈、视频解析、练习/模考报告、个人中心学习报告可视化、会员收银台、订单详情、售后入口、站内消息中心第一版、积分任务/兑换/积分明细第一版、题干/选项/解析/知识手册 RichContent 安全渲染、H5 KaTeX 公式渲染、私有资源 ID 题图短签名渲染、逐题复盘、背单词卡片学习/发音/收藏练习第一版;平台后台已接入创建租户、租户详情、状态变更、账务资料维护、平台审计查询/CSV 导出、订阅、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、收款、用量和公共题库授权第一版写操作;租户后台已接权限驱动工作台、学生运营操作台、主题模板预览/发布、角色模板、成员绑定、CRM/分佣操作台、积分任务/兑换操作台和用户通知查看第一版;下一步补小程序公式真机验收、题图资源字段化、独立消息中心增强、状态管理、更细数据范围 UI、学生批量运营增强和小程序兼容验证。 ### 第一批页面 @@ -240,7 +240,7 @@ 1. 补租户后台写操作台:公共题库采纳/同步、冲突查看、单条/批量冲突采纳平台或保留本地、导入问题、模板预览/下载、上传/粘贴 preview/import、字段映射编辑、异步导入轮询、导入后复检详情、权限驱动工作台、学生创建/更新/批量导入/批量分班/备注/跟进、角色模板配置、成员绑定模板、CRM 配置/队列/跟进分配策略、分佣规则/成员比例/结算生成审核打款/导出/凭证复核已接第一版;继续补成员批量运营、更细数据范围 UI、真实打款 provider 和发票。 2. 继续补 Taro 学生端旧体验:地区选择、刷题答题卡、后端权威断点续练、本地进度恢复、模拟倒计时、主观题后端自评、阅读理解/案例分析多小题、视频播放、反馈、模考报告、逐题复盘、错题/收藏专题、个人中心学习报告、收银台、订单详情、售后入口、站内消息筛选/已读/归档、积分任务/兑换/积分明细、题干/解析/知识手册 RichContent 安全渲染、H5 KaTeX 公式渲染、私有资源 ID 题图短签名、背单词卡片学习/发音/收藏练习、资料短签名水印预览/下载确认已接第一版;继续补小程序公式真机验收、题图资源字段化、独立消息中心增强、背单词更细统计、小程序支付容器、分享场景和状态管理。 -3. 补平台后台增强:租户基础资料编辑增强、平台审计报表导出/告警、外部催缴通知、平台在线收款和更细平台权限点。 +3. 补平台后台增强:租户基础资料编辑增强、平台审计告警、外部催缴通知、平台在线收款和更细平台权限点。 4. 云服务器部署 Supabase/PostgreSQL 和 API,配置对象存储生产环境变量,跑 `check:refactor` 的远程等价测试。 5. 导出现有 PocketBase 数据,按 `docs/refactor/pocketbase-real-data-migration-runbook.md` 做 production dry-run、导入演练、校验和抽样验收。 6. 并行补真实登录、真实生产账单格式验收、异常订单运营台、对象存储真实 AV/内容安全服务联调、转码/CDN 级水印/生命周期、题库导出模板精排/操作台、公共题库生产定时调度和失败告警。 diff --git a/docs/refactor/taro-frontend-integration.md b/docs/refactor/taro-frontend-integration.md index 093924f5..7b45bbee 100644 --- a/docs/refactor/taro-frontend-integration.md +++ b/docs/refactor/taro-frontend-integration.md @@ -2519,7 +2519,7 @@ src/services/ai.ts AI 择校推荐生成、报告列表、报告详情 src/services/pronunciation.ts H5/小程序单词发音适配 src/services/tenantAdmin.ts 租户后台看板、权限矩阵、成员、学生创建/批量导入/分班/备注/跟进、内容、营销、设置、角色模板写操作、公共题库采纳/同步/单条和批量冲突处理、导入详情/复检、CRM 配置/队列、分佣规则/成员比例/订单/结算、优惠券规则/核销报表、积分任务/兑换配置和记录 src/services/tenantFinance.ts 租户财务运营:退款状态机、官方账单任务、对账批次/明细、差错工单、异常订单和人工调整凭证 -src/services/platformAdmin.ts 平台后台租户、租户详情、账务资料、平台审计、套餐账单、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、逾期预览/内部催缴记录、用量、公共题库授权 +src/services/platformAdmin.ts 平台后台租户、租户详情、账务资料、平台审计查询/CSV 导出、套餐账单、订阅账单候选/dry-run/批量生成、自动计费生成结果查看、逾期预览/内部催缴记录、用量、公共题库授权 ``` 验证命令: @@ -2533,6 +2533,69 @@ npm run build:taro:h5:platform 已通过。构建仍有 Taro H5 入口体积 warning,属于当前 Taro 工程既有警告,不阻断联调。 +## 平台审计导出 + +平台审计属于超级管理员后台能力,学生端和租户后台不要接。前端只能调用后端命令式接口,不允许直接读 Supabase 表或拼 SQL 导出审计。 + +查询审计: + +```text +GET /api/platform-admin/audit-logs?tenantId=&action=platform.tenant&limit=100 +``` + +导出审计: + +```text +GET /api/platform-admin/audit-logs/export?format=csv&limit=1000 +GET /api/platform-admin/audit-logs/export?format=json&tenantId=&startDate=2026-06-01&endDate=2026-06-30 +``` + +可用筛选: + +```text +tenantId +action 前缀匹配,例如 platform.tenant +targetType +actorUserId +q 匹配 action/targetType/targetId +startDate YYYY-MM-DD +endDate YYYY-MM-DD +limit 查询最大 500,导出最大 5000 +format csv | json +``` + +导出响应: + +```json +{ + "item": { + "filename": "platform-audit-20260630T120000Z.csv", + "format": "csv", + "mimeType": "text/csv", + "rowCount": 25, + "exportedAt": "2026-06-30T12:00:00.000Z", + "sha256": "", + "sizeBytes": 4096, + "contentBase64": "", + "filters": { + "tenantId": "", + "action": "platform.tenant", + "startDate": "2026-06-01", + "endDate": "2026-06-30" + } + } +} +``` + +前端处理规则: + +- 只允许平台管理员页面调用,普通学生或租户成员会返回 `PLATFORM_ADMIN_REQUIRED`。 +- 后端会把导出动作再次写入 `platform.audit.exported`,前端不需要额外记日志。 +- 导出内容中的 `details` 会递归脱敏 token、secret、password、key、authorization、cookie、session、cert、signature 等敏感字段。 +- H5 可用 `contentBase64` 生成 `data:;base64,...` 下载;下载后可以显示 `sha256` 方便人工校验。 +- 小程序端如果无法稳定写入文件系统,建议展示“已生成,请到 H5 平台后台下载”,不要把完整 base64 长文本复制到剪贴板。 +- 平台后台不要把 `contentBase64` 持久存入本地缓存、日志或埋点。 + ## 平台账务逾期催缴 平台后台账务页只调用命令式 API,不直接改 `tenant_invoices.status`、`tenants.billing_status` 或 `tenant_invoice_reminders`。后端会做平台管理员鉴权、行锁、每日催缴去重和审计。 @@ -2590,7 +2653,7 @@ GET /api/platform-admin/invoices/reminders?tenantId=&invoiceId== 3, true, 'platform audit export should include tenant audit rows'); + assert.ok(auditExport.item?.sha256, 'platform audit export should include content hash'); + assert.ok(Buffer.from(auditExport.item?.contentBase64 || '', 'base64').toString('utf8').includes('platform.tenant.created'), 'platform audit export should contain audit actions'); + + const invalidAuditExportFormat = await request('/api/platform-admin/audit-logs/export', { + tenantId: false, + userId: false, + headers: adminHeaders, + query: { tenantId, format: 'xlsx' }, + expectStatus: 400, + }); + assert.equal(invalidAuditExportFormat.code, 'INVALID_EXPORT_FORMAT', 'platform audit export should reject invalid formats'); + const candidates = await request('/api/platform-admin/invoices/subscription-candidates', { tenantId: false, userId: false, @@ -1421,6 +1441,13 @@ async function testPlatformTenantOperationsAndAudit() { }); assert.equal(studentAuditDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not read platform audit logs'); + const studentAuditExportDenied = await request('/api/platform-admin/audit-logs/export', { + tenantId: false, + userId: USER_ID, + expectStatus: 403, + }); + assert.equal(studentAuditExportDenied.code, 'PLATFORM_ADMIN_REQUIRED', 'student must not export platform audit logs'); + const studentReminderDenied = await request('/api/platform-admin/invoices/reminders', { tenantId: false, userId: USER_ID,