==============================================================================
盛果智能应用软件 V1.0 源代码文档
==============================================================================
软件全称:盛果智能应用软件 软件简称:盛果 版 本 号:V1.0
说明:本文档包含软件前30页和后30页源代码,每页50行。
前30页为软件前端代码,后30页为软件后端代码。
##############################################################################
##############################################################################
// ═══════════════════════ // 文件: my-uniapp-vue3/src/main.ts // ═══════════════════════ import { createSSRApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; import { useUserStore } from './store/user'; // #ifdef H5 import VConsole from 'vconsole'; // #endif export function createApp() { const app = createSSRApp(App); const pinia = createPinia(); app.use(pinia); // 初始化用户状态 const userStore = useUserStore(); userStore.initUser(); // #ifdef H5 // vConsole 开启,用于调试微信支付问题 new VConsole(); // #endif return {
app,
pinia,
}; }
// ═══════════════════════ // 文件: my-uniapp-vue3/src/App.vue // ═══════════════════════ .mini-player { position: fixed; bottom: 120rpx; left: 16rpx; right: 16rpx; height: 96rpx; background: rgba(255, 255, 255, 0.9); backdrop-filter: blur(28px); -webkit-backdrop-filter: blur(28px); border-radius: 48rpx; box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12), 0 0 0 0.5rpx rgba(0, 0, 0, 0.06); display: flex; align-items: center; padding: 0 16rpx;
// ═══════════════════════ // 文件: my-uniapp-vue3/src/components/AudioDownload.vue // ═══════════════════════
<slot>
<view class="default-btn" :class="{ downloading: downloading }">
<text class="icon">{{ downloading ? '⏳' : '⬇️' }}</text>
<text class="text">{{ downloading ? `下载中 ${progress}%` : '下载音频' }}</text>
<view v-if="downloading" class="progress-bar">
<view class="progress-fill" :style="{ width: progress + '%' }"></view>
</view>
</view>
</slot>
.credit-modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
display: flex;
align-items: flex-end;
justify-content: center;
opacity: 0;
transition: opacity 0.25s ease;
}
// ═══════════════════════ // 文件: my-uniapp-vue3/src/components/GenerationStatusBadge.vue // ═══════════════════════
<text class="gen-icon">{{ icon }}</text>
<text class="gen-label">{{ label }}</text>
.gen-badge {
display: inline-flex;
align-items: center;
gap: 6rpx;
padding: 6rpx 16rpx;
border-radius: 20rpx;
font-size: 22rpx;
font-weight: 500;
white-space: nowrap;
transition: all 0.3s ease;
user-select: none;
}
.gen-icon {
font-size: 20rpx;
}
.gen-label {
/* 由父级控制文字颜色 */
}
##############################################################################
##############################################################################
// ═══════════════════════ // 文件: server/src/app.ts // ═══════════════════════ // Auto-deploy test comment // 设置进程时区(从.env读取,确保日志和数据库时间一致) process.env.TZ = process.env.TZ || 'Asia/Shanghai'; import Koa from 'koa'; import cors from '@koa/cors'; import Router from '@koa/router'; import bodyParser from '@koa/bodyparser'; import koaBody from 'koa-body'; import serve from 'koa-static'; import mount from 'koa-mount'; import path from 'path'; import fs from 'fs'; import http from 'http'; import { config } from './config'; import { connectDatabase } from './models'; import { errorHandler } from './middleware/errorHandler'; import { requestLogger } from './services/requestLogger'; import { httpLogger } from './services/logger.service'; import { redisService } from './services/redis.service'; import { ossService } from './services/oss.service'; import { storageService } from './services/storage.service'; import { FFmpegProcessor } from './services/ffmpeg.processor'; import { resumeInterruptedTasks } from './modules/audio-project/book-queue.processor'; import { startTtsQueue, stopTtsQueue } from './modules/audio-project/tts-queue'; import { startAudioScanner, stopAudioScanner } from './modules/audio-project/audio-scanner'; import { queueService } from './services/queue.service'; import { validateModelsJsonStructure, printValidationReport } from './config/models-validator'; import { initSentry, sentryErrorHandler } from './services/sentry.service'; import { xssProtection, sqlInjectionProtection } from './middleware/security'; import { performanceMonitor, getMetrics } from './middleware/performance'; import { apiRateLimiter } from './middleware/rate-limiter'; import logRoutes from './services/log.controller'; import authRoutes from './modules/auth/auth.controller'; import ttsRoutes from './modules/tts/tts.controller'; import memberRoutes from './modules/member/member.controller'; import shareRoutes from './modules/share/share.controller'; import playerRoutes from './modules/player/player.controller'; import favoritesRoutes from './modules/favorites/favorites.controller'; import preferencesRoutes from './modules/preferences/preferences.controller'; import searchRoutes from './modules/search/search.controller'; import categoriesRoutes from './modules/categories/categories.controller'; import commentsRoutes from './modules/comments/comments.controller'; import notificationsRoutes from './modules/notifications/notifications.controller'; import bgmRoutes from './modules/bgm/bgm.controller'; import audioEditRoutes from './modules/audioedit/audioedit.controller'; import langGraphRoutes from './modules/audio-project/langgraph-controller'; import aiGenerateRoutes from './modules/audio-project/ai-generate-controller'; import albumRoutes from './modules/audio-project/album-controller'; import albumManagementRoutes from './modules/audio-project/album-management.controller'; import bookGeneratorRoutes from './modules/audio-project/audio-project.controller'; import playlistRoutes from './modules/player/playlist.controller'; import draftsRoutes from './modules/drafts/drafts.controller'; import videoGeneratorRoutes from './modules/video-generator/video-generator.controller'; import publishRoutes from './modules/publish/publish.controller'; import signRoutes from './modules/sign/sign.controller'; import subscriptionRoutes from './modules/subscription/subscription.controller'; import paymentRoutes from './modules/payment/payment.controller'; import historyRoutes from './modules/history/history.controller'; import historyBatchRoutes from './modules/history/history-batch.controller'; import feedbackRoutes from './modules/feedback/feedback.controller'; import queueRoutes from './modules/queue/queue.controller'; import inviteRoutes from './modules/invite/invite.controller'; import textToVideoRoutes from './modules/text-to-video/text-to-video.controller'; import pixelleVideoRoutes from './modules/pixelle-video/pixelle-video.controller'; import { initializePlans } from './modules/subscription/subscription.service'; import { initWebSocket } from './services/websocket.service.js'; const app = new Koa(); const router = new Router(); // 创建 HTTP 服务器 const server = http.createServer(app.callback()); // 中间件 app.use(errorHandler); app.use(sentryErrorHandler()); // Sentry 错误监控 app.use(performanceMonitor()); // 性能监控 app.use(httpLogger); // Winston 日志 app.use(xssProtection()); // XSS 防护 app.use(sqlInjectionProtection()); // SQL 注入防护 app.use(cors({ origin: '*', allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
// ═══════════════════════ // 文件: server/src/config/index.ts // ═══════════════════════ import dotenv from 'dotenv'; import path from 'path'; import fs from 'fs'; // 使用 process.cwd() 获取项目根目录(server目录) const projectRoot = process.cwd(); dotenv.config({ path: path.resolve(projectRoot, '.env') }); // 加载模型配置 const modelsConfig = JSON.parse(fs.readFileSync(path.join(__dirname, 'models.json'), 'utf-8')); // 获取所有模型(扁平化) function getAllModels() { const models: any[] = []; for (const [vendorKey, vendor] of Object.entries(modelsConfig.vendors) as [string, any][]) {
for (const model of vendor.models) {
models.push({
...model,
vendor: vendorKey,
vendorName: vendor.name,
baseUrl: vendor.baseUrl,
apiKey: vendor.apiKey,
apiType: vendor.apiType,
});
}
} return models; } // 获取启用的模型 function getEnabledModels() { return getAllModels().filter((m: any) => m.enabled); } // 根据 ID 获取模型配置 function getModel(id: string) { return getAllModels().find((m: any) => m.id === id); } // 根据类型获取模型列表 (text, tts, image, video) function getModelsByType(type: string) { return getAllModels().filter((m: any) => m.input?.includes(type) && m.enabled); } // 检查模型是否可切换(根据错误类型判断是否需要切换) function shouldSwitchModel(error: any): boolean { if (!error) return false; const message = (error?.message || error?.error?.message || '').toLowerCase(); const status = error?.status || error?.response?.status || 0; // 不可切换的错误:认证/权限/参数问题,换供应商也没用 const nonSwitchablePatterns = [
'invalid api key', 'invalid api-key', 'authentication', 'unauthorized',
'invalid token', 'token expired',
'permission denied', 'access denied',
'invalid request', 'bad request',
'invalidparameter', 'invalid_parameter', // TTS API 参数错误(如文本过短)
]; if (nonSwitchablePatterns.some(p => message.includes(p))) return false; if (status === 401) return false; // 认证失败 // 可切换的错误:限流、余额不足、服务不可用、模型不存在 const switchablePatterns = [
'rate limit', 'rate_limit', 'too many requests', '请求过于频繁',
'quota', 'balance', 'insufficient', 'usage limit',
'model not found', 'model not support', 'does not exist', 'invalid model',
'service unavailable', 'bad gateway', 'gateway timeout',
'internal server error',
]; if (switchablePatterns.some(p => message.includes(p))) return true; // HTTP 状态码判断 if ([429, 502, 503, 504, 500].includes(status)) return true; if (status === 403) return true; // 403 多数是配额/权限,换Key可能有效 if (status === 404) return true; // 模型不存在,换供应商 // 状态码文本匹配(兜底) if (['429', '502', '503', '504'].some(c => message.includes(c))) return true; return false; } // 获取下一个可用模型(用于自动切换) function getNextModel(currentModelId: string, type: string): string | null { const models = getModelsByType(type); const currentIndex = models.findIndex((m: any) => m.id === currentModelId); if (currentIndex === -1 || currentIndex >= models.length - 1) {
return null; // 没有下一个模型
} return models[currentIndex + 1].id; } export const config = { port: parseInt(process.env.PORT || process.env.SERVER_PORT || '3000', 10),
// ═══════════════════════ // 文件: server/src/models/index.ts // ═══════════════════════ import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient({ datasources: {
db: {
url: process.env.DATABASE_URL,
},
}, }); export async function connectDatabase(): Promise { try {
await prisma.$connect();
console.log(`📦 MySQL 连接成功 (${process.env.TZ || 'Asia/Shanghai'})`);
} catch (error) {
console.error('📦 MySQL 连接失败:', error);
throw error;
} } export { prisma };
// ═══════════════════════ // 文件: server/src/middleware/auth.ts // ═══════════════════════ import { Context, Next } from 'koa'; import jwt from 'jsonwebtoken'; import { config } from '../config'; import { UnauthorizedError, AppError } from './errorHandler'; import { JwtPayload } from '../types'; import { safeParseInt } from '../utils/safe-parse'; export async function authMiddleware(ctx: Context, next: Next): Promise { // 开发阶段默认跳过认证(除非显式设置 AUTH_ENABLED=true) const authEnabled = process.env.AUTH_ENABLED === 'true'; if (!authEnabled) {
ctx.state.user = {
userId: '1',
phone: 'test',
memberLevel: 99,
};
await next();
return;
} const authHeader = ctx.get('Authorization'); if (!authHeader) {
throw new UnauthorizedError('缺少 Authorization 头');
} const parts = authHeader.split(' '); if (parts.length !== 2 || parts[0] !== 'Bearer') {
throw new UnauthorizedError('Authorization 格式错误');
} const token = parts[1]; try {
const secret = config.jwt.secret;
const payload = jwt.verify(token, secret) as JwtPayload;
ctx.state.user = payload;
await next();
} catch (err: unknown) {
const error = err as Error;
if (error.name === 'TokenExpiredError') {
throw new UnauthorizedError('Token 已过期');
}
if (error.name === 'JsonWebTokenError') {
throw new UnauthorizedError('Token 无效');
}
throw new AppError('认证失败', 401, 401);
} } // 可选认证(允许未登录访问,自动使用测试用户) export async function optionalAuth(ctx: Context, next: Next): Promise { const authHeader = ctx.get('Authorization'); if (authHeader) {
const parts = authHeader.split(' ');
if (parts.length === 2 && parts[0] === 'Bearer') {
try {
const payload = jwt.verify(parts[1], config.jwt.secret) as JwtPayload;
ctx.state.user = payload;
} catch {
// 忽略错误,使用测试用户
ctx.state.user = {
userId: '1',
phone: 'test',
memberLevel: 99, // 超级VIP
};
}
}
} else {
// 没有认证头,使用测试用户(超级VIP)
ctx.state.user = {
userId: '1',
phone: 'test',
memberLevel: 99, // 超级VIP
};
} await next(); }
// ═══════════════════════ // 文件: server/src/middleware/security.ts // ═══════════════════════ /**
// ═══════════════════════ // 文件: server/src/middleware/errorHandler.ts // ═══════════════════════ import { Context, Next } from 'koa'; export async function errorHandler(ctx: Context, next: Next): Promise { try {
await next();
} catch (err: unknown) {
const error = err as Error & { status?: number; code?: number };
console.error('❌ Error:', error.message);
console.error('Stack:', error.stack);
ctx.status = error.status || 500;
ctx.body = {
code: error.code || 500,
message: error.message || '服务器内部错误',
data: null,
};
// 开发环境返回详细错误
if (process.env.NODE_ENV === 'development') {
(ctx.body as Record<string, unknown>).stack = error.stack;
}
} } // 自定义错误类 export class AppError extends Error { public code: number; public status: number; constructor(message: string, code: number = 500, status: number = 500) {
super(message);
this.code = code;
this.status = status;
this.name = 'AppError';
} } export class UnauthorizedError extends AppError { constructor(message: string = '未授权访问') {
super(message, 401, 401);
} } export class ForbiddenError extends AppError { constructor(message: string = '禁止访问') {
super(message, 403, 403);
} } export class NotFoundError extends AppError { constructor(message: string = '资源不存在') {
super(message, 404, 404);
} } export class BadRequestError extends AppError { constructor(message: string = '请求参数错误') {
super(message, 400, 400);
} } export class QuotaExceededError extends AppError { constructor(message: string = '使用次数已达上限') {
super(message, 429, 429);
} }
// ═══════════════════════ // 文件: server/src/middleware/rate-limiter.ts // ═══════════════════════ import { RateLimiterMemory, RateLimiterRedis } from 'rate-limiter-flexible'; import { redisService } from '../services/redis.service'; interface RateLimitOptions { points: number; // 限流点数 duration: number; // 时间窗口(秒) blockDuration?: number; // 封锁时间(秒) keyGenerator?: (ctx: any) => string; // 自定义键生成器 } // 内存限流器(Redis 不可用时使用) const memoryLimiters = new Map(); // Redis 限流器 const redisLimiters = new Map(); /**
rate_limit:${name}:,
points: options.points,
duration: options.duration,
blockDuration: options.blockDuration || options.duration * 2,
});
redisLimiters.set(name, limiter);
}
return redisLimiters.get(name)!;
} else {
if (!memoryLimiters.has(name)) {
const limiter = new RateLimiterMemory({
points: options.points,
duration: options.duration,
});
memoryLimiters.set(name, limiter);
}
return memoryLimiters.get(name)!;
}
}
/**api:${ctx.ip},
});
/**login:${ctx.ip},// ═══════════════════════ // 文件: server/src/modules/auth/auth.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { Context } from 'koa'; import * as AuthService from './auth.service'; import { BadRequestError } from '../../middleware/errorHandler'; import { authMiddleware } from '../../middleware/auth'; import { prisma } from '../../models'; import { safeParseInt } from '../../utils/safe-parse'; const router = new Router(); // 发送验证码 router.post('/send-code', async (ctx: Context) => { const { phone } = ctx.request.body as { phone: string }; if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
throw new BadRequestError('请输入正确的手机号');
} const code = AuthService.generateSmsCode(phone); // 开发环境直接返回验证码 ctx.body = {
code: 0,
message: '验证码发送成功',
data: {
phone,
// 总是返回验证码(方便测试)
code,
},
}; }); // 手机号登录 router.post('/login', async (ctx: Context) => { const { phone, code, inviteCode, openid } = ctx.request.body as { phone: string; code?: string; inviteCode?: string; openid?: string }; if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
throw new BadRequestError('请输入正确的手机号');
} // 关闭验证码验证(线上线下都关闭) if (!code || !/^\d{4,6}$/.test(code)) {
// 跳过验证码检查,直接登录
} const result = await AuthService.loginWithPhone(phone, code, inviteCode, openid); ctx.body = {
code: 0,
message: '登录成功',
data: result,
}; }); // 根据 openid 自动登录(公众号场景) router.post('/login-by-openid', async (ctx: Context) => { const { openid } = ctx.request.body as { openid: string }; if (!openid) {
throw new BadRequestError('openid 不能为空');
} const result = await AuthService.loginWithOpenid(openid); if (!result) {
ctx.body = {
code: -1,
message: '该微信未绑定账号,请先登录绑定',
data: null,
};
return;
} ctx.body = {
code: 0,
message: '自动登录成功',
data: result,
}; }); // 获取用户信息 router.get('/user-info', authMiddleware, async (ctx: Context) => { const userId = ctx.state.user.userId; const userInfo = await AuthService.getUserInfo(userId); ctx.body = {
code: 0,
message: 'success',
data: userInfo,
}; }); // 更新用户信息 router.put('/user-info', authMiddleware, async (ctx: Context) => { const userId = ctx.state.user.userId; const uid = safeParseInt(userId); const { nickname, avatar } = ctx.request.body as { nickname?: string; avatar?: string }; const user = await prisma.user.findUnique({ where: { id: uid } });
// ═══════════════════════ // 文件: server/src/modules/auth/auth.service.ts // ═══════════════════════ import jwt from 'jsonwebtoken'; import { v4 as uuidv4 } from 'uuid'; import { config } from '../../config'; import { prisma } from '../../models'; import { JwtPayload } from '../../types'; import { handleInvite } from '../invite/invite.service'; import { grantSignupBonus } from '../subscription/subscription.service'; // 验证码存储(生产环境应使用 Redis) const smsCodes = new Map(); // 生成验证码 export function generateSmsCode(phone: string): string { const code = Math.random().toString().slice(2, 6); smsCodes.set(phone, {
code,
expireAt: Date.now() + 5 * 60 * 1000, // 5分钟有效
});
console.log(📱 验证码已生成: ${phone} -> ${code});
return code;
}
// 验证验证码
export function verifySmsCode(phone: string, code: string): boolean {
const stored = smsCodes.get(phone);
if (!stored) return false;
if (Date.now() > stored.expireAt) {
smsCodes.delete(phone);
return false;
} if (stored.code !== code) return false; smsCodes.delete(phone); return true; } // 生成 JWT Token export function generateToken(userId: string, phone?: string): string { const secret = config.jwt.secret; const payload: Omit = { userId, phone }; return jwt.sign(payload, secret, {
expiresIn: '7d',
}); } // 手机号登录/注册 export async function loginWithPhone(phone: string, code?: string, inviteCode?: string, openid?: string): Promise<{ token: string; user: {
id: string;
phone: string;
nickname: string;
avatar: string;
memberLevel: number;
isNewUser: boolean;
}; }> { // 免密登录:code 为空或为"123456"时直接登录(线上线下都关闭验证) const skipVerify = !code || code === '123456'; if (!skipVerify) {
// 验证验证码
const isValid = verifySmsCode(phone, code || '');
if (!isValid) {
throw new Error('验证码错误或已过期');
}
} // 查找或创建用户 let user = await prisma.user.findFirst({ where: { phone } }); let isNewUser = false; if (!user) {
user = await prisma.user.create({
data: {
phone,
openid: openid || null, // 首次注册时绑定 openid
nickname: `用户${phone.slice(-4)}`,
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${phone}`,
memberLevel: 0,
dailyUsage: 0,
lastUsageDate: '',
},
});
isNewUser = true;
// 新用户注册赠送积分
await grantSignupBonus(user.id);
// 处理邀请关系
if (inviteCode) {
// ═══════════════════════ // 文件: server/src/modules/audio-project/audio-project.controller.ts // ═══════════════════════ /**
无效的步骤: ${step} };
return;
}
}
// 检查是否有正在运行的批量生成任务
const existingTask = runningTasks.get(bookId);
if (existingTask) {
ctx.status = 400;
ctx.body = {
code: 1,
message: '项目已有批量转换任务在运行,请先取消后再试',
data: { taskId: existingTask.taskId }
};
return;
}
// 创建批量生成任务
const { taskId, orchestrator } = await createBatchGenerationTask(bookId, steps);
// 存储任务信息
runningTasks.set(bookId, { taskId, bookId });
// 后台执行任务
(async () => {
try {
const result = await orchestrator.execute();
console.log([BatchGen][${taskId}] 任务完成:, result);
// 发送完成消息
const { pushBatchGenerationProgress } = await import('../../services/websocket.service.js');
pushBatchGenerationProgress(taskId, 'completed', 100);
} catch (error: any) {
console.error([BatchGen][${taskId}] 任务异常:, error);// ═══════════════════════ // 文件: server/src/modules/audio-project/audio-project.service.ts // ═══════════════════════ /**
[BatchGen][${this.taskId}] ${step}: ${progress}% - ${message});
}
/**[BatchGen][${this.taskId}] 任务已取消);
throw new Error('TASK_CANCELLED');
}
}
/**// ═══════════════════════ // 文件: server/src/modules/audio-project/langgraph-controller.ts // ═══════════════════════ /**
: resolveGenLevel(scale); const genLevelDescriptions: Record = { 1: { label: '仅章', desc: '无节和小节,适合短文、长篇文本' }, 2: { label: '章→节', desc: '有节无小节,适合科普、商业书' }, 3: { label: '章→节→小节', desc: '完整三级结构,适合教材、技术书' }, }; const result: any = { scale, scaleConfig, audioMinutes: { min: estimateAudioMinutesFromWords(Math.round(totalWords * 0.8)), max: estimateAudioMinutesFromWords(Math.round(totalWords * 1.2)), avg: audioMinutes }, estimatedChapters, defaultGenLevel: genLevel, genLevelInfo: genLevelDescriptions[genLevel] || { label: '章→节', desc: '' }, }; // 如果提供了 userId,同时检查用户配额 if (userId) { const quotaCheck = await checkBookGenerationQuota(parseInt(userId), scale); result.quotaCheck = quotaCheck; } ctx.body = { code: 0, message: 'success', data: result }; }); /**
// ═══════════════════════ // 文件: server/src/modules/audio-project/audio-project.store.ts // ═══════════════════════ /**
[BookStore] 未知章节阶段: chapterId=${(ch as any).id}, genStage="${ch.genStage}",跳过该章节);
continue;
}
if (idx < minIdx) {
minIdx = idx;
}
}// ═══════════════════════ // 文件: server/src/modules/audio-project/audio-project.types.ts // ═══════════════════════ /**
// ═══════════════════════ // 文件: server/src/modules/audio-project/graph.ts // ═══════════════════════ /**
// ═══════════════════════ // 文件: server/src/modules/audio-project/nodes/content.node.ts // ═══════════════════════ /**
: '';
if (lastTopic.length > 3) {
return trimmed + 。以上就是关于${lastTopic}的讨论。;
}
return trimmed + '。以上就是本章的主要内容。';
}
/**
[Content] 父节点 #${parent.id} 状态转移失败:, err.message);
}
}
}
}
/**
// ═══════════════════════ // 文件: server/src/modules/audio-project/nodes/plan.node.ts // ═══════════════════════ /**
构建规划提示词 */ function buildPlanPrompt( title: string, description: string, bookScale: string ): string { const config = getScaleConfig(bookScale); const chapters = config?.chapters || 17; // 如果 description 里有明确字数要求,优先用它 const descWordCount = extractWordCountFromDescription(description); const totalWords = descWordCount !== null ? descWordCount
: (config?.totalWords || 130000); return `你对以下文本做全面分析规划。
标题:${title}
字数规模:约${totalWords}字,约${chapters}章
描述:${description}
对整篇文本做完整的结构分析。
判断这篇文本属于以下哪种类型:
教材/学术类:系统教学、理论知识体系完整
技术教程类:有操作步骤、实践指南
文学类:叙事性、描述性内容
商业/经管类:管理、营销、投资、创业
科普/大众类:普及科学知识,通俗易懂
其他类型
// ═══════════════════════ // 文件: server/src/modules/audio-project/nodes/outline.node.ts // ═══════════════════════ /**
[LangGraph] AI 返回章节数 ${actual} 超出上限 ${maxAllowed},截断);
outline.chapters = outline.chapters.slice(0, maxAllowed);
} else if (actual < minAllowed) {
console.warn([LangGraph] AI 返回章节数 ${actual} 少于下限 ${minAllowed},使用话题相关的默认章节补充);
const topicHint = state.topic?.substring(0, 30) || '';
const perChapterWords = Math.round(parseInt(state.bookScale) / minAllowed);
const gapTopics = [
概述与背景,
核心概念,
深入探究,
应用与实践,
总结与展望,
];
while (outline.chapters.length < minAllowed) {
const idx = outline.chapters.length;
const gapTitle = gapTopics[idx % gapTopics.length];
outline.chapters.push({
number: idx + 1,
title: topicHint ? 第${idx + 1}章 ${topicHint} - ${gapTitle} : 第${idx + 1}章 ${gapTitle},
summary: 本章围绕"${topicHint || gapTitle}"展开,介绍${gapTitle}相关内容。,
keyPoints: [gapTitle, '关键知识点', '实践要点'],
estimatedWords: perChapterWords,
});
}
}
// 在范围内(±20%)不做任何处理,让 AI 自己决定
}
// 短文类至少1个章节
if (outline.chapters.length === 0) {
console.warn('[LangGraph] AI 返回章节数为0,使用话题创建默认章节');
// ═══════════════════════ // 文件: server/src/modules/audio-project/nodes/quality-check.node.ts // ═══════════════════════ /**
\n## 第${ch.number}章:${ch.title}\n> 概述:${ch.summary || '无'}\n> 预估字数:${ch.estimatedWords || 0} | 实际字数:${countWords(ch.content)}\n\n;
const content = ch.content || '(空)';
chaptersText += header + content;
}
return [
{ role: 'system', content: QUALITY_CHECK_SYSTEM_PROMPT },
{
role: 'user',
content: 书名:《${bookTitle}》
主题:${bookTopic}
以下是 ${batchChapters.length} 个章节的完整内容,请逐一评估质量:
${chaptersText}
请输出 JSON 格式的质量评估报告。,
},
];
}
/**// ═══════════════════════ // 文件: server/src/modules/tts/tts.service.ts // ═══════════════════════ import path from 'path'; import fs from 'fs'; import { v4 as uuidv4 } from 'uuid'; import { config } from '../../config'; import { prisma } from '../../models'; import { VoiceParams, Voice } from '../../types'; import { AudioMerger } from './audio-merger'; import { aiSummaryService } from './ai-summary.service'; import { storageService } from '../../services/storage.service'; import { getTtsRegistry, getAvailableTtsProvider, startTtsHealthCheck } from './provider.registry'; import { ITtsProvider } from './provider.interface'; import { CircuitBreakerOpenError } from '../../common/circuit-breaker'; import { ProviderNode } from '../../common/provider-registry'; import { ttsLogger } from './tts-logger'; import axios from 'axios'; // ============ 统一音色定义(10个固定音色,前端使用)============ // 前端使用统一 ID,后端根据 Provider 类型映射到真实音色 const UNIFIED_VOICES: Voice[] = [ { id: 'voice_01', name: '温柔女声', gender: 'female', description: '柔和温暖,适合情感故事' }, { id: 'voice_02', name: '磁性男声', gender: 'male', description: '低沉有力,适合悬疑推理' }, { id: 'voice_03', name: '活泼女声', gender: 'female', description: '清新明亮,适合儿童故事' }, { id: 'voice_04', name: '知性女声', gender: 'female', description: '知性稳重,适合科普知识' }, { id: 'voice_05', name: '阳光男声', gender: 'male', description: '阳光活力,适合校园青春' }, { id: 'voice_06', name: '沧桑男声', gender: 'male', description: '成熟沧桑,适合历史军事' }, { id: 'voice_07', name: '甜美女声', gender: 'female', description: '甜美可爱,适合爱情都市' }, { id: 'voice_08', name: '清朗男声', gender: 'male', description: '清朗干练,适合职场商战' }, { id: 'voice_09', name: '亲切女声', gender: 'female', description: '亲切自然,适合日常叙事' }, { id: 'voice_10', name: '稚嫩童声', gender: 'female', description: '稚嫩天真,适合童话寓言' }, ]; // 统一音色 → 阿里云 真实音色映射 const ALIYUN_VOICE_MAP: Record = { voice_01: 'longanyang', voice_02: 'longsanshu_v3', voice_03: 'longhuhu_v3', voice_04: 'longyue_v3', voice_05: 'longyichen_v3', voice_06: 'longlaobo_v3', voice_07: 'longmiao_v3', voice_08: 'longshuo_v3', voice_09: 'longwan_v3', voice_10: 'longhuhu_v3', }; // 统一音色 → Edge-TTS 真实音色映射 const EDGE_VOICE_MAP: Record = { voice_01: 'zh-CN-XiaoxiaoNeural', // 温柔女声 → 晓晓 voice_02: 'zh-CN-YunxiNeural', // 磁性男声 → 云希 voice_03: 'zh-CN-XiaoyiNeural', // 活泼女声 → 晓依 voice_04: 'zh-CN-YunyangNeural', // 知性女声 → 云扬(新闻风格) voice_05: 'zh-CN-YunjianNeural', // 阳光男声 → 云健 voice_06: 'zh-CN-YunyangNeural', // 沧桑男声 → 云扬 voice_07: 'zh-CN-XiaoxiaoNeural', // 甜美女声 → 晓晓 voice_08: 'zh-CN-YunxiNeural', // 清朗男声 → 云希 voice_09: 'zh-CN-XiaoyiNeural', // 亲切女声 → 晓依 voice_10: 'zh-CN-XiaoshuangNeural', // 稚嫩童声 → 晓双(童声) }; function mapToProviderVoice(unifiedVoiceId: string, providerVendor: string): string { // Edge-TTS: 使用微软免费音色 if (providerVendor === 'edge') {
const mapped = EDGE_VOICE_MAP[unifiedVoiceId];
if (mapped) return mapped;
console.warn(`⚠️ [VoiceMap] Edge 未识别的音色ID: "${unifiedVoiceId}",降级使用默认音色`);
return 'zh-CN-XiaoxiaoNeural';
}
// 默认: 阿里云百炼 CosyVoice
const mapped = ALIYUN_VOICE_MAP[unifiedVoiceId];
if (mapped) return mapped;
// 不在映射表中(如遗留的 'cherry' 等旧 MiniMax 音色)→ 用 CosyVoice 默认音色
console.warn(⚠️ [VoiceMap] 未识别的音色ID: "${unifiedVoiceId}",降级使用默认音色 longyingling_v3);
return 'longyingling_v3';
}
// ============ Aliyun Instruct 情感/场景控制 ============
// 后期优化功能,暂不启用。启用时改为 true
const INSTRUCT_ENABLED = false;
interface EmotionScene {
emotion: string; // neutral | fearful | angry | sad | surprised | happy | disgusted
scene: string; // 闲聊互动 | 新闻播报 | 广告促销 | 比赛解说 | 一些儿童内容解说 | 语音导航 | 脱口秀表演
}
// 情感关键词库
const EMOTION_KEYWORDS: { emotion: string; keywords: string[] }[] = [
{ emotion: 'fearful', keywords: ['恐怖', '可怕', '惊悚', '恐惧', '阴森', '黑暗', '鬼', '死亡', '谋杀', '悬疑', '危险', '深渊', '噩梦'] },
{ emotion: 'sad', keywords: ['悲伤', '难过', '哭泣', '眼泪', '心痛', '遗憾', '孤独', '寂寞', '失落', '离别', '思念', '哀伤', '去世', '失去'] },
{ emotion: 'angry', keywords: ['愤怒', '生气', '怒火', '仇恨', '战斗', '厮杀', '复仇', '战争', '侵略', '暴怒'] },
{ emotion: 'happy', keywords: ['快乐', '开心', '幸福', '欢笑', '庆祝', '美好', '甜蜜', '温暖', '阳光', '喜悦', '高兴', '浪漫', '恋爱', '美好'] },
{ emotion: 'surprised', keywords: ['惊奇', '惊喜', '意外', '奇迹', '神奇', '魔法', '童话', '幻想', '奇妙'] },
{ emotion: 'disgusted', keywords: ['恶心', '厌恶', '肮脏', '丑陋', '卑鄙'] },
];
// ═══════════════════════ // 文件: server/src/modules/player/player.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { Context } from 'koa'; import * as PlayerService from './player.service'; import { BadRequestError } from '../../middleware/errorHandler'; import { optionalAuth } from '../../middleware/auth'; import { prisma } from '../../models'; // 测试用户ID(开发环境使用) const TEST_USER_ID = '1'; const router = new Router(); // 获取播放进度列表 router.get('/progress', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const { audioId } = ctx.query as { audioId?: string }; const records = await PlayerService.getPlayProgress(
userId,
audioId ? parseInt(audioId) : undefined
); ctx.body = {
code: 0,
message: 'success',
data: records,
}; }); // 保存播放进度 router.post('/progress', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const body = ctx.request.body as {
audioId: number;
progress: number;
duration: number;
}; const { audioId, progress, duration } = body; if (!audioId || typeof progress !== 'number' || typeof duration !== 'number') {
throw new BadRequestError('参数错误');
} const record = await PlayerService.savePlayProgress(userId, audioId, progress, duration); ctx.body = {
code: 0,
message: 'success',
data: record,
}; }); // 更新播放进度 (在 DELETE 路由之前定义) router.put('/progress/:audioId', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const audioId = parseInt(ctx.params.audioId as string); const body = ctx.request.body as { progress: number; duration?: number }; const { progress, duration } = body; if (typeof progress !== 'number') {
throw new BadRequestError('进度参数错误');
} const record = await PlayerService.updatePlayProgress(userId, audioId, progress, duration); ctx.body = {
code: 0,
message: 'success',
data: record,
}; }); // 删除播放记录 router.delete('/progress/:audioId', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const audioId = parseInt(ctx.params.audioId as string); await PlayerService.deletePlayRecord(userId, audioId); ctx.body = {
code: 0,
message: '删除成功',
}; }); // 批量删除播放记录 router.delete('/progress/batch', optionalAuth, async (ctx: Context) => { const userId = ctx.state.user?.userId || TEST_USER_ID; const { audioIds } = ctx.request.body as { audioIds: number[] }; if (!audioIds || !Array.isArray(audioIds)) {
throw new BadRequestError('参数错误');
} for (const audioId of audioIds) {
// ═══════════════════════ // 文件: server/src/modules/member/member.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { Context } from 'koa'; import * as MemberService from './member.service'; import { BadRequestError, NotFoundError } from '../../middleware/errorHandler'; import { authMiddleware } from '../../middleware/auth'; const router = new Router(); // 获取会员权益信息 router.get('/benefits', async (ctx: Context) => { const benefits = MemberService.getMemberBenefits(); ctx.body = {
code: 0,
message: 'success',
data: benefits,
}; }); // 获取用户会员信息(兼容前端 /api/member/info) router.get('/info', authMiddleware, async (ctx: Context) => { const userId = ctx.state.user.userId; const status = await MemberService.getMemberStatus(userId); ctx.body = {
code: 0,
message: 'success',
data: status,
}; }); // 获取用户会员状态 router.get('/status', authMiddleware, async (ctx: Context) => { const userId = ctx.state.user.userId; const status = await MemberService.getMemberStatus(userId); ctx.body = {
code: 0,
message: 'success',
data: status,
}; }); // 创建订单 router.post('/order', authMiddleware, async (ctx: Context) => { const userId = ctx.state.user.userId; const { productType } = ctx.request.body as { productType: 'monthly' | 'yearly' }; if (!['monthly', 'yearly'].includes(productType)) {
throw new BadRequestError('无效的产品类型');
} const result = await MemberService.createOrder(userId, productType); ctx.body = {
code: 0,
message: '订单创建成功',
data: result,
}; }); // 模拟支付(仅开发环境) router.post('/pay/mock', authMiddleware, async (ctx: Context) => { if (process.env.NODE_ENV === 'production') {
throw new BadRequestError('生产环境不可用');
} const userId = ctx.state.user.userId; const { orderNo } = ctx.request.body as { orderNo: string }; if (!orderNo) {
throw new BadRequestError('订单号不能为空');
} const result = await MemberService.mockPaymentSuccess(orderNo, userId); ctx.body = {
code: 0,
message: '支付成功',
data: result,
}; }); // 获取订单列表 router.get('/orders', authMiddleware, async (ctx: Context) => { const userId = ctx.state.user.userId; const { page = 1, pageSize = 10 } = ctx.query as { page?: string; pageSize?: string }; const result = await MemberService.getOrders(
userId,
Number(page) || 1,
Number(pageSize) || 10
); ctx.body = {
code: 0,
message: 'success',
data: result,
};
// ═══════════════════════ // 文件: server/src/modules/subscription/subscription.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { Context } from 'koa'; import * as SubscriptionService from './subscription.service'; import { BadRequestError } from '../../middleware/errorHandler'; import { authMiddleware, optionalAuth } from '../../middleware/auth'; import { prisma } from '../../models'; const router = new Router(); // 获取所有套餐列表 router.get('/plans', async (ctx: Context) => { const plans = await SubscriptionService.getPlans(); ctx.body = {
code: 0,
message: 'success',
data: { plans }
}; }); // 获取单个套餐详情 router.get('/plans/:id', async (ctx: Context) => { const planId = parseInt(ctx.params.id); const plan = await SubscriptionService.getPlanById(planId); ctx.body = {
code: 0,
message: 'success',
data: { plan }
}; }); // 获取用户订阅信息 router.get('/subscription', authMiddleware, async (ctx: Context) => { const userId = parseInt(ctx.state.user.userId); const subscription = await SubscriptionService.getUserSubscription(userId); ctx.body = {
code: 0,
message: 'success',
data: { subscription }
}; }); // 获取用户Token余额 router.get('/balance', authMiddleware, async (ctx: Context) => { const userId = parseInt(ctx.state.user.userId); const balance = await SubscriptionService.getUserTokenBalance(userId); ctx.body = {
code: 0,
message: 'success',
data: balance
}; }); // 获取Token使用记录 router.get('/usage', authMiddleware, async (ctx: Context) => { const userId = parseInt(ctx.state.user.userId); const { page = '1', pageSize = '20' } = ctx.query as { page?: string; pageSize?: string }; const result = await SubscriptionService.getTokenUsageList(
userId,
Number(page) || 1,
Number(pageSize) || 20
); ctx.body = {
code: 0,
message: 'success',
data: result
}; }); // 获取用户配额(兼容旧接口) router.get('/quota', authMiddleware, async (ctx: Context) => { const userId = parseInt(ctx.state.user.userId); const quota = await SubscriptionService.getUserQuota(userId); ctx.body = {
code: 0,
message: 'success',
data: quota
}; }); // 检查配额 router.post('/check-quota', authMiddleware, async (ctx: Context) => { const userId = parseInt(ctx.state.user.userId); const { tokens } = ctx.request.body as { tokens: number }; if (!tokens || tokens <= 0) {
throw new BadRequestError('请提供正确的Token数量');
} const result = await SubscriptionService.checkQuota(userId, tokens); ctx.body = {
// ═══════════════════════ // 文件: server/src/modules/payment/payment.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { Context } from 'koa'; import * as PaymentService from './payment.service'; import { BadRequestError } from '../../middleware/errorHandler'; import { authMiddleware } from '../../middleware/auth'; import { prisma } from '../../models'; import { safeParseInt } from '../../utils/safe-parse'; const router = new Router(); // 创建支付订单 router.post('/create', authMiddleware, async (ctx: Context) => { const userId = safeParseInt(ctx.state.user.userId); const { planId, paymentMethod, period = 'monthly', returnUrl } = ctx.request.body as {
planId: number;
paymentMethod: 'alipay' | 'wechat' | 'mock';
period?: 'monthly' | 'yearly';
returnUrl?: string;
}; if (!planId) {
throw new BadRequestError('请选择套餐');
} if (!['alipay', 'wechat', 'mock'].includes(paymentMethod)) {
throw new BadRequestError('请选择支付方式');
} const result = await PaymentService.createPaymentOrder(userId, planId, paymentMethod, period, returnUrl); ctx.body = {
code: 0,
message: '订单创建成功',
data: result
}; }); // 创建 Token 包支付订单 router.post('/token-packs/create', authMiddleware, async (ctx: Context) => { const userId = safeParseInt(ctx.state.user.userId); const { quantity, paymentMethod, returnUrl } = ctx.request.body as {
quantity: number;
paymentMethod: 'alipay' | 'wechat' | 'mock';
returnUrl?: string;
}; if (!quantity || quantity < 1) {
throw new BadRequestError('购买数量至少为1');
} if (!['alipay', 'wechat', 'mock'].includes(paymentMethod)) {
throw new BadRequestError('请选择支付方式');
} const result = await PaymentService.createTokenPackOrder(userId, quantity, paymentMethod, returnUrl); ctx.body = {
code: 0,
message: '订单创建成功',
data: result
}; }); // 模拟支付(仅开发环境) router.post('/mock', authMiddleware, async (ctx: Context) => { if (process.env.NODE_ENV === 'production') {
throw new BadRequestError('生产环境不可用');
} const userId = safeParseInt(ctx.state.user.userId); const { orderNo } = ctx.request.body as { orderNo: string }; if (!orderNo) {
throw new BadRequestError('订单号不能为空');
} const result = await PaymentService.mockPaymentSuccess(orderNo, userId); ctx.body = {
code: 0,
message: result.message,
data: result
}; }); // 支付宝异步通知回调 router.post('/alipay/notify', async (ctx: Context) => { const params = ctx.request.body as Record; console.log('[Alipay Notify] 收到异步通知:', params); // 验证签名 const signVerified = PaymentService.verifyAlipaySign(params); if (!signVerified) {
console.error('[Alipay Notify] 签名验证失败');
ctx.status = 400;
ctx.body = 'fail';
return;
}
// ═══════════════════════ // 文件: server/src/modules/favorites/favorites.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { Context } from 'koa'; import * as FavoritesService from './favorites.service'; import { BadRequestError } from '../../middleware/errorHandler'; import { optionalAuth } from '../../middleware/auth'; // 测试用户ID(开发环境使用) const TEST_USER_ID = '1'; const router = new Router(); // 获取收藏列表(兼容前端 /api/favorites/list) router.get('/list', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const favorites = await FavoritesService.getFavorites(userId); ctx.body = {
code: 0,
message: 'success',
data: favorites,
}; }); // 获取收藏列表 router.get('/', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const favorites = await FavoritesService.getFavorites(userId); ctx.body = {
code: 0,
message: 'success',
data: favorites,
}; }); // 添加收藏 router.post('/', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const body = ctx.request.body as { audioId: number }; const { audioId } = body; if (!audioId) {
throw new BadRequestError('音频ID不能为空');
} const favorite = await FavoritesService.addFavorite(userId, audioId); ctx.body = {
code: 0,
message: '收藏成功',
data: favorite,
}; }); // 取消收藏 router.delete('/:audioId', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const audioId = parseInt(ctx.params.audioId as string); await FavoritesService.removeFavorite(userId, audioId); ctx.body = {
code: 0,
message: '已取消收藏',
}; }); // 检查是否已收藏 router.get('/check/:audioId', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const audioId = parseInt(ctx.params.audioId as string); const isFavorited = await FavoritesService.isFavorited(userId, audioId); ctx.body = {
code: 0,
message: 'success',
data: { isFavorited },
}; }); export default router;
// ═══════════════════════ // 文件: server/src/modules/history/history.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { Context } from 'koa'; import { optionalAuth } from '../../middleware/auth'; import { prisma } from '../../models'; const TEST_USER_ID = '1'; const router = new Router(); // 获取音频生成历史列表(兼容前端 /api/history/list) router.get('/list', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const page = parseInt(ctx.query.page as string) || 1; const pageSize = parseInt(ctx.query.pageSize as string) || 20; const startDate = ctx.query.startDate as string; const where: any = {}; if (startDate) {
where.createdAt = { gte: new Date(startDate) };
} const [records, total] = await Promise.all([
prisma.audioRecord.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
prisma.audioRecord.count({ where }),
]); const list = records.map((r) => ({
_id: r.audioId,
id: r.audioId,
title: r.title,
text: r.text || '',
summary: '',
tags: [],
audioUrl: r.audioUrl || '',
audioDuration: r.audioDuration,
audioSize: r.audioSize,
wordCount: r.wordCount,
voiceId: r.voiceId,
voiceParams: r.voiceParams ? JSON.parse(r.voiceParams) : { speed: 1, pitch: 0, volume: 50 },
status: r.status,
isFavorite: false,
bookId: r.bookId,
createdAt: r.createdAt.toISOString(),
updatedAt: r.updatedAt.toISOString(),
})); ctx.body = {
code: 0,
message: 'success',
data: {
list,
total,
page,
pageSize,
totalPages: Math.ceil(total / pageSize),
},
}; }); // 获取音频生成历史列表 router.get('/', optionalAuth, async (ctx: Context) => { // 开发环境使用测试用户ID const userId = ctx.state.user?.userId || TEST_USER_ID; const page = parseInt(ctx.query.page as string) || 1; const pageSize = parseInt(ctx.query.pageSize as string) || 20; const startDate = ctx.query.startDate as string; const where: any = {}; if (startDate) {
where.createdAt = { gte: new Date(startDate) };
} const [records, total] = await Promise.all([
prisma.audioRecord.findMany({
where,
orderBy: { createdAt: 'desc' },
skip: (page - 1) * pageSize,
take: pageSize,
}),
prisma.audioRecord.count({ where }),
]); const list = records.map((r) => ({
_id: r.audioId,
id: r.audioId,
// ═══════════════════════ // 文件: server/src/modules/search/search.controller.ts // ═══════════════════════ import Router from '@koa/router'; import { searchService } from './search.service'; const router = new Router(); /**
// ═══════════════════════ // 文件: server/src/services/oss.service.ts // ═══════════════════════ import OSS from 'ali-oss'; import path from 'path'; import fs from 'fs'; interface OSSConfig { region: string; accessKeyId: string; accessKeySecret: string; bucket: string; endpoint?: string; } class OSSService { private client: OSS; private bucket: string; private cdnDomain?: string; constructor() {
const config: OSSConfig = {
region: process.env.OSS_REGION || 'oss-cn-hangzhou',
accessKeyId: process.env.OSS_ACCESS_KEY_ID || '',
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET || '',
bucket: process.env.OSS_BUCKET_NAME || '',
endpoint: process.env.OSS_ENDPOINT || 'oss-cn-hangzhou.aliyuncs.com',
};
this.client = new OSS(config);
this.bucket = config.bucket;
this.cdnDomain = process.env.OSS_CDN_DOMAIN;
} /**
[OSS] 文件上传成功: ${normalizedKey});
// 返回文件 URL
return this.getFileUrl(normalizedKey);
} catch (error) {
console.error('[OSS] 文件上传失败:', error);
throw new Error(OSS 上传失败: ${(error as Error).message});
}
}
/**
[OSS] Buffer 上传成功: ${normalizedKey});
return this.getFileUrl(normalizedKey);
} catch (error) {
console.error('[OSS] Buffer 上传失败:', error);
throw new Error(OSS Buffer 上传失败: ${(error as Error).message});
}
}
/**
audio/${audioId}/${path.basename(localPath)};
return this.uploadFile(localPath, objectKey);
// ═══════════════════════ // 文件: server/src/services/queue.service.ts // ═══════════════════════ /**
} // 任务进度回调 export type ProgressCallback = (progress: number, data?: any) => void; class QueueService { private queues: Map = new Map(); private progressCallbacks: Map = new Map(); private queueAvailable: boolean = true; constructor() {
// 如果 Redis 不可用,记录警告但不阻止程序运行
if (!redisService.isAvailable()) {
console.warn('[Queue] Redis 不可用,任务队列将无法正常工作');
this.queueAvailable = false;
}
} /**
// ═══════════════════════ // 文件: server/src/services/websocket.service.ts // ═══════════════════════ /**
[WS] 客户端连接: ${clientId}, 当前在线: ${clients.size});
}
/**[WS] 客户端断开: ${clientId}, 当前在线: ${clients.size});
}
/**[WS] 发送消息失败: ${clientId}, error);
return false;
}
}
/**[WS] 广播失败: ${clientId}, error);
}
}
});
}
// ============ 事件推送 ============
/**[WS] 推送 ${event}:, data);
// 广播给所有客户端(前端可根据 bookId 过滤)
broadcast(event, data);
}
/**[WS] 推送 ${event}:, data);
broadcast(event, data);
}
/**##############################################################################
##############################################################################