统一授权提示私有化口径

This commit is contained in:
WuYongJie 2026-05-28 17:37:27 +08:00
parent 7b9260d8d7
commit ec440c5c60
18 changed files with 60 additions and 60 deletions

View File

@ -2011,7 +2011,7 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
onClose={() => setShowUpgradeModal(false)}
featureKey="customTemplates"
featureName="自定义模板"
featureDescription="创建、复制、编辑和迁移自定义模板需要升级后才能使用。"
featureDescription="当前许可证未包含创建、复制、编辑和迁移自定义模板的授权。"
/>
{/* 素材迁移弹窗 */}

View File

@ -117,12 +117,12 @@ export default function BuiltInTemplateGrid({
'inline-flex items-center gap-1.5 shadow-none',
!canQuickCreate && 'opacity-75'
)}
title={canQuickCreate ? '快速创建副本' : '需要升级后才能创建自定义模板'}
title={canQuickCreate ? '快速创建副本' : '当前许可证未包含自定义模板功能'}
>
<svg className="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 3.5a.75.75 0 01.75.75v5h5a.75.75 0 010 1.5h-5v5a.75.75 0 01-1.5 0v-5h-5a.75.75 0 010-1.5h5v-5A.75.75 0 0110 3.5z" />
</svg>
{canQuickCreate ? '快速创建副本' : '升级后创建'}
{canQuickCreate ? '快速创建副本' : '授权后创建'}
</button>
</div>
</div>

View File

@ -58,7 +58,7 @@ export default function CustomTemplateSection({
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
{canManageCustomTemplates
? '这里列出你自己创建的模板,可继续编辑、复制或删除'
: '当前许可证未开放自定义模板功能,升级后可新建、复制、编辑和迁移模板'}
: '当前许可证未包含自定义模板功能,请联系管理员处理授权'}
</p>
</div>
</div>
@ -90,12 +90,12 @@ export default function CustomTemplateSection({
</div>
<div className="text-left">
<p className="text-sm font-semibold text-slate-800 dark:text-slate-200">
{canManageCustomTemplates ? '新建自定义模板' : '升级后新建模板'}
{canManageCustomTemplates ? '新建自定义模板' : '授权后可新建'}
</p>
<p className="text-[11px] text-slate-500 dark:text-slate-400 mt-1 leading-snug">
{canManageCustomTemplates
? '从空白模板起步,按照业务需求配置字段'
: '升级后可新建、复制和迁移自定义模板'}
: '当前不可新建、复制或迁移自定义模板'}
</p>
</div>
</button>

View File

@ -79,10 +79,10 @@ describe('CustomTemplateSection 授权提示', () => {
it('许可证不包含自定义模板时应显示前置授权提示并拦截新建入口', () => {
renderSection(false)
expect(screen.getByText('当前许可证未开放自定义模板功能,升级后可新建、复制、编辑和迁移模板')).toBeInTheDocument()
expect(screen.getByText('当前许可证未包含自定义模板功能,请联系管理员处理授权')).toBeInTheDocument()
expect(screen.getByTestId('pro-badge')).toBeInTheDocument()
fireEvent.click(screen.getByText('升级后新建模板'))
fireEvent.click(screen.getByText('授权后可新建'))
expect(callbacks.onRequestUpgrade).toHaveBeenCalledTimes(1)
expect(callbacks.onCreate).not.toHaveBeenCalled()

View File

@ -321,7 +321,7 @@ export async function POST(
if (!(await canUseCustomTemplates())) {
return NextResponse.json({
error: '自定义模板功能需要标准版或专业版许可证,请升级后使用',
error: '当前许可证未包含自定义模板功能,请联系管理员处理授权',
errorCode: 'LICENSE_REQUIRED',
}, { status: 403 })
}

View File

@ -384,7 +384,7 @@ export async function PUT(req: NextRequest, { params }: { params: { id: string }
if (!(await canUseCustomTemplates())) {
return NextResponse.json({
error: '自定义模板功能需要标准版或专业版许可证,请升级后使用',
error: '当前许可证未包含自定义模板功能,请联系管理员处理授权',
errorCode: 'LICENSE_REQUIRED',
}, { status: 403 })
}
@ -713,7 +713,7 @@ export async function DELETE(_: NextRequest, { params }: { params: { id: string
if (!(await canUseCustomTemplates())) {
return NextResponse.json({
error: '自定义模板功能需要标准版或专业版许可证,请升级后使用',
error: '当前许可证未包含自定义模板功能,请联系管理员处理授权',
errorCode: 'LICENSE_REQUIRED',
}, { status: 403 })
}

View File

@ -108,7 +108,7 @@ export async function POST(req: NextRequest) {
// 检查自定义模板功能是否可用
if (!(await canUseCustomTemplates())) {
return NextResponse.json({
error: '自定义模板功能需要标准版或专业版许可证,请升级后使用',
error: '当前许可证未包含自定义模板功能,请联系管理员处理授权',
errorCode: 'LICENSE_REQUIRED',
}, { status: 403 })
}

View File

@ -93,7 +93,7 @@ describe('departments route license guard', () => {
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toContain('部门隔离功能需要专业版许可证')
expect(data.error).toContain('当前许可证未包含部门隔离功能')
expect(prisma.department.findUnique).not.toHaveBeenCalled()
})
@ -128,7 +128,7 @@ describe('departments route license guard', () => {
const data = await response.json()
expect(response.status).toBe(403)
expect(data.error).toContain('部门隔离功能需要专业版许可证')
expect(data.error).toContain('当前许可证未包含部门隔离功能')
expect(prisma.department.findUnique).not.toHaveBeenCalled()
})

View File

@ -39,7 +39,7 @@ export async function POST(req: NextRequest) {
// 检查部门隔离功能是否可用
if (!(await canUseDepartmentIsolation())) {
return NextResponse.json(
{ error: '部门隔离功能需要专业版许可证,请升级后使用' },
{ error: '当前许可证未包含部门隔离功能,请联系管理员处理授权' },
{ status: 403 }
)
}
@ -93,7 +93,7 @@ export async function PATCH(req: NextRequest) {
if (!(await canUseDepartmentIsolation())) {
return NextResponse.json(
{ error: '部门隔离功能需要专业版许可证,请升级后使用' },
{ error: '当前许可证未包含部门隔离功能,请联系管理员处理授权' },
{ status: 403 }
)
}
@ -257,7 +257,7 @@ export async function DELETE(req: NextRequest) {
if (!(await canUseDepartmentIsolation())) {
return NextResponse.json(
{ error: '部门隔离功能需要专业版许可证,请升级后使用' },
{ error: '当前许可证未包含部门隔离功能,请联系管理员处理授权' },
{ status: 403 }
)
}

View File

@ -209,7 +209,7 @@ export async function PUT(request: NextRequest) {
// 检查系统设置功能是否可用(免费版只能查看,不能修改)
if (!(await canUseSystemSettings())) {
return NextResponse.json(
{ error: '系统设置功能需要标准版或专业版许可证,请升级后使用' },
{ error: '当前许可证未包含系统设置功能,请联系管理员处理授权' },
{ status: 403 }
)
}

View File

@ -4,7 +4,7 @@
* RestrictedToggle 组件
*
* 受限功能开关组件,集成 PRO 徽章和点击拦截逻辑
* 当功能受限时显示禁用状态,点击时触发升级弹窗
* 当功能受限时显示禁用状态,点击时触发授权提示弹窗
*
* @created 2025-12-16
* @component
@ -96,7 +96,7 @@ export default function RestrictedToggle({
)}
data-testid="restricted-hint"
>
升级到付费版解锁此功能
当前许可证未包含此功能
</p>
)}
</div>
@ -142,7 +142,7 @@ export default function RestrictedToggle({
</button>
</div>
{/* 升级弹窗 */}
{/* 授权提示弹窗 */}
<UpgradeModal
isOpen={showUpgradeModal}
onClose={() => setShowUpgradeModal(false)}

View File

@ -3,8 +3,8 @@
/**
* UpgradeModal 组件
*
* 升级提示弹窗,当免费版用户点击受限功能时显示
* 基于现有 Modal 组件构建,显示功能名称、限制说明和升级好处
* 授权提示弹窗,当当前许可证不包含某个功能时显示
* 基于现有 Modal 组件构建,显示功能名称和限制说明
*
* @created 2025-12-16
* @component
@ -41,9 +41,8 @@ export default function UpgradeModal({
const displayName = featureName || feature.name
const displayDescription = featureDescription || feature.description
const handleUpgradeClick = () => {
// TODO: 跳转到升级页面或显示联系方式
// 目前先关闭弹窗
const handleContactAdminClick = () => {
// 目前先关闭弹窗,后续可接入客户自己的授权处理入口。
onClose()
}
@ -112,7 +111,7 @@ export default function UpgradeModal({
</button>
<button
type="button"
onClick={handleUpgradeClick}
onClick={handleContactAdminClick}
className={cn(
'flex-1 px-4 py-2 rounded-xl text-sm font-medium transition-all',
'bg-slate-900 text-white hover:bg-slate-800',

View File

@ -181,13 +181,13 @@ describe('RestrictedToggle 组件', () => {
expect(toggleButton.className).not.toContain('opacity-50')
})
it('受限时应该显示升级提示文字', () => {
it('受限时应该显示授权提示文字', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={true} />)
expect(screen.getByTestId('restricted-hint')).toBeInTheDocument()
expect(screen.getByText('升级到付费版解锁此功能')).toBeInTheDocument()
expect(screen.getByText('当前许可证未包含此功能')).toBeInTheDocument()
})
it('不受限时不应该显示升级提示文字', () => {
it('不受限时不应该显示授权提示文字', () => {
render(<RestrictedToggle {...defaultProps} isRestricted={false} />)
expect(screen.queryByTestId('restricted-hint')).not.toBeInTheDocument()
})

View File

@ -160,9 +160,9 @@
- 预制模板是否被禁用
- 是否是管理员草稿模板
### 为什么保存自定义模板提示需要升级?
### 为什么保存自定义模板提示许可证未包含?
说明当前许可证没有开启 `customTemplates`。需要激活标准版或专业版,或调整许可证能力。
说明当前许可证没有开启 `customTemplates`。请联系管理员确认当前授权范围,必要时激活包含自定义模板能力的许可证。
### 能不能删除正在使用的模板?

View File

@ -854,17 +854,17 @@ AI 改图以后会消耗外部额度、CPU、存储和客户素材数据。即
- 已完成:监控页按管理员能力收口。
- 后续如果要给普通成员开放部分统计,应单独做低敏概览接口,不复用当前系统状态接口。
### P2-02 模板管理免费版缺少前置提示(已修复)
### P2-02 模板管理许可证限制缺少前置提示(已修复)
客户影响:
历史风险是免费版用户在后台看到新建自定义模板入口,点击后才收到后端许可证错误,体验不完整。当前前端已读取许可证状态,在模板管理页提前展示授权限制提示;受限动作会打开中性的授权提示弹窗,后端仍保留强制拦截。
历史风险是当前许可证不包含自定义模板时,用户在后台仍看到新建入口,点击后才收到后端许可证错误,体验不完整。当前前端已读取许可证状态,在模板管理页提前展示授权限制提示;受限动作会打开中性的授权提示弹窗,后端仍保留强制拦截。
证据:
- `hooks/useLicenseStatus.ts`:前端通过 `/api/license/info` 读取当前许可证和功能开关。
- `app/admin/_components/TemplateManager.tsx`:模板管理页用 `isFree + customTemplates` 计算自定义模板能力;新建、编辑、复制和从预制模板快速创建都会先检查能力,未授权时打开 `UpgradeModal`。
- `app/admin/_components/template-manager/CustomTemplateSection.tsx`:自定义模板区免费版显示 `ProBadge` 和“当前许可证未开放自定义模板功能”提示;点击受限动作只触发授权提示,不直接打开编辑器。
- `app/admin/_components/TemplateManager.tsx`:模板管理页用许可证能力计算自定义模板能力;新建、编辑、复制和从预制模板快速创建都会先检查能力,未授权时打开授权提示弹窗。
- `app/admin/_components/template-manager/CustomTemplateSection.tsx`:自定义模板区在未授权时显示“当前许可证未包含自定义模板功能,请联系管理员处理授权”提示;点击受限动作只触发授权提示,不直接打开编辑器。
- `components/ui/UpgradeModal.tsx`:授权提示已收口为“当前许可证未包含此功能,请联系管理员处理授权”的私有化工作提示。
- `app/api/admin/upload-templates/route.ts`:后端创建模板前仍调用 `canUseCustomTemplates()`,失败返回 `403 + LICENSE_REQUIRED`。
- `app/admin/_components/template-manager/__tests__/CustomTemplateSection.test.tsx`:新增测试覆盖未授权时显示前置提示并拦截新建、编辑、复制和启停动作;授权允许时新建动作正常执行。
@ -872,9 +872,9 @@ AI 改图以后会消耗外部额度、CPU、存储和客户素材数据。即
建议动作:
- 已完成:免费版自定义模板入口有前置授权提示,受限动作不会直接进入编辑流程。
- 已完成:后端创建、更新、删除和迁移入口继续保留许可证强制。
- 仍需继续:后续统一把“升级后”话术改成“当前许可证未包含/联系管理员处理授权”,保持私有化交付语气一致。
- 已完成:自定义模板入口有前置授权提示,受限动作不会直接进入编辑流程。
- 已完成:后端创建、更新、删除和迁移入口继续保留许可证强制,错误信息统一为“当前许可证未包含/联系管理员处理授权”口径。
- 已完成:前端授权提示、按钮、通用受限开关、数量限制和系统设置/部门隔离/模板 API 的“升级后”话术已统一成私有化交付口径。
### P2-03 必填 checkbox 前后端语义不一致(已修复)
@ -1069,25 +1069,26 @@ NAS 私有化系统可能在平板或窄屏浏览器里使用。模板编辑器
- 拖拽只保留虚线占位和轻背景,不使用旋转、放大、bounce、pulse、强阴影。
- 实时预览默认可以折叠,避免占据过多编辑空间。
### P2-11 授权/付费提示偏公网 SaaS 营销话术(升级弹窗已先收口)
### P2-11 授权/付费提示偏公网 SaaS 营销话术(已完成第一轮收口)
客户影响:
当前项目目标是私有化交付。许可证限制应该像“当前许可证不包含此能力”的工作提示,而不是“升级后可享受”的营销弹窗。否则客户会感觉产品不是为私有部署准备的。升级弹窗已先改成中性限制提示,但 PRO 徽章、其它授权入口和部分页面样式仍需继续统一。
当前项目目标是私有化交付。许可证限制应该像“当前许可证不包含此能力”的工作提示,而不是“升级后可享受”的营销弹窗。否则客户会感觉产品不是为私有部署准备的。当前第一轮已把前端授权提示、通用受限开关、数量限制和核心授权 API 的可见话术统一成“当前许可证未包含/联系管理员处理授权”。
证据:
- `components/ui/ProBadge.tsx:6`:组件说明是付费功能 PRO 徽章。
- `components/ui/ProBadge.tsx:38`:PRO 徽章使用紫蓝渐变。
- `components/ui/UpgradeModal.tsx:62`、`components/ui/UpgradeModal.tsx:86`、`components/ui/UpgradeModal.tsx:111`、`components/ui/UpgradeModal.tsx:122`:升级弹窗已改成中性图标、限制提示文案和普通按钮,不再显示“升级后可享受”。
- `components/ui/__tests__/UpgradeModal.test.tsx`:测试已改为锁定限制提示和中性按钮文案。
- `components/ui/UpgradeModal.tsx`:授权提示弹窗已改成中性图标、限制提示文案和普通按钮,不再显示“升级后可享受”。
- `components/ui/RestrictedToggle.tsx`:受限开关提示已改为“当前许可证未包含此功能”。
- `lib/license/check-limits.ts`:用户数和素材数超限提示已改为联系管理员处理授权。
- `app/api/admin/upload-templates/route.ts`、`app/api/admin/upload-templates/[id]/route.ts`、`app/api/admin/upload-templates/[id]/migrate/route.ts`、`app/api/departments/route.ts`、`app/api/system-settings/route.ts`:授权不足错误已统一成私有化口径。
- `components/ui/ProBadge.tsx`:PRO 徽章仍使用紫色渐变,属于后续统一风格的一部分。
- `components/ui/__tests__/UpgradeModal.test.tsx`:测试已改为锁定限制提示和中性按钮文案。
建议动作:
- 改成许可证状态提示:功能不可用、当前版本、需要的版本、联系管理员/服务商。
- 去掉营销化“升级好处”列表。
- PRO 徽章改为中性许可证标签,不用紫蓝渐变。
- 已完成:可见“升级后/请升级”话术第一轮清理,改成许可证状态提示和联系管理员处理授权。
- 已完成:通用授权弹窗不再展示营销化“升级好处”。
- 仍需继续:PRO 徽章改为中性许可证标签,不用紫蓝渐变。
### P2-12 登录、激活、回调页风格和后台不统一

View File

@ -54,16 +54,16 @@ describe('canCreateUser', () => {
expect(mockCheckLimit).toHaveBeenCalledWith('users');
});
it('当达到限制时(FREE 版本)应该返回升级提示', async () => {
it('当达到限制时(FREE 版本)应该返回授权处理提示', async () => {
mockCheckLimit.mockResolvedValue({ allowed: false, current: 10, max: 10 });
mockGetLicenseType.mockResolvedValue('FREE');
const result = await canCreateUser();
expect(result.allowed).toBe(false);
expect(result.message).toContain('免费版');
expect(result.message).toContain('当前许可证');
expect(result.message).toContain('10');
expect(result.message).toContain('升级');
expect(result.message).toContain('联系管理员');
});
it('当达到限制时(非 FREE 版本)应该返回联系管理员提示', async () => {
@ -94,16 +94,16 @@ describe('canUploadMaterial', () => {
expect(mockCheckLimit).toHaveBeenCalledWith('materials');
});
it('当达到限制时(FREE 版本)应该返回升级提示', async () => {
it('当达到限制时(FREE 版本)应该返回授权处理提示', async () => {
mockCheckLimit.mockResolvedValue({ allowed: false, current: 100, max: 100 });
mockGetLicenseType.mockResolvedValue('FREE');
const result = await canUploadMaterial();
expect(result.allowed).toBe(false);
expect(result.message).toContain('免费版');
expect(result.message).toContain('当前许可证');
expect(result.message).toContain('素材');
expect(result.message).toContain('升级');
expect(result.message).toContain('联系管理员');
});
it('当达到限制时(非 FREE 版本)应该返回联系管理员提示', async () => {

View File

@ -25,7 +25,7 @@ export async function canCreateUser(): Promise<LimitCheckResult> {
...result,
message:
licenseType === 'FREE'
? `免费版最多支持 ${result.max} 个用户,当前已有 ${result.current} 个用户。请升级到标准版或专业版以解除限制。`
? `当前许可证最多支持 ${result.max} 个用户,当前已有 ${result.current} 个用户。请联系管理员处理授权。`
: `已达到用户数量上限(${result.max}),请联系管理员。`,
};
}
@ -45,7 +45,7 @@ export async function canUploadMaterial(): Promise<LimitCheckResult> {
...result,
message:
licenseType === 'FREE'
? `免费版最多支持 ${result.max} 个素材,当前已有 ${result.current} 个素材。请升级到标准版或专业版以解除限制。`
? `当前许可证最多支持 ${result.max} 个素材,当前已有 ${result.current} 个素材。请联系管理员处理授权。`
: `已达到素材数量上限(${result.max}),请联系管理员。`,
};
}

View File

@ -1,7 +1,7 @@
/**
* 功能描述映射
*
* 定义每个付费功能的名称、描述和升级好处
* 定义每个授权功能的名称和描述
* 用于 UpgradeModal 和其他需要展示功能信息的地方
*/
@ -12,7 +12,7 @@ export interface FeatureDescription {
name: string;
/** 功能描述 */
description: string;
/** 升级好处列表 */
/** 功能说明要点 */
benefits: string[];
}
@ -80,7 +80,7 @@ export const FEATURE_DESCRIPTIONS: Record<FeatureKey, FeatureDescription> = {
export function getFeatureDescription(featureKey: FeatureKey): FeatureDescription {
return FEATURE_DESCRIPTIONS[featureKey] || {
name: '高级功能',
description: '此功能需要升级许可证才能使用',
benefits: ['解锁更多功能', '获得技术支持', '享受更新服务'],
description: '当前许可证未包含此功能,请联系管理员处理授权',
benefits: ['联系管理员处理授权', '确认当前许可证范围', '完成后重新尝试操作'],
};
}