From da7b4617024563b57bfb725712562e720f161ee9 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 29 Jun 2026 14:37:09 +0800 Subject: [PATCH] feat: bind tenant members to role templates --- apps/taro/README.md | 6 +- .../src/pages/tenant-admin/settings/index.tsx | 297 ++++++++++++++++++ .../pages/tenant-admin/workbench/index.tsx | 73 ++++- apps/taro/src/services/tenantAdmin.ts | 59 ++++ docs/refactor/backend-handoff-roadmap.md | 8 +- docs/refactor/frontend-handoff-index.md | 10 +- docs/refactor/next-development-todo.md | 7 +- docs/refactor/taro-frontend-integration.md | 6 +- 8 files changed, 440 insertions(+), 26 deletions(-) diff --git a/apps/taro/README.md b/apps/taro/README.md index 0113939b..1979e39b 100644 --- a/apps/taro/README.md +++ b/apps/taro/README.md @@ -56,17 +56,17 @@ pages/student/profile/index 个人中心、会员、订单、签到、激 已接入第一批真实后端 API: ```text -pages/tenant-admin/workbench/index 租户后台工作台 +pages/tenant-admin/workbench/index 租户后台工作台、权限驱动模块入口 pages/tenant-admin/dashboard/index 数据看板 pages/tenant-admin/students/index 学生与班级 pages/tenant-admin/content/index 内容入口、导入任务、字段模板、异步轮询、复检、公共题库采纳/同步/单条和批量冲突处理 pages/tenant-admin/marketing/index 优惠券、激活码、CRM、分佣摘要 -pages/tenant-admin/settings/index 品牌、域名、支付、登录、角色模板创建/编辑/停用 +pages/tenant-admin/settings/index 品牌、域名、支付、登录、角色模板和成员绑定 ``` 当前后台页面已经从只读联调推进到第一批运营写操作。题库内容页已接入公共题库采纳、公共题库同步、同步冲突查看、单条/批量采纳平台版本或保留本地版本、导入任务详情、异步任务轮询、导入问题查看、模板预览/下载、导入后复检详情,以及 JSON/CSV/Excel 的 H5 选择文件或粘贴内容、后端预览、字段别名覆盖和同步/异步执行导入第一版;营销、设置和学生页仍以扫描和轻量操作为主。真正权限以后端 permission keys 为准,前端菜单隐藏只做体验优化。 -租户设置页已接入角色模板写操作第一版:可新建、编辑、停用非系统模板,并配置权限点、菜单可见、模块可见、字段可见和基础数据范围。前端只负责操作体验,`tenant_owner`、`tenant_admin`、通配权限和系统模板保护仍以后端校验与审计为准。 +租户工作台会读取 `/api/tenant-admin/permissions`,按菜单权限和有效权限隐藏不可见模块。租户设置页已接入角色模板与成员绑定写操作第一版:可新建、编辑、停用非系统模板,配置权限点、菜单可见、模块可见、字段可见和基础数据范围,也可以搜索/新建成员、绑定角色模板、设置成员状态和额外权限覆盖。前端只负责操作体验,`tenant_owner`、`tenant_admin`、通配权限、系统模板保护和最后一名 owner 保护仍以后端校验与审计为准。 ## 当前平台后台页面 diff --git a/apps/taro/src/pages/tenant-admin/settings/index.tsx b/apps/taro/src/pages/tenant-admin/settings/index.tsx index 6889f44f..767316d7 100644 --- a/apps/taro/src/pages/tenant-admin/settings/index.tsx +++ b/apps/taro/src/pages/tenant-admin/settings/index.tsx @@ -2,14 +2,18 @@ import { useEffect, useMemo, useState } from 'react'; import Taro from '@tarojs/taro'; import { Button, Input, Text, Textarea, View } from '@tarojs/components'; import { + disableTenantMember, disableRoleTemplate, loadAuthProviders, + loadTenantMembers, loadPaymentAccounts, loadRoleTemplates, loadTenantDomains, loadTenantOverview, loadTenantPermissions, + upsertTenantMember, upsertRoleTemplate, + type TenantMemberItem, type TenantOverview, type TenantPermissionCatalogItem, type TenantPermissionsPayload, @@ -35,6 +39,19 @@ interface RoleTemplateForm { ownLeadsOnly: boolean; } +interface MemberForm { + membershipId?: string; + userId: string; + username: string; + name: string; + phone: string; + email: string; + role: string; + status: string; + roleTemplateId: string; + permissions: Record; +} + const BASE_ROLE_OPTIONS = [ { key: 'tenant_operator', label: '运营' }, { key: 'teacher', label: '教师' }, @@ -44,6 +61,12 @@ const BASE_ROLE_OPTIONS = [ { key: 'tenant_admin', label: '管理员' }, ]; +const MEMBER_STATUS_OPTIONS = [ + { key: 'active', label: '启用' }, + { key: 'invited', label: '邀请中' }, + { key: 'disabled', label: '停用' }, +]; + const STATUS_OPTIONS = [ { key: 'active', label: '启用' }, { key: 'disabled', label: '停用' }, @@ -103,6 +126,20 @@ function emptyRoleTemplateForm(): RoleTemplateForm { }; } +function emptyMemberForm(): MemberForm { + return { + userId: '', + username: '', + name: '', + phone: '', + email: '', + role: 'tenant_operator', + status: 'active', + roleTemplateId: '', + permissions: {}, + }; +} + function roleTemplateFormFrom(item?: TenantRoleTemplateItem | null): RoleTemplateForm { if (!item) return emptyRoleTemplateForm(); const dataScope = item.dataScope && typeof item.dataScope === 'object' ? item.dataScope : {}; @@ -123,6 +160,22 @@ function roleTemplateFormFrom(item?: TenantRoleTemplateItem | null): RoleTemplat }; } +function memberFormFrom(item?: TenantMemberItem | null): MemberForm { + if (!item) return emptyMemberForm(); + return { + membershipId: item.id, + userId: item.userId || '', + username: item.username || '', + name: item.name || '', + phone: item.phone || '', + email: item.email || '', + role: item.role || 'tenant_operator', + status: item.status || 'active', + roleTemplateId: item.roleTemplateId || '', + permissions: boolRecord(item.permissions), + }; +} + function catalogLabel(item: TenantPermissionCatalogItem) { return item.label ? `${item.label}` : item.key; } @@ -133,9 +186,13 @@ export default function TenantSettingsPage() { const [payments, setPayments] = useState[]>([]); const [authProviders, setAuthProviders] = useState[]>([]); const [roles, setRoles] = useState([]); + const [members, setMembers] = useState([]); const [permissionsPayload, setPermissionsPayload] = useState({}); const [selectedRoleId, setSelectedRoleId] = useState(''); const [roleForm, setRoleForm] = useState(() => emptyRoleTemplateForm()); + const [memberKeyword, setMemberKeyword] = useState(''); + const [selectedMemberId, setSelectedMemberId] = useState(''); + const [memberForm, setMemberForm] = useState(() => emptyMemberForm()); const [busy, setBusy] = useState(''); const [error, setError] = useState(''); @@ -145,6 +202,7 @@ export default function TenantSettingsPage() { ); const fieldCatalog = permissionsPayload.fieldGroups || permissionsPayload.fieldCatalog || []; const editingSystemRole = Boolean(selectedRole?.isSystem); + const activeRoleTemplates = roles.filter(item => item.status === 'active'); async function reloadSettings(preferredRoleId = selectedRoleId) { setError(''); @@ -172,8 +230,23 @@ export default function TenantSettingsPage() { } } + async function reloadMembers(preferredMemberId = selectedMemberId, keyword = memberKeyword) { + setError(''); + try { + const payload = await loadTenantMembers({ keyword, limit: 80 }); + const nextMembers = payload.items || []; + const nextSelected = nextMembers.find(item => item.id === preferredMemberId) || nextMembers[0] || null; + setMembers(nextMembers); + setSelectedMemberId(nextSelected?.id || ''); + setMemberForm(nextSelected ? memberFormFrom(nextSelected) : emptyMemberForm()); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '成员加载失败'); + } + } + useEffect(() => { void reloadSettings(''); + void reloadMembers(''); }, []); function startNewRole() { @@ -186,6 +259,16 @@ export default function TenantSettingsPage() { setRoleForm(roleTemplateFormFrom(item)); } + function startNewMember() { + setSelectedMemberId(''); + setMemberForm(emptyMemberForm()); + } + + function selectMember(item: TenantMemberItem) { + setSelectedMemberId(item.id); + setMemberForm(memberFormFrom(item)); + } + function updateBooleanMap(field: BooleanMapField, key: string) { setRoleForm(prev => ({ ...prev, @@ -196,6 +279,16 @@ export default function TenantSettingsPage() { })); } + function updateMemberPermission(key: string) { + setMemberForm(prev => ({ + ...prev, + permissions: { + ...prev.permissions, + [key]: !prev.permissions[key], + }, + })); + } + async function submitRoleTemplate() { if (!roleForm.name.trim() || !roleForm.code.trim()) { Taro.showToast({ title: '名称和编码必填', icon: 'none' }); @@ -267,6 +360,60 @@ export default function TenantSettingsPage() { } } + async function submitMember() { + const hasIdentity = memberForm.userId.trim() || memberForm.phone.trim() || memberForm.email.trim() || memberForm.username.trim() || memberForm.name.trim(); + if (!hasIdentity) { + Taro.showToast({ title: '请填写成员身份', icon: 'none' }); + return; + } + setBusy('save-member'); + setError(''); + try { + const selectedTemplate = activeRoleTemplates.find(item => item.id === memberForm.roleTemplateId); + const result = await upsertTenantMember({ + membershipId: memberForm.membershipId, + userId: memberForm.userId.trim() || undefined, + username: memberForm.username.trim() || null, + name: memberForm.name.trim() || null, + phone: memberForm.phone.trim() || null, + email: memberForm.email.trim() || null, + primaryRole: selectedTemplate?.baseRole || memberForm.role, + role: selectedTemplate?.baseRole || memberForm.role, + status: memberForm.status, + roleTemplateId: memberForm.roleTemplateId || null, + permissions: memberForm.permissions, + }); + const nextId = result.item?.id || memberForm.membershipId || ''; + Taro.showToast({ title: '成员已保存', icon: 'success' }); + await reloadMembers(nextId); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '成员保存失败'); + } finally { + setBusy(''); + } + } + + async function disableMember(item: TenantMemberItem) { + const confirmed = await Taro.showModal({ + title: '停用成员', + content: `确认停用 ${item.name || item.username || item.phone || item.email || item.userId}?`, + confirmText: '停用', + cancelText: '取消', + }); + if (!confirmed.confirm) return; + setBusy(`disable-member:${item.id}`); + setError(''); + try { + await disableTenantMember(item.id); + Taro.showToast({ title: '成员已停用', icon: 'success' }); + await reloadMembers(item.id); + } catch (nextError) { + setError(nextError instanceof Error ? nextError.message : '成员停用失败'); + } finally { + setBusy(''); + } + } + function renderToggleGroup(items: TenantPermissionCatalogItem[], field: BooleanMapField) { if (!items.length) return 暂无可配置项。; return ( @@ -284,6 +431,23 @@ export default function TenantSettingsPage() { ); } + function renderMemberPermissionGroup(items: TenantPermissionCatalogItem[]) { + if (!items.length) return 暂无可配置项。; + return ( + + {items.map(item => ( + + ))} + + ); + } + return ( @@ -473,6 +637,139 @@ export default function TenantSettingsPage() { + + + 成员与模板绑定 + + + + setMemberKeyword(String(event.detail.value || ''))} + /> + + + + {members.map(item => ( + + {item.name || item.username || item.phone || item.email || item.userId} + {item.role} · {item.status || 'active'} · {item.roleTemplateName || item.roleTemplateCode || '未绑定模板'} + {item.phone || '-'} · {item.email || '-'} + + + {item.status !== 'disabled' ? ( + + ) : null} + + + ))} + + {!members.length ? 暂无成员或搜索无结果。 : null} + + + + {selectedMemberId ? '编辑成员权限' : '新建成员权限'} + + setMemberForm(prev => ({ ...prev, userId: String(event.detail.value || '') }))} + /> + setMemberForm(prev => ({ ...prev, name: String(event.detail.value || '') }))} + /> + setMemberForm(prev => ({ ...prev, username: String(event.detail.value || '') }))} + /> + setMemberForm(prev => ({ ...prev, phone: String(event.detail.value || '') }))} + /> + setMemberForm(prev => ({ ...prev, email: String(event.detail.value || '') }))} + /> + + + + + 绑定角色模板 + + + {activeRoleTemplates.map(item => ( + + ))} + + + + + 成员基础角色 + + {BASE_ROLE_OPTIONS.map(item => ( + + ))} + + + + + 成员状态 + + {MEMBER_STATUS_OPTIONS.map(item => ( + + ))} + + + + + 成员额外权限覆盖 + {renderMemberPermissionGroup(permissionsPayload.permissions || [])} + + + + + + + + + {error ? {error} : null} diff --git a/apps/taro/src/pages/tenant-admin/workbench/index.tsx b/apps/taro/src/pages/tenant-admin/workbench/index.tsx index 0c3de45e..722a99f2 100644 --- a/apps/taro/src/pages/tenant-admin/workbench/index.tsx +++ b/apps/taro/src/pages/tenant-admin/workbench/index.tsx @@ -2,28 +2,84 @@ import { useEffect, useState } from 'react'; import Taro from '@tarojs/taro'; import { Button, Text, View } from '@tarojs/components'; import { getTenantContext } from '@/services/api'; -import { loadTenantDashboard, loadTenantOverview, type TenantDashboard, type TenantOverview } from '@/services/tenantAdmin'; +import { + loadTenantDashboard, + loadTenantOverview, + loadTenantPermissions, + type TenantDashboard, + type TenantOverview, + type TenantPermissionsPayload, +} from '@/services/tenantAdmin'; import '../admin.css'; +interface AdminModule { + key: string; + name: string; + path: string; + meta: string; + permission?: string; +} + +const MODULES: AdminModule[] = [ + { key: 'dashboard', name: '数据看板', path: '/pages/tenant-admin/dashboard/index', meta: '收益、注册、学习、内容', permission: 'dashboard:read' }, + { key: 'students', name: '学生运营', path: '/pages/tenant-admin/students/index', meta: '学生、班级、教师范围', permission: 'students:read' }, + { key: 'content', name: '题库内容', path: '/pages/tenant-admin/content/index', meta: '入口、导入、公共题库', permission: 'content:*' }, + { key: 'marketing', name: '营销中心', path: '/pages/tenant-admin/marketing/index', meta: '优惠券、激活码、分佣', permission: 'marketing:read' }, + { key: 'settings', name: '租户设置', path: '/pages/tenant-admin/settings/index', meta: '品牌、域名、支付、登录、角色', permission: 'tenant:overview:read' }, +]; + +function boolRecord(value: unknown) { + return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; +} + +function explicitPermission(permissions: Record, permission: string) { + const parts = permission.split(':').filter(Boolean); + const candidates = [permission]; + for (let index = parts.length - 1; index >= 1; index -= 1) { + candidates.push(`${parts.slice(0, index).join(':')}:*`); + } + candidates.push('*'); + for (const key of candidates) { + if (typeof permissions[key] === 'boolean') return permissions[key] as boolean; + } + return null; +} + +function canOpenModule(payload: TenantPermissionsPayload, item: AdminModule) { + const current = payload.current || {}; + const menuPermissions = boolRecord(current.menuPermissions); + if (typeof menuPermissions[item.key] === 'boolean') return menuPermissions[item.key] as boolean; + + if (!item.permission) return true; + const effectivePermissions = boolRecord(current.effectivePermissions); + const explicit = explicitPermission(effectivePermissions, item.permission); + if (explicit !== null) return explicit; + + const role = String(current.role || ''); + const defaults = payload.roleDefaults?.[role] || []; + return defaults.some(permission => { + if (permission === '*') return true; + if (permission === item.permission) return true; + if (permission.endsWith(':*')) return item.permission?.startsWith(permission.slice(0, -1)); + return false; + }); +} + export default function TenantWorkbenchPage() { const tenant = getTenantContext(); const [dashboard, setDashboard] = useState(null); const [overview, setOverview] = useState(null); + const [permissions, setPermissions] = useState({}); useEffect(() => { loadTenantDashboard('30d').then(payload => setDashboard(payload.item || null)).catch(() => setDashboard(null)); loadTenantOverview().then(payload => setOverview(payload.item || null)).catch(() => setOverview(null)); + loadTenantPermissions().then(payload => setPermissions(payload)).catch(() => setPermissions({})); }, []); const cards = dashboard?.cards || {}; const payment = dashboard?.paymentStats || {}; - const modules = [ - { name: '数据看板', path: '/pages/tenant-admin/dashboard/index', meta: '收益、注册、学习、内容' }, - { name: '学生运营', path: '/pages/tenant-admin/students/index', meta: '学生、班级、教师范围' }, - { name: '题库内容', path: '/pages/tenant-admin/content/index', meta: '入口、导入、公共题库' }, - { name: '营销中心', path: '/pages/tenant-admin/marketing/index', meta: '优惠券、激活码、分佣' }, - { name: '租户设置', path: '/pages/tenant-admin/settings/index', meta: '品牌、域名、支付、登录' }, - ]; + const modules = MODULES.filter(item => canOpenModule(permissions, item)); return ( @@ -63,6 +119,7 @@ export default function TenantWorkbenchPage() { ))} + {!modules.length ? 当前角色暂无可见后台模块。 : null} diff --git a/apps/taro/src/services/tenantAdmin.ts b/apps/taro/src/services/tenantAdmin.ts index e8d5a500..a9bf1f61 100644 --- a/apps/taro/src/services/tenantAdmin.ts +++ b/apps/taro/src/services/tenantAdmin.ts @@ -297,6 +297,45 @@ export interface TenantRoleTemplateInput { sortOrder?: number; } +export interface TenantMemberItem { + id: string; + userId: string; + username?: string | null; + name?: string | null; + phone?: string | null; + email?: string | null; + avatarUrl?: string | null; + primaryRole?: string | null; + role: string; + status?: string; + permissions?: Record; + roleTemplateId?: string | null; + roleTemplateCode?: string | null; + roleTemplateName?: string | null; + menuPermissions?: Record; + modulePermissions?: Record; + fieldPermissions?: Record; + dataScope?: Record; + legacyRole?: string | null; + lastSeenAt?: string | null; + createdAt?: string; + updatedAt?: string; +} + +export interface TenantMemberInput { + membershipId?: string; + userId?: string; + username?: string | null; + name?: string | null; + phone?: string | null; + email?: string | null; + primaryRole?: string | null; + role: string; + status?: string; + permissions?: Record; + roleTemplateId?: string | null; +} + export async function loadTenantDashboard(timeRange = '30d') { return apiRequest<{ item?: TenantDashboard }>('/api/tenant-admin/dashboard', { query: { timeRange } }); } @@ -459,6 +498,26 @@ export async function disableRoleTemplate(roleTemplateId: string) { }); } +export async function loadTenantMembers(query: { keyword?: string; role?: string; status?: string; limit?: number } = {}) { + return apiRequest<{ items?: TenantMemberItem[] }>('/api/tenant-admin/members', { + query: { ...query, limit: query.limit || 50 }, + }); +} + +export async function upsertTenantMember(input: TenantMemberInput) { + return apiRequest<{ item?: TenantMemberItem }>('/api/tenant-admin/members', { + method: 'PUT', + body: input, + }); +} + +export async function disableTenantMember(membershipId: string) { + return apiRequest<{ item?: TenantMemberItem }>('/api/tenant-admin/members/disable', { + method: 'POST', + body: { membershipId }, + }); +} + export async function loadCrmQueue(limit = 20) { return apiRequest<{ items?: Record[] }>('/api/crm/queue', { query: { limit } }); } diff --git a/docs/refactor/backend-handoff-roadmap.md b/docs/refactor/backend-handoff-roadmap.md index dde3c539..257d2d0a 100644 --- a/docs/refactor/backend-handoff-roadmap.md +++ b/docs/refactor/backend-handoff-roadmap.md @@ -22,7 +22,7 @@ | --- | --- | --- | --- | | 多租户底座 | 可联调 | 租户、域名、品牌、设置、RLS 基础、审计、Supabase JWT/API 身份映射 | 真实云端 Auth/JWKS 回归、生产 RLS 深测 | | 平台后台 | 基础完成 | 租户、套餐、订阅、账单、服务费、用量、公共题库授权、公共题库自动同步 worker、公共题库冲突单条/批量处理 API | 自动计费、平台审计、公共题库版本通知和运营消息 | -| 租户后台 | 可联调 | 品牌、域名、支付账户、登录配置、密钥掩码、活动、兑换码、优惠券、勋章管理/发放、成员权限、角色模板、菜单/模块/字段权限配置 API、班级/教师/学生范围权限;Taro 租户设置页已接角色模板操作台第一版 | 成员绑定模板、权限驱动菜单和更细的数据范围组合 | +| 租户后台 | 可联调 | 品牌、域名、支付账户、登录配置、密钥掩码、活动、兑换码、优惠券、勋章管理/发放、成员权限、角色模板、菜单/模块/字段权限配置 API、班级/教师/学生范围权限;Taro 工作台已接权限驱动模块入口,租户设置页已接角色模板和成员绑定操作台第一版 | 更细的数据范围组合、成员批量运营和完整权限菜单 | | 题库与练习 | 可联调 | 内容入口、任意深度分类、题目集合、顺序/随机/全真模拟蓝图、组卷快照、答题、错题、收藏、模考报告、排行榜、公共题库采纳快照、手动同步、自动同步 worker、冲突查询/单条和批量处理 API、JSON/试卷 payload 导出 | 专项策略、PDF/Word 导出 worker、公共题库版本通知、排行榜防刷/预聚合 | | 背单词 | 可联调 | 单元、单词、进度、收藏、统计、每日计划、JSON/CSV/Excel 导入、排行榜 | 更细复习参数 | | 知识手册 | 可联调 | 科目、章节、条目、Markdown 内容、嵌套 JSON/CSV/Excel 导入 | 富文本资源、版本管理、附件/PDF 关联 | @@ -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 JSON schema、报告渲染、PDF 生成 | -| Taro 前端 | 地基已建 | `apps/taro` 已有 Taro 4 React 工程、H5 三入口、租户解析、统一 API client、Supabase Auth client 初始化;学生端、租户后台和平台后台均已有第一批真实 API 页面;学生端已接地区选择、错题/收藏复习、题目反馈、视频解析、练习/模考报告、收银台、订单详情和售后入口第一版;平台后台已接关键写操作第一版,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版;租户设置页已接角色模板创建、编辑、停用、权限点、菜单、模块、字段和基础数据范围配置第一版 | 刷题细节 UI、权限驱动菜单、成员绑定模板、平台后台审计/详情增强、小程序兼容验证和端到端测试 | +| Taro 前端 | 地基已建 | `apps/taro` 已有 Taro 4 React 工程、H5 三入口、租户解析、统一 API client、Supabase Auth client 初始化;学生端、租户后台和平台后台均已有第一批真实 API 页面;学生端已接地区选择、错题/收藏复习、题目反馈、视频解析、练习/模考报告、收银台、订单详情和售后入口第一版;平台后台已接关键写操作第一版,租户工作台已接权限驱动模块入口,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版;租户设置页已接角色模板和成员绑定操作台第一版 | 刷题细节 UI、更细数据范围 UI、平台后台审计/详情增强、小程序兼容验证和端到端测试 | ## 前端接入建议 @@ -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,学生端已接地区选择、错题/收藏复习、题目反馈、视频解析、练习/模考报告、收银台、订单详情和售后入口第一版,平台后台关键写操作第一版已接入,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版,租户设置页已接角色模板创建/编辑/停用和权限可见性配置第一版;下一步补刷题细节 UI、成员绑定模板、权限驱动菜单、平台后台审计增强和小程序兼容验证。 +- Taro scaffold:`apps/taro` 地基已建立;学生端、租户后台、平台后台第一批 H5 页面已接真实 API,学生端已接地区选择、错题/收藏复习、题目反馈、视频解析、练习/模考报告、收银台、订单详情和售后入口第一版,平台后台关键写操作第一版已接入,租户工作台已接权限驱动模块入口,租户内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、字段模板预览/下载、上传/粘贴预览、字段别名覆盖、同步/异步导入、异步轮询和复检详情第一版,租户设置页已接角色模板创建/编辑/停用、成员绑定模板和权限可见性配置第一版;下一步补刷题细节 UI、更细数据范围 UI、平台后台审计增强和小程序兼容验证。 ### P1:商用收费和运营能力 @@ -93,7 +93,7 @@ ### P2:企业级体验和增长闭环 -- 租户自定义角色模板基础 API 与 Taro 权限配置 UI 第一版已完成;继续补成员绑定模板、权限驱动菜单、班级/教师/学生组合范围 UI 和平台级审计报表。 +- 租户自定义角色模板基础 API、Taro 权限配置 UI、成员绑定模板和工作台权限驱动入口第一版已完成;继续补班级/教师/学生组合范围 UI、成员批量运营和平台级审计报表。 - 三套默认主题、租户主题预览、Logo/图标/分享图配置。 - CRM worker:钉钉、飞书、企微机器人发送、签名、失败重试已落地;继续补轮询/定向分配、富卡片、失败告警和死信运营台。 - 销售/代理分佣结算、销售团队看板、客资跟进效果。 diff --git a/docs/refactor/frontend-handoff-index.md b/docs/refactor/frontend-handoff-index.md index 43eb17d1..ba26ddc7 100644 --- a/docs/refactor/frontend-handoff-index.md +++ b/docs/refactor/frontend-handoff-index.md @@ -26,7 +26,7 @@ ## 当前可进入的前端工作 - `apps/taro` 已经建立,且学生端第一批 H5 页面已经可构建:登录、首页、地区选择、题库、练习、错题/收藏、练习报告、视频解析、会员收银台、订单详情、背单词、知识手册、分数线、资料、个人中心。 -- 租户后台第一批 H5 页面已经可构建:工作台、数据看板、学生/班级、题库内容、营销中心、租户设置;题库内容页已具备公共题库采纳/同步、冲突查看、单条/批量采纳平台版本或保留本地版本、导入任务详情、异步轮询、导入问题查看、模板预览/下载、导入后复检详情、JSON/CSV/Excel 选择文件或粘贴内容、后端预览、字段别名覆盖和同步/异步执行导入的第一版操作能力;租户设置页已具备角色模板新建、编辑、停用、权限点、菜单、模块、字段和基础数据范围配置第一版。 +- 租户后台第一批 H5 页面已经可构建:工作台、数据看板、学生/班级、题库内容、营销中心、租户设置;工作台已接 `/api/tenant-admin/permissions` 做权限驱动模块入口;题库内容页已具备公共题库采纳/同步、冲突查看、单条/批量采纳平台版本或保留本地版本、导入任务详情、异步轮询、导入问题查看、模板预览/下载、导入后复检详情、JSON/CSV/Excel 选择文件或粘贴内容、后端预览、字段别名覆盖和同步/异步执行导入的第一版操作能力;租户设置页已具备角色模板新建、编辑、停用、成员搜索/新建、成员绑定模板、成员状态和额外权限覆盖第一版。 - 平台后台第一批 H5 页面已经可构建:工作台、租户管理、账务中心、公共题库授权。 - 可以继续复刻旧题库学生端主要视觉和交互:刷题细节、勋章展示和小程序端分享/支付体验。地区选择、视频解析、题目反馈、模考/练习报告、错题复习、收藏复习、商城收银台、订单详情和售后入口已经有第一版页面。 - 可以按新后端主模型接入内容导航: @@ -38,7 +38,7 @@ - H5 可以直接用 Supabase Auth access token 调 `apps/api`;后端已支持 JWT 验签和业务用户映射。 - H5 可以优先验证 `@supabase/supabase-js` 管理 Auth session;微信小程序端先验证运行时兼容性,业务数据默认仍走 `apps/api`。 - 可以接入租户品牌、主题、功能开关和域名/小程序参数解析。 -- 租户后台可以接入角色模板 API:`/api/tenant-admin/role-templates`,用于运营、教师、销售、代理等自定义菜单/模块/字段可见性。 +- 租户后台可以接入角色模板和成员 API:`/api/tenant-admin/role-templates`、`/api/tenant-admin/members`,用于运营、教师、销售、代理等自定义菜单/模块/字段可见性和成员模板绑定。 - 租户后台可以接入勋章管理和手动发放:`GET/PUT /api/tenant-admin/badges`、`GET/POST /api/tenant-admin/badge-grants`;学生端用 `GET /api/profile/badges` 展示成就。 ## 不能误认为已商用完成的部分 @@ -84,14 +84,14 @@ | 页面 | 文件 | 已接接口 | | --- | --- | --- | -| 工作台 | `apps/taro/src/pages/tenant-admin/workbench/index.tsx` | `tenant-admin/overview`、`tenant-admin/dashboard` | +| 工作台 | `apps/taro/src/pages/tenant-admin/workbench/index.tsx` | `tenant-admin/overview`、`tenant-admin/dashboard`、`tenant-admin/permissions` | | 数据看板 | `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`、`imports/detail`、`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`、`public-question-banks/conflicts/resolve`、`public-question-banks/conflicts/resolve-batch` | | 营销中心 | `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`、`permissions`、`GET/PUT role-templates`、`POST role-templates/disable` | +| 租户设置 | `apps/taro/src/pages/tenant-admin/settings/index.tsx` | `tenant-admin/overview`、`domains`、`payment-accounts`、`auth-providers`、`permissions`、`GET/PUT role-templates`、`POST role-templates/disable`、`GET/PUT members`、`POST members/disable` | -当前租户后台已有第一批运营操作:题库内容页支持公共题库采纳/同步、同步冲突查看、单条/批量采纳平台版本或保留本地版本、导入任务详情、异步 job 轮询、导入问题查看、字段映射/模板预览/下载、JSON/CSV/Excel 导入预览和执行、字段别名覆盖和导入后复检详情;租户设置页支持角色模板创建、编辑、停用、权限点、菜单、模块、字段和基础数据范围配置。下一批需要继续补完整后台写入表单、学生批量导入、成员绑定角色模板、权限驱动菜单和更细数据范围 UI。 +当前租户后台已有第一批运营操作:工作台按权限矩阵隐藏不可见模块;题库内容页支持公共题库采纳/同步、同步冲突查看、单条/批量采纳平台版本或保留本地版本、导入任务详情、异步 job 轮询、导入问题查看、字段映射/模板预览/下载、JSON/CSV/Excel 导入预览和执行、字段别名覆盖和导入后复检详情;租户设置页支持角色模板创建、编辑、停用、权限点、菜单、模块、字段、基础数据范围配置、成员搜索/新建、成员绑定模板、成员状态和额外权限覆盖。下一批需要继续补完整后台写入表单、学生批量导入、更细数据范围 UI 和成员批量运营。 ## 已落地的 Taro 平台后台页面 diff --git a/docs/refactor/next-development-todo.md b/docs/refactor/next-development-todo.md index 8465c5f5..7ffff9c6 100644 --- a/docs/refactor/next-development-todo.md +++ b/docs/refactor/next-development-todo.md @@ -139,7 +139,8 @@ - 已完成租户内角色模板、菜单可见、模块可见、字段级权限和权限变更审计基础 API。 - 已完成班级/教师/学生范围权限 API,教师只能查看自己负责班级的学生。 - 已完成 Taro 租户设置页角色模板可视化配置第一版,支持创建、编辑、停用、权限点、菜单、模块、字段和基础数据范围配置。 - - 继续补成员绑定角色模板、权限驱动菜单和更细的数据范围 UI,例如地区、题库、销售团队、本人客资、班级学生组合规则。 + - 已完成 Taro 成员绑定模板第一版,支持成员搜索/新建、绑定角色模板、成员状态和额外权限覆盖;工作台已按后端权限矩阵隐藏不可见模块。 + - 继续补更细的数据范围 UI,例如地区、题库、销售团队、本人客资、班级学生组合规则,以及成员批量运营和完整后台菜单权限联动。 2. 主题系统 - 平台默认三套主题。 @@ -163,7 +164,7 @@ - H5 和小程序共用同一套业务 API client。 - 租户通过域名、小程序配置或启动参数解析。 - 页面主题、品牌、功能开关都从后端租户配置读取。 -- 当前已完成 H5 学生端、租户后台、平台后台三套构建入口和统一 API client;学生端、租户后台、平台后台都有第一批真实 API 页面;学生端已补地区选择、错题/收藏复习、题目反馈、视频解析、练习/模考报告、会员收银台、订单详情和售后入口第一版;平台后台已接入创建租户、状态变更、订阅、账单、收款、用量和公共题库授权第一版写操作;下一步补租户后台深度写操作、刷题细节 UI、状态管理和小程序兼容验证。 +- 当前已完成 H5 学生端、租户后台、平台后台三套构建入口和统一 API client;学生端、租户后台、平台后台都有第一批真实 API 页面;学生端已补地区选择、错题/收藏复习、题目反馈、视频解析、练习/模考报告、会员收银台、订单详情和售后入口第一版;平台后台已接入创建租户、状态变更、订阅、账单、收款、用量和公共题库授权第一版写操作;租户后台已接权限驱动工作台、角色模板和成员绑定第一版;下一步补租户后台 CRM/分佣深度写操作、刷题细节 UI、状态管理和小程序兼容验证。 ### 第一批页面 @@ -212,7 +213,7 @@ ## 推荐下一步顺序 -1. 补租户后台写操作台:公共题库采纳/同步、冲突查看、单条/批量冲突采纳平台或保留本地、导入问题、模板预览/下载、上传/粘贴 preview/import、字段映射编辑、异步导入轮询、导入后复检详情和角色模板配置已接第一版;继续补成员绑定模板、权限驱动菜单、CRM/分佣。 +1. 补租户后台写操作台:公共题库采纳/同步、冲突查看、单条/批量冲突采纳平台或保留本地、导入问题、模板预览/下载、上传/粘贴 preview/import、字段映射编辑、异步导入轮询、导入后复检详情、权限驱动工作台、角色模板配置和成员绑定模板已接第一版;继续补 CRM/分佣深度写操作、成员批量运营和更细数据范围 UI。 2. 继续补 Taro 学生端旧体验:地区选择、视频播放、反馈、模考报告、错题/收藏专题、收银台、订单详情和售后入口已接第一版;继续补刷题细节 UI、小程序支付容器、分享场景和状态管理。 3. 补平台后台增强:租户详情/编辑、平台审计报表、自动计费、账单批量操作和更细平台权限点。 4. 云服务器部署 Supabase/PostgreSQL 和 API,配置对象存储生产环境变量,跑 `check:refactor` 的远程等价测试。 diff --git a/docs/refactor/taro-frontend-integration.md b/docs/refactor/taro-frontend-integration.md index 8abb96c9..5e51747a 100644 --- a/docs/refactor/taro-frontend-integration.md +++ b/docs/refactor/taro-frontend-integration.md @@ -1408,7 +1408,7 @@ src/services/learning.ts 练习 session、答题、收藏、错题/收藏复 src/services/commerce.ts 套餐、优惠券、下单、支付参数、订单详情、权益、激活码 src/services/profile.ts 个人中心、地区目标、签到、反馈、勋章、倒计时 src/services/video.ts 题目视频列表、播放签名 -src/services/tenantAdmin.ts 租户后台看板、学生、内容、营销、设置、角色模板写操作、公共题库采纳/同步/单条和批量冲突处理、导入详情/复检 +src/services/tenantAdmin.ts 租户后台看板、权限矩阵、成员、学生、内容、营销、设置、角色模板写操作、公共题库采纳/同步/单条和批量冲突处理、导入详情/复检 src/services/platformAdmin.ts 平台后台租户、套餐账单、用量、公共题库授权 ``` @@ -1426,7 +1426,7 @@ npm run build:taro:h5:platform 下一批前端开发重点: - 学生端:地区选择、题目视频播放、题目反馈、错题/收藏专题页、模考交卷报告、收银台、订单详情和售后入口已接第一版;下一批继续补刷题细节 UI、小程序支付容器和分享场景。 -- 租户后台:题库内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、模板预览/下载、异步任务轮询和导入后复检第一版;租户设置页已接角色模板创建、编辑、停用、权限点、菜单、模块、字段和基础数据范围配置第一版;下一批继续补完整写入表单、学生批量导入、成员绑定角色模板、权限驱动菜单、CRM 分配和分佣结算操作。 +- 租户后台:工作台已接权限驱动模块入口;题库内容页已接公共题库采纳/同步、冲突查看、单条/批量采纳平台或保留本地、导入问题、模板预览/下载、异步任务轮询和导入后复检第一版;租户设置页已接角色模板创建、编辑、停用、权限点、菜单、模块、字段、基础数据范围、成员搜索/新建、成员绑定模板、成员状态和额外权限覆盖第一版;下一批继续补完整写入表单、学生批量导入、CRM 分配和分佣结算操作。 - 平台后台:租户创建、状态变更、订阅开通、账单生成、人工收款确认、用量录入、公共题库授权编辑已接第一版;继续补租户详情/编辑、平台审计、自动计费和批量账单操作。 - 小程序:验证 `Taro.login`、微信支付、分享 scene/referral、Supabase client 兼容性;如不稳定,保留 `apps/api/auth/*` 作为小程序登录适配层。 @@ -1440,7 +1440,7 @@ npm run build:taro:h5:platform - 支付账户/登录 provider/密钥引用 - 用户与成员权限 - 班级/教师/学生:`/api/tenant-admin/classes`、`classes/members`、`students`、`teachers`、`students/notes`、`students/followups` -- 角色模板:`GET/PUT /api/tenant-admin/role-templates`、`POST /api/tenant-admin/role-templates/disable`;当前 Taro 租户设置页已提供第一版创建、编辑、停用、权限点、菜单、模块、字段和基础数据范围配置。 +- 角色模板与成员:`GET/PUT /api/tenant-admin/role-templates`、`POST /api/tenant-admin/role-templates/disable`、`GET/PUT /api/tenant-admin/members`、`POST /api/tenant-admin/members/disable`;当前 Taro 租户设置页已提供第一版创建、编辑、停用、权限点、菜单、模块、字段、基础数据范围、成员绑定模板和成员状态配置。 - 内容入口/分类树/题目集合/练习蓝图 - 题目/单词/知识手册/分数线/视频维护 - 题目/单词/知识手册/分数线/视频 JSON/CSV/Excel 导入 preview/import/issues、字段映射、模板下载和导入后复检