diff --git a/apps/taro/README.md b/apps/taro/README.md index 2b971630..d35aea23 100644 --- a/apps/taro/README.md +++ b/apps/taro/README.md @@ -47,12 +47,12 @@ pages/student/profile/index 个人中心、会员、订单、签到、激 pages/tenant-admin/workbench/index 租户后台工作台 pages/tenant-admin/dashboard/index 数据看板 pages/tenant-admin/students/index 学生与班级 -pages/tenant-admin/content/index 内容入口、导入任务、公共题库 +pages/tenant-admin/content/index 内容入口、导入任务、字段模板、复检、公共题库采纳/同步 pages/tenant-admin/marketing/index 优惠券、激活码、CRM、分佣摘要 pages/tenant-admin/settings/index 品牌、域名、支付、登录、角色模板 ``` -当前后台页面以只读联调和运营扫描为主,少量写入动作后续会按权限、表单校验、审计结果继续补。真正权限以后端 permission keys 为准,前端菜单隐藏只做体验优化。 +当前后台页面已经从只读联调推进到第一批运营写操作。题库内容页已接入公共题库采纳、公共题库同步、同步冲突查看、导入问题查看、字段映射预览、模板预览和导入后复检;营销、设置和学生页仍以扫描和轻量操作为主。真正权限以后端 permission keys 为准,前端菜单隐藏只做体验优化。 ## 当前平台后台页面 diff --git a/apps/taro/src/pages/tenant-admin/admin.css b/apps/taro/src/pages/tenant-admin/admin.css index 752c1391..3a7df363 100644 --- a/apps/taro/src/pages/tenant-admin/admin.css +++ b/apps/taro/src/pages/tenant-admin/admin.css @@ -167,6 +167,31 @@ font-size: 24px; } +.admin-row-actions { + display: flex; + gap: 10px; + margin-top: 14px; + overflow-x: auto; +} + +.admin-mini-button { + min-width: 112px; + height: 54px; + border: 1px solid #cbd5e1; + border-radius: 8px; + background: #fff; + color: #1e3a8a; + font-size: 21px; + font-weight: 700; + line-height: 54px; +} + +.admin-mini-button.primary { + border-color: #2563eb; + background: #2563eb; + color: #fff; +} + .admin-error { display: block; margin-top: 12px; diff --git a/apps/taro/src/pages/tenant-admin/content/index.tsx b/apps/taro/src/pages/tenant-admin/content/index.tsx index 84553336..743c4cf2 100644 --- a/apps/taro/src/pages/tenant-admin/content/index.tsx +++ b/apps/taro/src/pages/tenant-admin/content/index.tsx @@ -1,15 +1,55 @@ import { useEffect, useState } from 'react'; -import { Text, View } from '@tarojs/components'; -import { loadContentEntriesAdmin, loadImportJobs, loadPublicQuestionBanks, type ContentEntryAdminItem, type ImportJobItem, type PublicQuestionBankItem } from '@/services/tenantAdmin'; +import Taro from '@tarojs/taro'; +import { Button, Input, Text, View } from '@tarojs/components'; +import { + adoptPublicQuestionBank, + loadContentEntriesAdmin, + loadImportFieldMapping, + loadImportIssues, + loadImportJobs, + loadImportPostCheck, + loadImportTemplate, + loadPublicQuestionBankConflicts, + loadPublicQuestionBanks, + runImportPostCheck, + syncPublicQuestionBank, + type ContentEntryAdminItem, + type ImportFieldMapping, + type ImportIssueItem, + type ImportJobItem, + type ImportTemplateItem, + type PublicQuestionBankItem, +} from '@/services/tenantAdmin'; import '../admin.css'; +function displayBankName(item: PublicQuestionBankItem) { + return item.sourceQuestionBankName || item.name || '公共题库'; +} + +function displayRegionName(item: PublicQuestionBankItem) { + return item.sourceRegionName || item.regionName || '通用'; +} + +function adoptionIdOf(item: PublicQuestionBankItem) { + return item.adoptedId || (item.adoption && String(item.adoption.id || '')) || ''; +} + export default function TenantContentPage() { const [entries, setEntries] = useState([]); const [jobs, setJobs] = useState([]); const [publicBanks, setPublicBanks] = useState([]); + const [selectedJobId, setSelectedJobId] = useState(''); + const [selectedImportType, setSelectedImportType] = useState('questions'); + const [copyLimit, setCopyLimit] = useState('200'); + const [issues, setIssues] = useState([]); + const [mapping, setMapping] = useState(null); + const [template, setTemplate] = useState(null); + const [postCheck, setPostCheck] = useState | null>(null); + const [conflicts, setConflicts] = useState | null>(null); + const [busy, setBusy] = useState(''); const [error, setError] = useState(''); - useEffect(() => { + function reload() { Promise.all([ loadContentEntriesAdmin().catch(() => ({ items: [] })), loadImportJobs(20).catch(() => ({ items: [] })), @@ -19,8 +59,145 @@ export default function TenantContentPage() { setJobs(jobPayload.items || []); setPublicBanks(bankPayload.items || []); }).catch(nextError => setError(nextError instanceof Error ? nextError.message : '内容数据加载失败')); + } + + useEffect(() => { + reload(); }, []); + async function confirm(title: string, content: string) { + const result = await Taro.showModal({ title, content, confirmText: '确认', cancelText: '取消' }); + return result.confirm; + } + + async function selectJob(item: ImportJobItem) { + const importType = item.importType || 'questions'; + setSelectedJobId(item.id); + setSelectedImportType(importType); + setPostCheck(null); + setError(''); + try { + const [issuePayload, mappingPayload, templatePayload, postCheckPayload] = await Promise.all([ + loadImportIssues(item.id).catch(() => ({ items: [] })), + loadImportFieldMapping(importType).catch(() => ({ item: null })), + loadImportTemplate(importType, 'json').catch(() => ({ item: null })), + loadImportPostCheck(item.id).catch(() => ({ item: null })), + ]); + setIssues(issuePayload.items || []); + setMapping(mappingPayload.item || null); + setTemplate(templatePayload.item || null); + setPostCheck(postCheckPayload.item || null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '导入任务详情加载失败'); + } + } + + async function submitPostCheck() { + if (!selectedJobId) { + setError('请先选择一个导入任务。'); + return; + } + const ok = await confirm('导入后复检', '确认对该导入任务执行后端复检?复检结果会写入任务摘要和审计日志。'); + if (!ok) return; + setBusy('post-check'); + setError(''); + try { + const payload = await runImportPostCheck(selectedJobId); + setPostCheck(payload.item || null); + Taro.showToast({ title: '复检完成', icon: 'success' }); + reload(); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '导入复检失败'); + } finally { + setBusy(''); + } + } + + async function previewTemplate(type = selectedImportType) { + setError(''); + try { + const [mappingPayload, templatePayload] = await Promise.all([ + loadImportFieldMapping(type), + loadImportTemplate(type, 'json'), + ]); + setSelectedImportType(type); + setMapping(mappingPayload.item || null); + setTemplate(templatePayload.item || null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '模板加载失败'); + } + } + + async function submitAdopt(item: PublicQuestionBankItem) { + if (!item.grantId) { + setError('该公共题库缺少授权 ID,无法采纳。'); + return; + } + const ok = await confirm('采纳公共题库', `确认采纳 ${displayBankName(item)}?系统会复制题目快照并创建本租户入口和题目集合。`); + if (!ok) return; + setBusy(`adopt:${item.grantId}`); + setError(''); + try { + await adoptPublicQuestionBank({ + grantId: item.grantId, + entryName: displayBankName(item), + collectionName: `${displayBankName(item)}题目`, + copyLimit: Math.max(1, Math.min(Number(copyLimit || 200), 1000)), + }); + Taro.showToast({ title: '已采纳', icon: 'success' }); + reload(); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '公共题库采纳失败'); + } finally { + setBusy(''); + } + } + + async function submitSync(item: PublicQuestionBankItem) { + const adoptionId = adoptionIdOf(item); + if (!adoptionId) { + setError('请先采纳公共题库,再执行同步。'); + return; + } + const ok = await confirm('同步公共题库', `确认同步 ${displayBankName(item)} 的平台新增/更新题目?租户自改题目会被冲突保护。`); + if (!ok) return; + setBusy(`sync:${adoptionId}`); + setError(''); + try { + const payload = await syncPublicQuestionBank({ + adoptionId, + copyLimit: Math.max(1, Math.min(Number(copyLimit || 1000), 1000)), + }); + setConflicts(payload.sync || null); + Taro.showToast({ title: '同步完成', icon: 'success' }); + reload(); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '公共题库同步失败'); + } finally { + setBusy(''); + } + } + + async function showConflicts(item: PublicQuestionBankItem) { + const adoptionId = adoptionIdOf(item); + if (!adoptionId) { + setError('该公共题库尚未采纳,没有冲突记录。'); + return; + } + setError(''); + try { + const payload = await loadPublicQuestionBankConflicts(adoptionId); + setConflicts(payload.item || null); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '冲突记录加载失败'); + } + } + + const postCheckSummary = postCheck?.importPostCheck && typeof postCheck.importPostCheck === 'object' + ? postCheck.importPostCheck as Record + : postCheck; + const postCheckIssueCount = Array.isArray(postCheckSummary?.issues) ? postCheckSummary.issues.length : 0; + return ( @@ -43,6 +220,26 @@ export default function TenantContentPage() { {!entries.length ? 暂无内容入口。 : null} + + 导入操作 + + setCopyLimit(String(event.detail.value || ''))} /> + + + + {['questions', 'vocabulary', 'handbook', 'scoreline', 'videos'].map(type => ( + + ))} + + + 当前任务{selectedJobId ? selectedJobId.slice(0, 8) : '-'} + 问题行{String(issues.length)} + 字段数{String(mapping?.fields?.length || 0)} + 复检状态{String(postCheckSummary?.status || '-')} + 复检问题{String(postCheckIssueCount)} + + + 导入任务 @@ -50,24 +247,74 @@ export default function TenantContentPage() { {item.importType || 'import'} · {item.status || 'unknown'} 总数 {item.totalCount || 0} · 成功 {item.successCount || 0} · 错误 {item.errorCount || 0} · 警告 {item.warningCount || 0} + 任务 {item.id} · {item.updatedAt || item.createdAt || ''} + + + + ))} {!jobs.length ? 暂无导入任务。 : null} + + 问题与模板 + + {issues.slice(0, 8).map(item => ( + + {item.severity || 'issue'} · {item.code || '-'} + 第 {item.rowNo || '-'} 行 · {item.fieldPath || '整行'} · {item.message || '-'} + + ))} + + {!issues.length ? 选择导入任务后可查看问题行。 : null} + {mapping?.fields?.length ? ( + + {mapping.fields.slice(0, 6).map(field => ( + + {field.label || field.field}{field.required ? ' · 必填' : ''} + {field.field || '-'} · {(field.aliases || []).join(' / ') || '无别名'} + {field.description || ''} + + ))} + + ) : null} + {template?.contentPreview ? {template.fileName || '模板'}:{template.contentPreview.slice(0, 240)} : null} + + 公共题库 {publicBanks.map((item, index) => ( - {item.name || '公共题库'} - {item.regionName || '通用'} · {item.grantScope || '授权'} · {item.adoption ? '已采纳' : '可采纳'} + {displayBankName(item)} + {displayRegionName(item)} · {item.grantScope || '授权'} · {adoptionIdOf(item) ? '已采纳' : '可采纳'} · {item.questionCount || 0} 题 + 同步 {item.syncStatus || '-'} · 已复制 {item.copiedQuestionCount || 0} · {item.lastSyncedAt ? String(item.lastSyncedAt).slice(0, 10) : '未同步'} + + + + + ))} {!publicBanks.length ? 暂无可见公共题库授权。 : null} + + + 同步冲突 + {conflicts ? ( + + 状态 {String(conflicts.status || conflicts.syncStatus || '-')} + 冲突数 {String(conflicts.conflictCount || 0)} · 最近同步 {String(conflicts.lastSyncedAt || '')} + {JSON.stringify(conflicts).slice(0, 300)} + + ) : 同步后或点击“冲突”可查看最近冲突摘要。} + {error ? {error} : null} diff --git a/apps/taro/src/services/tenantAdmin.ts b/apps/taro/src/services/tenantAdmin.ts index 5733a632..ca4d0dd9 100644 --- a/apps/taro/src/services/tenantAdmin.ts +++ b/apps/taro/src/services/tenantAdmin.ts @@ -54,6 +54,7 @@ export interface ImportJobItem { warningCount?: number; createdAt?: string; updatedAt?: string; + summary?: Record; } export interface ContentEntryAdminItem { @@ -68,12 +69,59 @@ export interface ContentEntryAdminItem { export interface PublicQuestionBankItem { grantId?: string; sourceQuestionBankId?: string; + sourceQuestionBankName?: string; + sourceRegionName?: string | null; name?: string; regionName?: string | null; grantScope?: string; + questionCount?: number; + adoptedId?: string | null; + adoptionStatus?: string | null; + syncStatus?: string | null; + targetEntryId?: string | null; + targetCollectionId?: string | null; + copiedQuestionCount?: number; + lastSyncedAt?: string | null; adoption?: Record | null; } +export interface ImportIssueItem { + id: string; + rowNo?: number | null; + severity?: string; + code?: string; + fieldPath?: string | null; + message?: string; + externalId?: string | null; + itemStatus?: string | null; + createdAt?: string; +} + +export interface ImportFieldMapping { + importType?: string; + title?: string; + description?: string; + fields?: Array<{ + field?: string; + label?: string; + required?: boolean; + aliases?: string[]; + description?: string; + example?: unknown; + }>; + requiredFields?: string[]; +} + +export interface ImportTemplateItem { + importType?: string; + format?: string; + fileName?: string; + mimeType?: string; + contentBase64?: string; + contentPreview?: string; + fields?: Record[]; +} + export interface CouponItem { id: string; code: string; @@ -139,13 +187,58 @@ export async function loadImportJobs(limit = 20) { } export async function loadImportIssues(jobId: string) { - return apiRequest<{ items?: Record[] }>('/api/tenant-content/imports/issues', { query: { jobId } }); + return apiRequest<{ items?: ImportIssueItem[] }>('/api/tenant-content/imports/issues', { query: { jobId } }); } export async function loadPublicQuestionBanks() { return apiRequest<{ items?: PublicQuestionBankItem[] }>('/api/tenant-content/public-question-banks'); } +export async function adoptPublicQuestionBank(input: { grantId: string; entryName?: string; collectionName?: string; copyLimit?: number }) { + return apiRequest<{ item?: Record }>('/api/tenant-content/public-question-banks/adopt', { + method: 'POST', + body: input, + }); +} + +export async function syncPublicQuestionBank(input: { adoptionId: string; copyLimit?: number }) { + return apiRequest<{ item?: Record; sync?: Record }>('/api/tenant-content/public-question-banks/sync', { + method: 'POST', + body: input, + }); +} + +export async function loadPublicQuestionBankConflicts(adoptionId: string) { + return apiRequest<{ item?: Record }>('/api/tenant-content/public-question-banks/conflicts', { + query: { adoptionId }, + }); +} + +export async function runImportPostCheck(jobId: string) { + return apiRequest<{ item?: Record }>('/api/tenant-content/imports/post-check', { + method: 'POST', + body: { jobId }, + }); +} + +export async function loadImportPostCheck(jobId: string) { + return apiRequest<{ item?: Record }>('/api/tenant-content/imports/post-check', { + query: { jobId }, + }); +} + +export async function loadImportFieldMapping(importType: string) { + return apiRequest<{ item?: ImportFieldMapping }>('/api/tenant-content/imports/field-mapping', { + query: { importType }, + }); +} + +export async function loadImportTemplate(importType: string, format: 'json' | 'csv' = 'json') { + return apiRequest<{ item?: ImportTemplateItem }>('/api/tenant-content/imports/templates', { + query: { importType, format }, + }); +} + export async function loadCoupons() { return apiRequest<{ items?: CouponItem[] }>('/api/tenant-admin/coupons'); } diff --git a/docs/refactor/backend-handoff-roadmap.md b/docs/refactor/backend-handoff-roadmap.md index c01dc50c..4ff08791 100644 --- a/docs/refactor/backend-handoff-roadmap.md +++ b/docs/refactor/backend-handoff-roadmap.md @@ -35,7 +35,7 @@ | 内容导入 | 可联调 | 题目、单词、知识手册、分数线、视频 JSON/CSV/Excel preview/import、issue、job、审计、幂等、`executionMode=async`、imports worker、导入后复检、模板下载、字段映射 API、PocketBase JSON dry-run 报告 | 字段映射 UI、真实数据 dry-run 执行验收和导入性能压测 | | 数据看板 | 可联调 | 租户 dashboard 聚合接口,收益、注册、学习、内容、激活码、反馈、趋势、24h 活跃、套餐销量和运营动态 | 预聚合 worker、缓存、慢 SQL 监控和销售转化看板 | | AI 择校推荐 | 未开始 | 暂无 | 数据上下文、AI JSON schema、报告渲染、PDF 生成 | -| Taro 前端 | 地基已建 | `apps/taro` 已有 Taro 4 React 工程、H5 三入口、租户解析、统一 API client、Supabase Auth client 初始化;学生端、租户后台和平台后台均已有第一批真实 API 页面;平台后台已接关键写操作第一版 | 学生端完整体验、租户后台写操作页面、平台后台审计/详情增强、小程序兼容验证和端到端测试 | +| Taro 前端 | 地基已建 | `apps/taro` 已有 Taro 4 React 工程、H5 三入口、租户解析、统一 API client、Supabase Auth client 初始化;学生端、租户后台和平台后台均已有第一批真实 API 页面;平台后台已接关键写操作第一版,租户内容页已接公共题库采纳/同步、导入问题、字段模板和复检第一版 | 学生端完整体验、租户后台导入上传/字段映射编辑/冲突处理详情、平台后台审计/详情增强、小程序兼容验证和端到端测试 | ## 前端接入建议 @@ -79,7 +79,7 @@ - 对象存储:上传/下载签名已接入阿里云 OSS、腾讯云 COS、Supabase Storage;上传确认、PDF/图片预览签名和 assets worker 复检已完成,继续补 PDF 渲染、视频播放防盗链、杀毒扫描和水印。 - 真实数据 dry-run:导出 PocketBase 用户、题库、单词、知识手册、分数线、订单、权益,先跑 `npm run pb:import:dry-run`,再跑迁移和校验报告。 - 生产环境配置:`.env.example` 和 `npm run readiness:production` / `npm run readiness:production:db` 已补;继续补数据库迁移流程、备份恢复、日志、告警和 API 容器部署说明。 -- Taro scaffold:`apps/taro` 地基已建立;学生端、租户后台、平台后台第一批 H5 页面已接真实 API,平台后台关键写操作第一版已接入;下一步补学生端完整刷题体验、租户后台写操作台、平台后台审计增强和小程序兼容验证。 +- Taro scaffold:`apps/taro` 地基已建立;学生端、租户后台、平台后台第一批 H5 页面已接真实 API,平台后台关键写操作第一版已接入,租户内容页已接公共题库采纳/同步、导入问题、字段模板和复检第一版;下一步补学生端完整刷题体验、租户后台导入上传/字段映射编辑/冲突处理详情、平台后台审计增强和小程序兼容验证。 ### P1:商用收费和运营能力 diff --git a/docs/refactor/frontend-handoff-index.md b/docs/refactor/frontend-handoff-index.md index 5817874e..13c7da45 100644 --- a/docs/refactor/frontend-handoff-index.md +++ b/docs/refactor/frontend-handoff-index.md @@ -26,7 +26,7 @@ ## 当前可进入的前端工作 - `apps/taro` 已经建立,且学生端第一批 H5 页面已经可构建:登录、首页、题库、练习、背单词、知识手册、分数线、资料、个人中心。 -- 租户后台第一批 H5 页面已经可构建:工作台、数据看板、学生/班级、题库内容、营销中心、租户设置。 +- 租户后台第一批 H5 页面已经可构建:工作台、数据看板、学生/班级、题库内容、营销中心、租户设置;题库内容页已具备公共题库采纳/同步、冲突查看、导入问题查看、模板预览和导入后复检的第一版操作能力。 - 平台后台第一批 H5 页面已经可构建:工作台、租户管理、账务中心、公共题库授权。 - 可以继续复刻旧题库学生端主要视觉和交互:选地区、刷题细节、视频解析、题目反馈、模考报告、错题复习、商城收银台、订单详情、勋章展示。 - 可以按新后端主模型接入内容导航: @@ -47,7 +47,7 @@ - 不要把“Supabase 支持前端 Data API”误解为“本项目所有业务表都由 Taro 直写”;订单、支付、权益、租户后台、导入、CRM、私有资源必须走 RPC、`apps/api`、Edge Function 或 worker 这类后端命令层。 - 短信、微信小程序/网页登录、QQ 登录、微信支付、支付宝支付 provider 已有本地 adapter 和测试覆盖;生产账号、回调域名、证书和商户资料仍需正式联调。 - 对象存储已完成签名 provider、上传后校验、PDF/图片预览和资源复检 worker,但 CDN 防盗链、视频水印和杀毒扫描还要补。 -- 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入已可联调;大批量导入可传 `executionMode=async` 交给 imports worker;模板下载、字段映射 API 和导入后复检已可用,前端还要补上传预览、排队轮询、问题行展示、模板下载按钮、字段映射 UI 和复检结果面板。 +- 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入已可联调;大批量导入可传 `executionMode=async` 交给 imports worker;模板下载、字段映射 API 和导入后复检已可用。租户内容页已经可以查看问题行、预览字段映射/模板并触发复检,后续还要补上传预览、排队轮询、模板文件下载按钮、字段映射编辑 UI 和复检结果详情面板。 - 数据看板、分佣结算和勋章手动发放基础 API 已可联调;勋章自动发放、分佣真实打款/导出/凭证、AI 择校、主题模板市场等仍是后续商用增强项。 ## 前后端协作建议 @@ -81,11 +81,11 @@ | 工作台 | `apps/taro/src/pages/tenant-admin/workbench/index.tsx` | `tenant-admin/overview`、`tenant-admin/dashboard` | | 数据看板 | `apps/taro/src/pages/tenant-admin/dashboard/index.tsx` | `tenant-admin/dashboard` | | 学生运营 | `apps/taro/src/pages/tenant-admin/students/index.tsx` | `tenant-admin/classes`、`tenant-admin/students` | -| 题库内容 | `apps/taro/src/pages/tenant-admin/content/index.tsx` | `tenant-content/content-entries`、`tenant-content/imports`、`tenant-content/public-question-banks` | +| 题库内容 | `apps/taro/src/pages/tenant-admin/content/index.tsx` | `tenant-content/content-entries`、`tenant-content/imports`、`imports/issues`、`imports/field-mapping`、`imports/templates`、`imports/post-check`、`tenant-content/public-question-banks`、`public-question-banks/adopt`、`public-question-banks/sync`、`public-question-banks/conflicts` | | 营销中心 | `apps/taro/src/pages/tenant-admin/marketing/index.tsx` | `tenant-admin/coupons`、`code-batches`、`activation-codes`、`crm/queue`、`commission/summary` | | 租户设置 | `apps/taro/src/pages/tenant-admin/settings/index.tsx` | `tenant-admin/overview`、`domains`、`payment-accounts`、`auth-providers`、`role-templates` | -当前页面以运营看板和只读列表为主。下一批需要继续补后台写入表单、导入 preview/import/issue 操作台、公共题库采纳/同步按钮、学生批量导入、角色模板配置 UI 和权限驱动菜单。 +当前租户后台已有第一批运营操作:题库内容页支持公共题库采纳/同步、同步冲突查看、导入问题查看、字段映射/模板预览和导入后复检。下一批需要继续补完整后台写入表单、导入上传 preview/import 操作台、字段映射编辑、学生批量导入、角色模板配置 UI 和权限驱动菜单。 ## 已落地的 Taro 平台后台页面 diff --git a/docs/refactor/next-development-todo.md b/docs/refactor/next-development-todo.md index 9e9a0acd..a791212e 100644 --- a/docs/refactor/next-development-todo.md +++ b/docs/refactor/next-development-todo.md @@ -211,7 +211,7 @@ ## 推荐下一步顺序 1. 继续补 Taro 学生端旧体验:地区选择、视频播放、反馈、模考报告、错题/收藏专题、订单收银台。 -2. 补租户后台写操作台:导入 preview/import/issues、字段映射、公共题库采纳/同步、角色模板、CRM/分佣。 +2. 补租户后台写操作台:公共题库采纳/同步、冲突查看、导入问题、模板预览和导入后复检已接第一版;继续补导入上传 preview/import、字段映射编辑、冲突处理详情、角色模板、CRM/分佣。 3. 补平台后台增强:租户详情/编辑、平台审计报表、自动计费、账单批量操作和更细平台权限点。 4. 云服务器部署 Supabase/PostgreSQL 和 API,配置对象存储生产环境变量,跑 `check:refactor` 的远程等价测试。 5. 导出现有 PocketBase 数据,做完整 dry-run 迁移。 diff --git a/docs/refactor/taro-frontend-integration.md b/docs/refactor/taro-frontend-integration.md index a43c23fb..83f87ce1 100644 --- a/docs/refactor/taro-frontend-integration.md +++ b/docs/refactor/taro-frontend-integration.md @@ -1374,7 +1374,7 @@ src/services/catalog.ts 目录、题库、单词、手册、分数线、资 src/services/learning.ts 练习 session、答题、收藏、单词复习 src/services/commerce.ts 套餐、订单、权益、激活码 src/services/profile.ts 个人中心、签到、勋章、倒计时 -src/services/tenantAdmin.ts 租户后台看板、学生、内容、营销、设置 +src/services/tenantAdmin.ts 租户后台看板、学生、内容、营销、设置、公共题库采纳/同步、导入复检 src/services/platformAdmin.ts 平台后台租户、套餐账单、用量、公共题库授权 ``` @@ -1392,7 +1392,7 @@ npm run build:taro:h5:platform 下一批前端开发重点: - 学生端:选地区、题目视频播放、题目反馈、错题/收藏专题页、模考交卷报告、订单收银台和订单详情。 -- 租户后台:写入表单、字段映射 UI、导入 preview/import/issues 操作台、公共题库采纳/同步、学生批量导入、角色模板配置 UI、CRM 分配和分佣结算操作。 +- 租户后台:题库内容页已接公共题库采纳/同步、冲突查看、导入问题、模板预览和导入后复检第一版;下一批继续补完整写入表单、导入上传 preview/import 操作台、字段映射编辑、学生批量导入、角色模板配置 UI、CRM 分配和分佣结算操作。 - 平台后台:租户创建、状态变更、订阅开通、账单生成、人工收款确认、用量录入、公共题库授权编辑已接第一版;继续补租户详情/编辑、平台审计、自动计费和批量账单操作。 - 小程序:验证 `Taro.login`、微信支付、分享 scene/referral、Supabase client 兼容性;如不稳定,保留 `apps/api/auth/*` 作为小程序登录适配层。 @@ -1409,7 +1409,7 @@ npm run build:taro:h5:platform - 角色模板:`GET/PUT /api/tenant-admin/role-templates`、`POST /api/tenant-admin/role-templates/disable` - 内容入口/分类树/题目集合/练习蓝图 - 题目/单词/知识手册/分数线/视频维护 -- 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入 preview/import/issues +- 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入 preview/import/issues、字段映射、模板下载和导入后复检 - Banner/FAQ/公告/激活码/优惠券 - 勋章:`GET/PUT /api/tenant-admin/badges`、`GET/POST /api/tenant-admin/badge-grants` - 考试日期:`GET/PUT /api/tenant-admin/exam-dates`