线上配置2
Production deploy / build (push) Successful in 1m31s Details

This commit is contained in:
wendazhi 2026-02-12 19:07:41 +08:00
parent 33fdcac27f
commit ba3422afc4
10 changed files with 212 additions and 78 deletions

View File

@ -17,6 +17,7 @@
"ali-oss": "^6.23.0",
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"canvas": "^3.2.1",
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"epub": "^1.3.0",
@ -3065,13 +3066,13 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"proxy-from-env": "^1.1.0"
}
},
@ -3674,6 +3675,20 @@
],
"license": "CC-BY-4.0"
},
"node_modules/canvas": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz",
"integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^7.0.0",
"prebuild-install": "^7.1.3"
},
"engines": {
"node": "^18.12.0 || >= 20.9.0"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@ -4277,9 +4292,9 @@
}
},
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz",
"integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@ -7666,6 +7681,12 @@
"node": ">=10"
}
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"license": "MIT"
},
"node_modules/node-ensure": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/node-ensure/-/node-ensure-0.0.0.tgz",

View File

@ -1,64 +0,0 @@
# 课程生成功能测试指南
## 部署状态
✅ **代码已部署到服务器**
- 服务器地址: https://api.muststudy.xin
- PM2 服务: 运行中
- 修复内容: 大纲生成完成后自动开始生成课程内容
## 测试方法
### 方法1: 使用测试脚本(推荐)
1. **获取Token**
- 使用手机号登录获取Token
- 或使用现有的测试账号
2. **运行测试脚本**
```bash
export TEST_TOKEN='your_token_here'
cd backend
bash scripts/test-course-generation-direct.sh
```
### 方法2: 直接调用API
1. **创建课程**
```bash
curl -X POST https://api.muststudy.xin/api/ai/content/upload \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"content": "测试内容...",
"style": "essence"
}'
```
2. **查询状态**
```bash
curl -X GET https://api.muststudy.xin/api/my-courses \
-H 'Authorization: Bearer YOUR_TOKEN'
```
3. **查询日志**
```bash
curl -X GET "https://api.muststudy.xin/api/ai/prompts/logs?taskId=TASK_ID" \
-H 'Authorization: Bearer YOUR_TOKEN'
```
## 测试检查点
1. ✅ 创建课程后立即返回 courseId 和 taskId
2. ✅ 进度从 0% 开始,逐步增长
3. ✅ 大纲生成完成后30%自动继续生成内容40%+
4. ✅ 最终完成100%
5. ✅ 所有步骤都记录了日志(可通过 taskId 查询)
## 问题排查
如果进度卡在30%
- 检查服务器日志: `pm2 logs wildgrowth-api`
- 检查任务状态: `GET /api/ai/content/tasks/:taskId`
- 检查错误信息: 查看 `error_message` 字段

View File

@ -6,6 +6,7 @@ import { CustomError } from '../middleware/errorHandler';
import { AuthRequest } from '../middleware/auth';
import fs from 'fs';
import { documentParserService } from '../services/documentParserService';
import ossService from '../services/ossService';
import { logger } from '../utils/logger';
// 确保 images 目录存在
@ -91,6 +92,36 @@ const documentUpload = multer({
},
});
// OSS 上传:内存存储,上传后直接传到 OSS
const ossFileFilter = (req: Request, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
// 允许常见文件类型(可按需扩展)
const allowedMimes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/webp',
'image/gif',
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/epub+zip',
'text/plain',
'application/octet-stream',
];
if (allowedMimes.includes(file.mimetype) || file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new CustomError('不支持的文件类型', 400));
}
};
const ossUpload = multer({
storage: multer.memoryStorage(),
fileFilter: ossFileFilter,
limits: {
fileSize: 50 * 1024 * 1024, // 50MB
},
});
/**
*
* POST /api/upload/image
@ -184,3 +215,95 @@ export const uploadDocument = [
}
},
];
/**
* OSS
* POST /api/upload/oss
* body: FormData with 'file' field
* OSS 访 URL
*/
export const uploadToOss = [
ossUpload.single('file'),
async (req: AuthRequest, res: Response, next: NextFunction) => {
try {
const userId = req.userId;
if (!userId) {
throw new CustomError('未授权', 401);
}
if (!req.file) {
throw new CustomError('请选择要上传的文件', 400);
}
const ext = path.extname(req.file.originalname) || '';
const objectName = `uploads/${Date.now()}-${uuidv4()}${ext}`;
await ossService.putBuffer(objectName, req.file.buffer, {
contentType: req.file.mimetype,
});
const url = ossService.getObjectUrl(objectName);
res.json({
success: true,
data: {
objectName,
url,
filename: req.file.originalname,
size: req.file.size,
contentType: req.file.mimetype,
},
});
} catch (error) {
next(error);
}
},
];
/**
* OSS
* GET /api/upload/oss/download?objectName=xxx
* objectName OSS
*/
export const downloadFromOss = async (
req: AuthRequest,
res: Response,
next: NextFunction
): Promise<void> => {
try {
const userId = req.userId;
if (!userId) {
throw new CustomError('未授权', 401);
}
const objectName = req.query.objectName as string;
if (!objectName || typeof objectName !== 'string') {
throw new CustomError('缺少 objectName 参数', 400);
}
// 防止路径遍历
if (objectName.includes('..') || objectName.startsWith('/')) {
throw new CustomError('无效的 objectName', 400);
}
const { stream, contentLength, contentType } =
await ossService.getObjectStream(objectName);
const filename = objectName.split('/').pop() || 'download';
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(filename)}"`);
if (contentType) {
res.setHeader('Content-Type', contentType);
}
if (contentLength) {
res.setHeader('Content-Length', contentLength);
}
stream.pipe(res);
} catch (error) {
next(error);
}
};

View File

@ -1,5 +1,10 @@
import { Router } from 'express';
import { uploadImage, uploadDocument } from '../controllers/uploadController';
import {
uploadImage,
uploadDocument,
uploadToOss,
downloadFromOss,
} from '../controllers/uploadController';
import { authenticate } from '../middleware/auth';
const router = Router();
@ -20,6 +25,23 @@ router.post('/image', authenticate, uploadImage);
*/
router.post('/document', authenticate, uploadDocument);
/**
* @route POST /api/upload/oss
* @desc OSS
* @access Private
* @body FormData with 'file' field
* @returns { success: true, data: { objectName, url, filename, size, contentType } }
*/
router.post('/oss', authenticate, uploadToOss);
/**
* @route GET /api/upload/oss/download
* @desc OSS
* @access Private
* @query objectName - OSS uploads/xxx.png
*/
router.get('/oss/download', authenticate, downloadFromOss);
export default router;

View File

@ -119,6 +119,30 @@ export class OssService {
}
}
/**
* OSS
* @param objectName - OSS对象名称
* @returns Promise<{ stream: Readable; contentLength?: number; contentType?: string }>
*/
async getObjectStream(objectName: string): Promise<{
stream: NodeJS.ReadableStream;
contentLength?: number;
contentType?: string;
}> {
try {
const result = await this.client.getStream(objectName);
const headers = (result as { res?: { headers?: Record<string, string> } }).res?.headers;
return {
stream: result.stream,
contentLength: headers?.['content-length'] ? parseInt(headers['content-length'], 10) : undefined,
contentType: headers?.['content-type'],
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`从OSS获取文件失败: ${errorMessage}`);
}
}
/**
*
* @param objectName - OSS对象名称

View File

@ -4,7 +4,7 @@
#include "Shared.xcconfig"
// API 域名(注入 Info.plist运行时通过 Bundle.main 读取)
API_DOMAIN = https://api.muststudy.xin
API_DOMAIN = https:/$()/api.muststudy.xin
INFOPLIST_KEY_API_DOMAIN = $(API_DOMAIN)
// Swift 编译条件:代码中可用 #if API_ENV_DEVELOP

View File

@ -5,7 +5,7 @@
#include "Shared.xcconfig"
// API 域名(注入 Info.plist运行时通过 Bundle.main 读取)
API_DOMAIN = http://localhost:3000
API_DOMAIN = http:/$()/localhost:3000
INFOPLIST_KEY_API_DOMAIN = $(API_DOMAIN)
// Swift 编译条件:代码中可用 #if API_ENV_LOCAL

View File

@ -4,7 +4,7 @@
#include "Shared.xcconfig"
// API 域名(注入 Info.plist运行时通过 Bundle.main 读取)
API_DOMAIN = https://wildgrowth.upolar.com
API_DOMAIN = https:/$()/wildgrowth.upolar.com
INFOPLIST_KEY_API_DOMAIN = $(API_DOMAIN)
// Swift 编译条件:代码中可用 #if API_ENV_ONLINE

View File

@ -2,5 +2,8 @@
// 包含 Swift 版本、编译选项等通用设置
SWIFT_VERSION = 5.0
// 启用 Info.plist 中的 $(变量) 展开,否则 Info-API.plist 的 $(API_DOMAIN) 无法注入
INFOPLIST_EXPAND_BUILD_SETTINGS = YES
SWIFT_EMIT_LOC_STRINGS = YES
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES

View File

@ -376,6 +376,7 @@
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Config/Info-API.plist";
INFOPLIST_KEY_CFBundleDisplayName = "电子成长";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.books";
@ -507,6 +508,7 @@
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
API_DOMAIN = "https://wildgrowth.upolar.com";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -569,6 +571,7 @@
baseConfigurationReference = A3CF00022EE29B130004B865 /* Online.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
API_DOMAIN = "https://wildgrowth.upolar.com";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -623,6 +626,7 @@
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
API_DOMAIN = "https://api.muststudy.xin";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -685,6 +689,7 @@
baseConfigurationReference = A3CF00032EE29B130004B865 /* Develop.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
API_DOMAIN = "https://api.muststudy.xin";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -739,6 +744,7 @@
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
API_DOMAIN = "http://localhost:3000";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -801,6 +807,7 @@
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
API_DOMAIN = "http://localhost:3000";
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
@ -954,7 +961,6 @@
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
API_DOMAIN = "";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
@ -1005,7 +1011,6 @@
isa = XCBuildConfiguration;
baseConfigurationReference = A3CF00042EE29B130004B865 /* Local.xcconfig */;
buildSettings = {
API_DOMAIN = "";
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;