import { prisma } from '@/lib/prisma'; import { generateFingerprint, getInstanceId } from './fingerprint-service'; import { verifyLicenseSignature, encryptLocal, decryptLocal } from './crypto-service'; import { initializeSystem as initializeSystemCore } from '../system-init'; import type { LicenseConfig, SignedLicense, ActivateResponse, VerifyResponse, HeartbeatResponse, FeatureKey, LocalLicenseData, } from './types'; import { LICENSE_CONFIGS } from './types'; // 宽限期(7天) const GRACE_PERIOD_DAYS = 7; // 心跳间隔(1小时) const HEARTBEAT_INTERVAL = 60 * 60 * 1000; // 缓存的许可证配置 let cachedLicense: LicenseConfig | null = null; let lastVerified: Date | null = null; let heartbeatTimer: NodeJS.Timeout | null = null; function isLicenseConfigExpired(license: LicenseConfig | null): boolean { return Boolean(license?.expiresAt && new Date(license.expiresAt) < new Date()); } function extractLicenseConfig(license: SignedLicense): LicenseConfig { const { issuedAt: _issuedAt, ...config } = license.data; return config; } function normalizeLocalLicenseData(localData: LocalLicenseData): LocalLicenseData | null { if ( !localData || typeof localData.licenseKey !== 'string' || typeof localData.licenseData !== 'string' || typeof localData.lastVerified !== 'string' ) { return null; } try { const decrypted = decryptLocal(localData.licenseData); const signedLicense = JSON.parse(decrypted) as SignedLicense; if (!signedLicense?.data || !signedLicense?.signature) { return null; } if (!verifyLicenseSignature(signedLicense.data, signedLicense.signature)) { return null; } return { ...localData, cachedConfig: extractLicenseConfig(signedLicense), }; } catch (error) { console.warn('[License] 本地许可证校验失败:', error); return null; } } async function getCurrentLicenseConfig(): Promise { const localLicense = await getLocalLicense(); const currentConfig = localLicense?.cachedConfig ?? null; if (!localLicense || !currentConfig || isLicenseConfigExpired(currentConfig)) { cachedLicense = null; lastVerified = localLicense?.lastVerified ? new Date(localLicense.lastVerified) : null; return null; } cachedLicense = currentConfig; lastVerified = new Date(localLicense.lastVerified); return currentConfig; } /** * 激活许可证 * * ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API * 实际的激活逻辑已移至 app/api/license/activate/route.ts(后端代理) * * 安全改进: * - 客户端不再直接访问许可证服务器 * - 硬件指纹在后端生成(更安全) * - 许可证服务器地址完全隐藏 */ export async function activate(licenseKey: string): Promise<{ success: boolean; error?: string }> { try { // ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器) const response = await fetch('/api/license/activate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ licenseKey, }), }); const data = await response.json(); if (!data.success) { return { success: false, error: data.error || '激活失败' }; } // 重新加载许可证(因为后端已保存) await loadLicenseFromLocal(); // 更新缓存 if (cachedLicense) { lastVerified = new Date(); } return { success: true }; } catch (error) { console.error('激活失败:', error); return { success: false, error: '网络错误,请检查网络连接' }; } } /** * 激活免费试用版 * * ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API * 实际的注册逻辑已移至 app/api/license/activate-free/route.ts(后端代理) * * 安全改进: * - 客户端不再直接访问许可证服务器 * - 硬件指纹在后端生成(更安全) * - 许可证服务器地址完全隐藏 * * @param customerName 客户名称(用于许可证系统后台管理) * @param contactInfo 联系方式(手机/微信/邮箱) */ export async function activateFreeTrial(customerName: string, contactInfo: string = ''): Promise<{ success: boolean; error?: string }> { try { // ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器) const response = await fetch('/api/license/activate-free', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ customerName, contactInfo, }), }); const data = await response.json(); if (!data.success) { return { success: false, error: data.error || '激活失败,请检查网络连接' }; } // 重新加载许可证(因为后端已保存) await loadLicenseFromLocal(); // 更新缓存 if (cachedLicense) { lastVerified = new Date(); } return { success: true }; } catch (error) { console.error('免费试用激活失败:', error); return { success: false, error: '网络错误,请检查网络连接后重试' }; } } /** * 初始化系统(创建默认管理员、部门、店铺等) */ export async function initializeSystem() { return initializeSystemCore(); } /** * 验证许可证 * * ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API * 实际的验证逻辑已移至 app/api/license/verify/route.ts(后端代理) * * 安全改进: * - 客户端不再直接访问许可证服务器 * - 硬件指纹在后端生成(更安全) * - 许可证服务器地址完全隐藏 */ export async function verify(): Promise<{ valid: boolean; error?: string; revoked?: boolean }> { try { // 先检查本地缓存 const localLicense = await getLocalLicense(); if (!localLicense) { return { valid: false, error: '未找到许可证' }; } // ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器) const response = await fetch('/api/license/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); if (!data.valid) { // 检查是否是被吊销 if (data.revoked) { console.log('[License] 许可证已被吊销:', data.error); cachedLicense = null; return { valid: false, error: data.error || '许可证已被吊销', revoked: true }; } // 其他错误,检查宽限期 if (isWithinGracePeriod(localLicense.lastVerified)) { cachedLicense = localLicense.cachedConfig; return { valid: true }; } return { valid: false, error: data.error || '验证失败' }; } // 重新加载许可证(因为后端已更新) await loadLicenseFromLocal(); // 更新内存缓存 if (cachedLicense) { lastVerified = new Date(); } return { valid: true }; } catch (error) { console.error('验证失败:', error); // 网络错误时检查宽限期 const localLicense = await getLocalLicense(); if (localLicense && isWithinGracePeriod(localLicense.lastVerified)) { cachedLicense = localLicense.cachedConfig; return { valid: true }; } return { valid: false, error: '网络错误' }; } } /** * 发送心跳 * * ⚠️ 注意:此函数现在只用于客户端调用素材系统自己的API * 实际的心跳逻辑已移至 app/api/license/heartbeat/route.ts(后端代理) * * 安全改进: * - 客户端不再直接访问许可证服务器 * - 硬件指纹在后端生成(更安全) * - 许可证服务器地址完全隐藏 * * @returns 如果许可证被吊销返回 true */ export async function heartbeat(): Promise { try { const localLicense = await getLocalLicense(); if (!localLicense) { return false; } // ✅ 安全改进:调用素材系统自己的API(不再直接调用许可证服务器) const response = await fetch('/api/license/heartbeat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); // 检查是否被吊销 if (data.revoked) { console.log('[License] 心跳检测到许可证已被吊销'); cachedLicense = null; return true; // 返回 true 表示被吊销 } if (data.acknowledged) { // 重新加载许可证(因为后端已更新) await loadLicenseFromLocal(); lastVerified = new Date(); } return false; } catch (error) { console.error('心跳发送失败:', error); return false; } } /** * 获取当前许可证配置 */ export function getLicense(): LicenseConfig | null { if (isLicenseConfigExpired(cachedLicense)) { cachedLicense = null; return null; } return cachedLicense; } /** * 检查功能是否可用 */ export async function isFeatureEnabled(feature: FeatureKey): Promise { const currentLicense = await getCurrentLicenseConfig(); return Boolean(currentLicense?.features[feature]); } /** * 检查是否达到限制 */ export async function checkLimit(type: 'users' | 'materials'): Promise<{ allowed: boolean; current: number; max: number }> { const currentLicense = await getCurrentLicenseConfig(); if (!currentLicense) { return { allowed: false, current: 0, max: 0 }; } const max = type === 'users' ? currentLicense.limits.maxUsers : currentLicense.limits.maxMaterials; // -1 表示无限制 if (max === -1) { return { allowed: true, current: 0, max: -1 }; } const current = type === 'users' ? await prisma.user.count() : await prisma.material.count(); return { allowed: current < max, current, max, }; } /** * 获取许可证类型 */ export async function getLicenseType(): Promise { const currentLicense = await getCurrentLicenseConfig(); return currentLicense?.type || 'FREE'; } /** * 检查许可证是否已过期 */ export function isExpired(): boolean { return isLicenseConfigExpired(cachedLicense); } /** * 启动心跳定时器 */ export function startHeartbeat(): void { if (heartbeatTimer) { clearInterval(heartbeatTimer); } heartbeatTimer = setInterval(heartbeat, HEARTBEAT_INTERVAL); } /** * 停止心跳定时器 */ export function stopHeartbeat(): void { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } } /** * 初始化许可证服务 */ export async function initLicenseService(): Promise { const result = await verify(); if (result.valid) { startHeartbeat(); } } // ============ 内部辅助函数 ============ export async function saveLicenseLocally(licenseKey: string, license: SignedLicense): Promise { const instanceId = await getInstanceId(); const currentConfig = extractLicenseConfig(license); const localData: LocalLicenseData = { instanceId, licenseKey, licenseData: encryptLocal(JSON.stringify(license)), lastVerified: new Date().toISOString(), cachedConfig: currentConfig, }; await prisma.systemConfig.upsert({ where: { key: 'license' }, update: { value: JSON.stringify(localData) }, create: { key: 'license', value: JSON.stringify(localData) }, }); cachedLicense = isLicenseConfigExpired(currentConfig) ? null : currentConfig; lastVerified = new Date(localData.lastVerified); } export async function updateLocalLicense(license: SignedLicense): Promise { const existing = await getLocalLicense(); if (!existing) return; const currentConfig = extractLicenseConfig(license); const localData: LocalLicenseData = { ...existing, licenseData: encryptLocal(JSON.stringify(license)), lastVerified: new Date().toISOString(), cachedConfig: currentConfig, }; await prisma.systemConfig.update({ where: { key: 'license' }, data: { value: JSON.stringify(localData) }, }); cachedLicense = isLicenseConfigExpired(currentConfig) ? null : currentConfig; lastVerified = new Date(localData.lastVerified); } /** * 从本地加载许可证并更新内存缓存 */ async function loadLicenseFromLocal(): Promise { const localLicense = await getLocalLicense(); if (localLicense) { cachedLicense = localLicense.cachedConfig; lastVerified = new Date(localLicense.lastVerified); } else { cachedLicense = null; lastVerified = null; } } export async function getLocalLicense(): Promise { try { const config = await prisma.systemConfig.findUnique({ where: { key: 'license' }, }); if (!config) return null; const parsed = JSON.parse(config.value) as LocalLicenseData; return normalizeLocalLicenseData(parsed); } catch { return null; } } /** * 清除本地许可证缓存(许可证被吊销时调用) */ export async function clearLocalLicense(): Promise { try { await prisma.systemConfig.delete({ where: { key: 'license' }, }); console.log('[License] 本地许可证缓存已清除'); } catch (error) { console.error('[License] 清除本地许可证缓存失败:', error); } } function isWithinGracePeriod(lastVerifiedStr: string): boolean { const lastVerified = new Date(lastVerifiedStr); const gracePeriodEnd = new Date(lastVerified.getTime() + GRACE_PERIOD_DAYS * 24 * 60 * 60 * 1000); return new Date() < gracePeriodEnd; }