视频提前访问改为权限组级能力,人进组即获得,看板直接开关

This commit is contained in:
chenpei 2026-08-27 21:05:38 +08:00
parent d374a0bba2
commit d6a41d9283
10 changed files with 67 additions and 73 deletions

View File

@ -86,7 +86,7 @@ RUN test -s "/app/public/brand/header-logo.png" && \
test -f "/app/prisma/migrations/20260824_expand_ding_workflow_insights/migration.sql" && \
test -f "/app/prisma/migrations/20260826_add_ding_workflow_demand_type/migration.sql" && \
test -f "/app/prisma/migrations/20260827_add_ding_workflow_detail_fields/migration.sql" && \
test -f "/app/prisma/migrations/20260826_add_user_video_early_access/migration.sql"
test -f "/app/prisma/migrations/20260826_add_pack_video_early_access/migration.sql"
# 这两个动态路由负责兜底访问本地上传文件;如果它们在构建上下文里丢失,
# 线上音频/视频就会直接 404。这里提前失败,避免再把缺路由的镜像发出去。
@ -205,7 +205,7 @@ COPY --from=builder --chown=root:root /app/prisma/migrations/20260820_add_ding_w
COPY --from=builder --chown=root:root /app/prisma/migrations/20260824_expand_ding_workflow_insights ./prisma/migrations/20260824_expand_ding_workflow_insights
COPY --from=builder --chown=root:root /app/prisma/migrations/20260826_add_ding_workflow_demand_type ./prisma/migrations/20260826_add_ding_workflow_demand_type
COPY --from=builder --chown=root:root /app/prisma/migrations/20260827_add_ding_workflow_detail_fields ./prisma/migrations/20260827_add_ding_workflow_detail_fields
COPY --from=builder --chown=root:root /app/prisma/migrations/20260826_add_user_video_early_access ./prisma/migrations/20260826_add_user_video_early_access
COPY --from=builder --chown=root:root /app/prisma/migrations/20260826_add_pack_video_early_access ./prisma/migrations/20260826_add_pack_video_early_access
COPY --from=builder --chown=nextjs:nodejs /app/lib ./lib
COPY --from=builder --chown=nextjs:nodejs /app/scripts/public-traffic-rollup-from-nginx.py ./scripts/public-traffic-rollup-from-nginx.py
COPY --from=builder --chown=nextjs:nodejs /app/scripts/collapse-performance-survey-template-versions.mjs ./scripts/collapse-performance-survey-template-versions.mjs

View File

@ -22,7 +22,6 @@ export async function GET() {
isActive: true, // 保留,向后兼容
status: true, // 新增:用户状态(阶段一)
canDownload: true,
videoEarlyAccess: true, // 视频提前访问豁免(仅超管可开关)
department: true, // 保留,向后兼容
departmentId: true, // 新增:部门ID(阶段二)
managedDepartment: true, // 管理员负责的部门

View File

@ -1,64 +0,0 @@
// 切换用户「视频提前访问」豁免API(跳过视频类素材 15 天保护期)
// 仅超级管理员可操作,与"设为管理员"同级的高敏感授权。
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
// POST - 切换视频提前访问豁免(仅超级管理员)
export async function POST(request: NextRequest) {
try {
const session = await getServerSession(authOptions);
if (!session?.user || !(session.user as any).id) {
return NextResponse.json(
{ error: '未登录' },
{ status: 401 }
);
}
if (!(session.user as any).isSuperAdmin) {
return NextResponse.json(
{ error: '仅超级管理员可操作' },
{ status: 403 }
);
}
const { userId } = await request.json();
if (!userId) {
return NextResponse.json(
{ error: '缺少用户ID' },
{ status: 400 }
);
}
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
return NextResponse.json(
{ error: '用户不存在' },
{ status: 404 }
);
}
const updatedUser = await prisma.user.update({
where: { id: userId },
data: { videoEarlyAccess: !user.videoEarlyAccess },
});
return NextResponse.json({
message: updatedUser.videoEarlyAccess
? '已开启视频提前访问(跳过 15 天保护期)'
: '已关闭视频提前访问(恢复 15 天保护期)',
videoEarlyAccess: updatedUser.videoEarlyAccess,
});
} catch (error) {
console.error('Error toggling video early access:', error);
return NextResponse.json(
{ error: '操作失败' },
{ status: 500 }
);
}
}

View File

@ -69,6 +69,7 @@ export async function GET() {
id: true,
slug: true,
name: true,
videoEarlyAccess: true,
items: {
select: {
materialType: true,
@ -170,6 +171,7 @@ export async function GET() {
groups: packs.map((pack) => ({
slug: pack.slug,
name: pack.name,
videoEarlyAccess: pack.videoEarlyAccess,
permissions: packItemsToMap(pack.items),
departments: (departmentsByPack.get(pack.id) || []).map((path) => ({
path,
@ -232,6 +234,15 @@ export async function PUT(request: NextRequest) {
return NextResponse.json({ ok: true })
}
if (action === 'set-video-early-access') {
const enabled = body?.enabled === true
await prisma.materialPermissionPack.update({
where: { id: pack.id },
data: { videoEarlyAccess: enabled },
})
return NextResponse.json({ ok: true, videoEarlyAccess: enabled })
}
if (action === 'delete') {
await prisma.$transaction([
prisma.userPermissionInherit.deleteMany({ where: { packId: pack.id } }),

View File

@ -30,6 +30,7 @@ type Member = {
type Group = {
slug: string
name: string
videoEarlyAccess?: boolean
permissions: Record<string, MaterialPermissionFlags>
departments: Array<{ path: string; name: string }>
members: Member[]
@ -464,6 +465,35 @@ export default function PermissionGroupsDashboard() {
部门进组是底子,人进组是加餐。没单独进组的下级,跟着最近的上级组走。
</p>
</div>
<div className="flex items-center gap-3 rounded-lg border border-[#e2e8f0] bg-[#f8fafc] px-4 py-2.5">
<div className="min-w-0">
<p className="text-sm font-semibold text-[#101828]">视频提前访问</p>
<p className="mt-0.5 text-xs text-[#66758b]">
开启后,本组成员不受视频类素材 15 天保护期限制
</p>
</div>
<button
type="button"
role="switch"
aria-checked={Boolean(selected.videoEarlyAccess)}
disabled={saving}
onClick={() =>
void mutate(
{ action: 'set-video-early-access', slug: selected.slug, enabled: !selected.videoEarlyAccess },
selected.videoEarlyAccess ? '已关闭视频提前访问' : '已开启视频提前访问',
)
}
className={`relative h-6 w-11 flex-shrink-0 rounded-full transition-colors ${
selected.videoEarlyAccess ? 'bg-[#1677ff]' : 'bg-[#cfd8e3]'
}`}
>
<span
className={`absolute top-0.5 h-5 w-5 rounded-full bg-white shadow transition-all ${
selected.videoEarlyAccess ? 'left-[22px]' : 'left-0.5'
}`}
/>
</button>
</div>
<div className="flex flex-wrap gap-2">
<button
type="button"

View File

@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs'
import { resolveSecureAuthCookieForCurrentRequest } from '@/lib/auth-cookie'
import { authorizeDingtalkH5UserForNextAuth } from '@/lib/dingtalk-login'
import { resolveBrandCenterAccessForUser } from '@/lib/brand-center-access'
import { resolveUserVideoEarlyAccess } from '@/lib/user-permission-inherit'
const LEGACY_DINGTALK_AUTO_LOGIN_PASSWORD = 'dingtalk_auto_login'
@ -154,7 +155,7 @@ export const authOptions: NextAuthOptions = {
console.error('读取品牌中心访问范围失败,已按无权限处理:', error)
return false
})
token.videoEarlyAccess = (user as any).videoEarlyAccess === true
token.videoEarlyAccess = await resolveUserVideoEarlyAccess(user.id).catch(() => false)
} else if (token.id) {
const lastCheckedAt = Number((token as any).authCheckedAt || 0)
const accessClaimMissing = typeof (token as any).canAccessBrandCenter !== 'boolean'
@ -169,7 +170,6 @@ export const authOptions: NextAuthOptions = {
department: true,
managedDepartment: true,
isSuperAdmin: true,
videoEarlyAccess: true,
},
})
@ -184,7 +184,7 @@ export const authOptions: NextAuthOptions = {
token.isSuperAdmin = currentUser.isSuperAdmin
token.department = currentUser.department || ''
token.managedDepartment = currentUser.managedDepartment || null
token.videoEarlyAccess = currentUser.videoEarlyAccess === true
token.videoEarlyAccess = await resolveUserVideoEarlyAccess(String(token.id)).catch(() => false)
token.canAccessBrandCenter = await resolveBrandCenterAccessForUser(String(token.id), {
role: currentUser.role,
isSuperAdmin: currentUser.isSuperAdmin,

View File

@ -102,6 +102,24 @@ export async function loadUserInheritPack(userId: string): Promise<PermissionPac
}
}
/**
* 视频提前访问豁免:用户加入的任意权限组开启了该组级能力即获得。
* 部门进组是素材可见范围的底子,不参与此判定;豁免按"人进组是加餐"的口径只认个人挂载。
*/
export async function resolveUserVideoEarlyAccess(userId: string): Promise<boolean> {
if (!userId) return false
try {
const assignment = await prisma.userPermissionInherit.findFirst({
where: { userId, pack: { videoEarlyAccess: true } },
select: { id: true },
})
return Boolean(assignment)
} catch (error) {
if (isMissingInheritTable(error)) return false
throw error
}
}
export async function savePermissionPackItems(
slug: string,
permissions: Array<MaterialPermissionFlags & { materialType: string }>,

View File

@ -0,0 +1,2 @@
-- 视频提前访问:权限组级能力。人进组(UserPermissionInherit)即获得豁免,默认关闭,仅超级管理员在权限组看板开关。
ALTER TABLE "material_permission_packs" ADD COLUMN "video_early_access" BOOLEAN NOT NULL DEFAULT false;

View File

@ -1,2 +0,0 @@
-- 视频提前访问豁免:默认关闭,仅超级管理员可在用户管理中为个别用户开启。
ALTER TABLE "users" ADD COLUMN "video_early_access" BOOLEAN NOT NULL DEFAULT false;

View File

@ -18,7 +18,6 @@ model User {
role Role @default(USER)
isActive Boolean @default(false)
canDownload Boolean @default(true)
videoEarlyAccess Boolean @default(false) @map("video_early_access")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
department String @default("")
@ -391,6 +390,7 @@ model MaterialPermissionPack {
id String @id @default(uuid())
slug String @unique
name String
videoEarlyAccess Boolean @default(false) @map("video_early_access")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
items MaterialPermissionPackItem[]