管理后台
Sistine Starter 管理后台完整使用指南
管理后台
管理后台为管理员提供了强大的用户管理、订阅管理和积分管理能力。本文档详细介绍管理员权限设置、功能模块使用和最佳实践。
管理员权限设置
权限标识
管理员权限通过数据库中的 user.role
字段标识:
该字段位于 user
表中,建表语句请参考 数据库
文档。
角色类型:
角色 | 标识 | 权限 |
---|---|---|
普通用户 | user | 访问用户功能 (仪表板、聊天、积分购买等) |
管理员 | admin | 访问所有用户功能 + 管理后台 |
创建管理员账户
方法 1: 使用 pnpm 命令 (推荐)
前置条件:
⚠️ 在运行命令之前,请确保:
- 你已经在网站上完成了账户注册(使用邮箱密码或 Google OAuth)
- 记住注册时使用的邮箱地址
项目提供了便捷的管理员权限提升脚本:
ADMIN_EMAIL=your-email@example.com pnpm admin:setup
重要说明:
- 将命令中的
your-email@example.com
替换为你在网站上注册的真实用户邮箱 - 此命令会将现有用户提升为管理员,而不是创建新账户
- 如果该邮箱对应的用户不存在,命令会失败
使用示例:
# 将 alice@company.com 用户提升为管理员
ADMIN_EMAIL=alice@company.com pnpm admin:setup
命令执行后的输出:
✅ 管理员权限设置成功!
📧 邮箱: alice@company.com
🔑 角色: admin
💰 当前积分: 300
方法 2: 数据库手动设置
如果已有账户需要提升为管理员:
使用 SQL 命令:
UPDATE "user"
SET "role" = 'admin'
WHERE "email" = 'your-email@example.com';
使用 Drizzle Studio:
# 启动 Drizzle Studio
pnpm db:studio
# 在浏览器中打开 https://local.drizzle.studio
# 找到对应用户,修改 role 字段为 'admin'
方法 3: 通过代码创建
import { db } from "@/lib/db";
import { user } from "@/lib/db/schema";
import { eq } from "drizzle-orm";
// 提升现有用户为管理员
await db
.update(user)
.set({ role: "admin" })
.where(eq(user.email, "admin@example.com"));
权限验证机制
管理后台使用 lib/auth/admin.ts
中的函数进行权限验证:
isAdmin()
检查当前用户是否为管理员:
import { isAdmin } from "@/lib/auth/admin";
const adminStatus = await isAdmin();
if (adminStatus) {
// 用户是管理员
}
实现逻辑:
export async function isAdmin(): Promise<boolean> {
const session = await auth.api.getSession({ headers: await headers() });
if (!session || !session.user) {
return false;
}
const dbUser = await db
.select({ role: user.role })
.from(user)
.where(eq(user.id, session.user.id))
.limit(1);
return dbUser[0]?.role === "admin";
}
requireAdmin()
保护管理员路由,非管理员自动重定向:
import { requireAdmin } from "@/lib/auth/admin";
export default async function AdminPage() {
await requireAdmin(); // 非管理员会被重定向到 /dashboard
// 管理员专属逻辑
}
实现逻辑:
export async function requireAdmin() {
const adminStatus = await isAdmin();
if (!adminStatus) {
redirect("/dashboard"); // 重定向到仪表板
}
}
getCurrentUserWithRole()
获取当前用户的完整信息 (包括角色):
import { getCurrentUserWithRole } from "@/lib/auth/admin";
const currentUser = await getCurrentUserWithRole();
if (currentUser?.role === "admin") {
// 执行管理员操作
}
功能模块
管理后台位于 app/[locale]/(admin)/admin/
目录,提供以下核心功能:
1. 管理面板概览
路由: /admin
(或 /zh/admin
, /en/admin
)
文件: app/[locale]/(admin)/admin/page.tsx
功能:
- 📊 系统统计数据展示
- 👥 最近注册用户
- 💰 最近支付记录
- 💬 对话使用统计
统计指标:
指标 | 说明 | 数据来源 |
---|---|---|
总用户数 | 所有注册用户 | user 表 |
活跃用户数 | 30天内有活动的用户 | user.updatedAt |
总支付次数 | 所有支付记录 | payment 表 |
总收入 | 成功支付的总金额 | payment.amountCents (status='succeeded') |
总对话数 | 所有聊天会话 | chatSession 表 |
总积分消耗 | 所有负值账本记录的总和 | creditLedger.delta < 0 |
示例代码 (简化版):
export default async function AdminPage() {
// 获取统计数据
const stats = {
totalUsers: await db.select({ count: sql`count(*)` }).from(user),
activeUsers: await db
.select({ count: sql`count(*)` })
.from(user)
.where(sql`${user.updatedAt} > NOW() - INTERVAL '30 days'`),
totalRevenue: await db
.select({ total: sql`sum(${payment.amountCents})` })
.from(payment)
.where(sql`${payment.status} = 'succeeded'`),
// ...
};
return <AdminDashboard stats={stats} />;
}
2. 用户管理
路由: /admin/users
文件: app/[locale]/(admin)/admin/users/page.tsx
功能:
查看所有用户
- 用户列表展示 (ID、邮箱、积分、角色、状态)
- 排序和筛选 (按创建时间、积分余额、订阅状态等)
- 查看用户详细信息
显示字段:
{
id: string;
name: string | null;
email: string;
emailVerified: boolean;
credits: number; // 当前积分
role: "user" | "admin"; // 角色
banned: boolean; // 是否封禁
banReason: string | null; // 封禁原因
banExpires: Date | null; // 封禁到期时间
planKey: string; // 订阅计划
createdAt: Date; // 注册时间
updatedAt: Date; // 最后活动时间
}
手动调整用户积分
API 端点: POST /api/admin/users/[userId]/credits
文件: app/api/admin/users/[userId]/credits/route.ts
请求格式:
POST /api/admin/users/user_123/credits
Content-Type: application/json
{
"amount": 1000, // 积分变动量 (正数=增加, 负数=扣除)
"reason": "admin_adjustment" // 变动原因
}
示例:
// 增加 1000 积分
fetch('/api/admin/users/user_123/credits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 1000,
reason: 'compensation'
})
});
// 扣除 500 积分
fetch('/api/admin/users/user_456/credits', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: -500,
reason: 'violation_penalty'
})
});
核心实现:
export async function POST(
request: NextRequest,
{ params }: { params: { userId: string } }
) {
// 1. 验证管理员权限
await requireAdmin();
const { amount, reason } = await request.json();
const delta = Number(amount);
// 2. 事务性更新
await db.transaction(async (tx) => {
// 更新用户积分
await tx
.update(user)
.set({ credits: sql`${user.credits} + ${delta}` })
.where(eq(user.id, params.userId));
// 记录到账本
await tx.insert(creditLedger).values({
id: crypto.randomUUID(),
userId: params.userId,
delta,
reason: reason || "adjustment",
});
});
return NextResponse.json({ success: true });
}
常见原因标识:
Reason | 说明 | 使用场景 |
---|---|---|
admin_adjustment | 管理员手动调整 | 默认原因 |
compensation | 补偿积分 | 服务故障补偿 |
bonus | 奖励积分 | 活动奖励 |
violation_penalty | 违规扣除 | 处罚用户违规 |
refund | 退款 | 订单退款 |
test | 测试用途 | 测试账号 |
修改用户订阅
API 端点: POST /api/admin/users/[userId]/subscription
文件: app/api/admin/users/[userId]/subscription/route.ts
请求格式:
POST /api/admin/users/user_123/subscription
Content-Type: application/json
{
"planKey": "pro_yearly", // 订阅计划 key
"action": "upgrade" // 操作类型: 'upgrade', 'downgrade', 'cancel'
}
可用计划:
free
- 免费计划starter_monthly
- Starter 月付starter_yearly
- Starter 年付pro_monthly
- Pro 月付pro_yearly
- Pro 年付
封禁/解封用户
封禁用户:
// 临时封禁 (7天)
await db
.update(user)
.set({
banned: true,
banReason: "Spam behavior",
banExpires: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // 7天后
})
.where(eq(user.id, userId));
// 永久封禁
await db
.update(user)
.set({
banned: true,
banReason: "Terms of service violation",
banExpires: null, // null 表示永久
})
.where(eq(user.id, userId));
解封用户:
await db
.update(user)
.set({
banned: false,
banReason: null,
banExpires: null,
})
.where(eq(user.id, userId));
封禁效果:
- 用户无法登录
- 所有 API 请求返回 403
- 前端显示封禁提示
3. 订阅管理
路由: /admin/subscriptions
文件: app/[locale]/(admin)/admin/subscriptions/page.tsx
功能:
- 查看所有活跃订阅
- 查看订阅详情 (计划、价格、到期时间)
- 查看订阅关联的用户信息
- 查看订阅的支付历史
显示信息:
{
subscriptionId: string;
userId: string;
userEmail: string;
planKey: string; // 计划标识
status: string; // 订阅状态
currentPeriodEnd: Date; // 当前周期结束时间
createdAt: Date; // 订阅创建时间
providerSubId: string; // Creem 订阅 ID
}
常见订阅状态:
Status | 说明 |
---|---|
active | 活跃订阅 |
canceled | 已取消 (但可能仍在有效期内) |
past_due | 逾期未支付 |
expired | 已过期 |
4. 积分管理
路由: /admin/credits
文件: app/[locale]/(admin)/admin/credits/page.tsx
功能:
- 查看所有积分变动记录 (creditLedger)
- 按用户、原因、时间筛选
- 导出积分账本数据
- 审计积分流向
显示字段:
{
id: string;
userId: string;
userEmail: string; // 关联的用户邮箱
delta: number; // 积分变化量
reason: string; // 变动原因
paymentId: string | null; // 关联的支付 ID
createdAt: Date; // 变动时间
}
筛选功能:
// 按用户筛选
const userCredits = await db
.select()
.from(creditLedger)
.where(eq(creditLedger.userId, userId));
// 按原因筛选
const chatUsage = await db
.select()
.from(creditLedger)
.where(eq(creditLedger.reason, "chat_usage"));
// 按时间范围筛选
const recentCredits = await db
.select()
.from(creditLedger)
.where(sql`${creditLedger.createdAt} > NOW() - INTERVAL '7 days'`);
审计功能:
检查用户积分余额与账本记录是否一致:
import { sql } from "drizzle-orm";
// 计算账本总和
const ledgerSum = await db
.select({ total: sql`SUM(delta)` })
.from(creditLedger)
.where(eq(creditLedger.userId, userId));
// 查询用户当前积分
const currentCredits = await db
.select({ credits: user.credits })
.from(user)
.where(eq(user.id, userId));
// 验证一致性
if (ledgerSum[0].total !== currentCredits[0].credits) {
console.error("Credits mismatch detected!");
}
安全考虑
1. 权限验证
每个管理员 API 端点都必须验证权限:
// ✅ 正确: 使用 requireAdmin()
export async function POST(request: NextRequest) {
await requireAdmin(); // 非管理员自动返回 403 或重定向
// 管理员操作
}
// ❌ 错误: 信任前端传来的角色
export async function POST(request: NextRequest) {
const { isAdmin } = await request.json(); // 不安全!
if (isAdmin) {
// 任何人都可以伪造请求
}
}
2. 操作审计
所有管理员操作都应记录日志:
// 记录管理员操作
await db.insert(adminAuditLog).values({
id: randomUUID(),
adminId: currentUser.id,
action: "adjust_credits",
targetUserId: userId,
details: JSON.stringify({ amount, reason }),
timestamp: new Date(),
});
3. 敏感操作二次确认
对于高风险操作,建议前端实现二次确认:
// 前端示例
function BanUserButton({ userId }) {
const handleBan = async () => {
// 二次确认
const confirmed = window.confirm(
"确定要封禁此用户吗?此操作将立即生效。"
);
if (!confirmed) return;
await fetch(`/api/admin/users/${userId}/ban`, { method: 'POST' });
};
return <button onClick={handleBan}>封禁用户</button>;
}
4. 限制管理员权限
不要给管理员过度权限:
// ✅ 正确: 管理员只能调整积分
await db.update(user)
.set({ credits: sql`${user.credits} + ${delta}` })
.where(eq(user.id, userId));
// ❌ 错误: 管理员可以修改任意字段
const { field, value } = await request.json();
await db.update(user)
.set({ [field]: value }) // 不安全! 可能修改密码、邮箱等
.where(eq(user.id, userId));
5. 防止管理员删除自己
export async function DELETE(
request: NextRequest,
{ params }: { params: { userId: string } }
) {
const currentUser = await getCurrentUserWithRole();
// 防止自删
if (currentUser?.id === params.userId) {
return NextResponse.json(
{ error: "Cannot delete your own account" },
{ status: 400 }
);
}
// 删除用户
await db.delete(user).where(eq(user.id, params.userId));
}
常见操作示例
1. 批量发放积分
为所有活跃用户发放积分奖励:
import { db } from "@/lib/db";
import { user, creditLedger } from "@/lib/db/schema";
import { sql } from "drizzle-orm";
async function grantBonusToActiveUsers() {
// 获取 30 天内活跃的用户
const activeUsers = await db
.select({ id: user.id })
.from(user)
.where(sql`${user.updatedAt} > NOW() - INTERVAL '30 days'`);
const bonusAmount = 500;
for (const u of activeUsers) {
await db.transaction(async (tx) => {
// 增加积分
await tx
.update(user)
.set({ credits: sql`${user.credits} + ${bonusAmount}` })
.where(eq(user.id, u.id));
// 记录账本
await tx.insert(creditLedger).values({
id: randomUUID(),
userId: u.id,
delta: bonusAmount,
reason: "monthly_active_bonus",
});
});
}
console.log(`✅ 已为 ${activeUsers.length} 个用户发放奖励`);
}
2. 导出用户数据
导出所有用户的基本信息和积分状态:
import { db } from "@/lib/db";
import { user } from "@/lib/db/schema";
import { writeFile } from "fs/promises";
async function exportUserData() {
const users = await db
.select({
email: user.email,
credits: user.credits,
planKey: user.planKey,
createdAt: user.createdAt,
})
.from(user);
// 转换为 CSV
const csv = [
"Email,Credits,Plan,Created At",
...users.map(u =>
`${u.email},${u.credits},${u.planKey},${u.createdAt.toISOString()}`
),
].join("\n");
await writeFile("users-export.csv", csv);
console.log("✅ 用户数据已导出到 users-export.csv");
}
3. 清理过期订阅
标记已过期但状态仍为 active 的订阅:
import { db } from "@/lib/db";
import { subscription } from "@/lib/db/schema";
import { sql } from "drizzle-orm";
async function cleanupExpiredSubscriptions() {
const result = await db
.update(subscription)
.set({ status: "expired" })
.where(
sql`${subscription.status} = 'active' AND ${subscription.currentPeriodEnd} < NOW()`
);
console.log(`✅ 已清理 ${result.rowCount} 个过期订阅`);
}
4. 查看用户的完整活动历史
async function getUserActivityReport(userId: string) {
const [userInfo, creditHistory, chatSessions, payments] = await Promise.all([
// 用户基本信息
db.select().from(user).where(eq(user.id, userId)),
// 积分历史
db
.select()
.from(creditLedger)
.where(eq(creditLedger.userId, userId))
.orderBy(desc(creditLedger.createdAt))
.limit(50),
// 聊天会话
db
.select()
.from(chatSession)
.where(eq(chatSession.userId, userId))
.orderBy(desc(chatSession.createdAt))
.limit(20),
// 支付记录
db
.select()
.from(payment)
.where(eq(payment.userId, userId))
.orderBy(desc(payment.createdAt)),
]);
return {
user: userInfo[0],
credits: creditHistory,
chats: chatSessions,
payments,
};
}
5. 测试用户初始化
为测试账号初始化大量积分:
async function setupTestUser(email: string) {
const testUser = await db
.select()
.from(user)
.where(eq(user.email, email))
.limit(1);
if (!testUser.length) {
throw new Error("User not found");
}
const userId = testUser[0].id;
const testCredits = 100000; // 10万测试积分
await db.transaction(async (tx) => {
await tx
.update(user)
.set({ credits: testCredits })
.where(eq(user.id, userId));
await tx.insert(creditLedger).values({
id: randomUUID(),
userId,
delta: testCredits,
reason: "test_account_setup",
});
});
console.log(`✅ 测试账号 ${email} 已初始化 ${testCredits} 积分`);
}
最佳实践
1. 操作前备份
在执行批量操作前,先备份数据:
# PostgreSQL 备份
pg_dump -h localhost -U postgres -d sistine_db > backup_$(date +%Y%m%d).sql
# 或使用 Drizzle
pnpm db:push -- --dry-run # 预览变更
2. 使用事务
所有涉及多步操作的管理功能都应使用事务:
// ✅ 正确: 使用事务
await db.transaction(async (tx) => {
await tx.update(user).set({ credits: 1000 }).where(eq(user.id, userId));
await tx.insert(creditLedger).values({ ... });
});
// ❌ 错误: 分离操作可能导致不一致
await db.update(user).set({ credits: 1000 }).where(eq(user.id, userId));
await db.insert(creditLedger).values({ ... }); // 如果这里失败,积分已改变
3. 记录详细日志
console.log(`[Admin] User ${userId} credits adjusted by ${delta} (${reason})`);
console.log(`[Admin] ${adminEmail} banned user ${targetUserEmail}`);
4. 提供撤销机制
对于可逆操作,保留撤销能力:
// 记录操作 ID,以便后续撤销
const operationId = randomUUID();
await db.insert(creditLedger).values({
id: operationId,
userId,
delta: amount,
reason: "admin_adjustment",
});
// 撤销操作
async function undoOperation(operationId: string) {
const original = await db
.select()
.from(creditLedger)
.where(eq(creditLedger.id, operationId));
if (original.length) {
// 反向操作
await refundCredits(original[0].userId, -original[0].delta, "undo");
}
}
5. 定期审计
定期检查数据一致性:
// 每周运行审计脚本
async function weeklyAudit() {
// 检查积分一致性
const users = await db.select().from(user);
for (const u of users) {
const ledgerSum = await db
.select({ total: sql`SUM(delta)` })
.from(creditLedger)
.where(eq(creditLedger.userId, u.id));
if (ledgerSum[0].total !== u.credits) {
console.error(`⚠️ Credits mismatch for user ${u.email}`);
}
}
}
故障排查
问题: 管理员无法访问后台
症状: 访问 /admin
被重定向到 /dashboard
原因:
user.role
不是'admin'
- 会话过期
- 数据库连接问题
解决:
// 检查用户角色
const user = await db
.select({ role: user.role })
.from(user)
.where(eq(user.email, "admin@example.com"));
console.log(user[0]?.role); // 应该是 'admin'
// 如果不是,手动设置
await db
.update(user)
.set({ role: "admin" })
.where(eq(user.email, "admin@example.com"));
问题: 积分调整失败
症状: 调整积分后余额未变化
原因:
- 事务失败未抛出错误
- 用户 ID 错误
- 数据库权限问题
解决:
// 添加详细日志
try {
await db.transaction(async (tx) => {
console.log(`Updating credits for user ${userId}`);
const result = await tx
.update(user)
.set({ credits: sql`${user.credits} + ${delta}` })
.where(eq(user.id, userId));
console.log(`Update result:`, result);
await tx.insert(creditLedger).values({
id: randomUUID(),
userId,
delta,
reason,
});
});
} catch (error) {
console.error("Transaction failed:", error);
throw error;
}
问题: 管理员被误删
症状: 唯一的管理员账户被删除
解决:
# 直接在数据库中创建新管理员
psql -d sistine_db
# 提升现有用户为管理员
UPDATE "user" SET "role" = 'admin' WHERE "email" = 'recovery@example.com';
# 或使用 pnpm 脚本
pnpm admin:setup
相关文档
总结
管理后台为管理员提供了:
- 用户管理: 查看、调整积分、修改订阅、封禁用户
- 订阅管理: 监控订阅状态和支付记录
- 积分管理: 审计积分流向和账本记录
- 统计分析: 系统整体运营数据
使用管理后台时,请遵循最佳实践:
- ✅ 始终验证管理员权限
- ✅ 使用事务保证数据一致性
- ✅ 记录所有管理员操作
- ✅ 定期审计数据完整性
- ✅ 备份数据后再执行批量操作