Upload 10 files
Browse files- backend/package.json +30 -0
- backend/src/app.js +69 -0
- backend/src/config/users.js +38 -0
- backend/src/middleware/auth.js +19 -0
- backend/src/middleware/errorHandler.js +35 -0
- backend/src/routes/auth.js +111 -0
- backend/src/routes/ppt.js +244 -0
- backend/src/routes/public.js +373 -0
- backend/src/services/githubService.js +211 -0
- backend/src/services/memoryStorageService.js +69 -0
backend/package.json
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "pptist-backend",
|
3 |
+
"version": "1.0.0",
|
4 |
+
"description": "PPTist Backend Server for Huggingface Space",
|
5 |
+
"main": "src/app.js",
|
6 |
+
"type": "module",
|
7 |
+
"scripts": {
|
8 |
+
"dev": "node src/app.js",
|
9 |
+
"start": "node src/app.js",
|
10 |
+
"build": "echo 'No build step required'"
|
11 |
+
},
|
12 |
+
"dependencies": {
|
13 |
+
"express": "^4.18.2",
|
14 |
+
"cors": "^2.8.5",
|
15 |
+
"helmet": "^7.1.0",
|
16 |
+
"bcryptjs": "^2.4.3",
|
17 |
+
"jsonwebtoken": "^9.0.2",
|
18 |
+
"multer": "^1.4.5-lts.1",
|
19 |
+
"axios": "^1.6.0",
|
20 |
+
"dotenv": "^16.3.1",
|
21 |
+
"express-rate-limit": "^7.1.5",
|
22 |
+
"uuid": "^9.0.1"
|
23 |
+
},
|
24 |
+
"devDependencies": {
|
25 |
+
"nodemon": "^3.0.2"
|
26 |
+
},
|
27 |
+
"engines": {
|
28 |
+
"node": ">=18.0.0"
|
29 |
+
}
|
30 |
+
}
|
backend/src/app.js
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import cors from 'cors';
|
3 |
+
import helmet from 'helmet';
|
4 |
+
import rateLimit from 'express-rate-limit';
|
5 |
+
import dotenv from 'dotenv';
|
6 |
+
import path from 'path';
|
7 |
+
import { fileURLToPath } from 'url';
|
8 |
+
|
9 |
+
import authRoutes from './routes/auth.js';
|
10 |
+
import pptRoutes from './routes/ppt.js';
|
11 |
+
import publicRoutes from './routes/public.js';
|
12 |
+
import { authenticateToken } from './middleware/auth.js';
|
13 |
+
import { errorHandler } from './middleware/errorHandler.js';
|
14 |
+
|
15 |
+
dotenv.config();
|
16 |
+
|
17 |
+
const __filename = fileURLToPath(import.meta.url);
|
18 |
+
const __dirname = path.dirname(__filename);
|
19 |
+
|
20 |
+
const app = express();
|
21 |
+
const PORT = process.env.PORT || 7860; // 修改为7860端口
|
22 |
+
|
23 |
+
// 安全中间件
|
24 |
+
app.use(helmet({
|
25 |
+
contentSecurityPolicy: false, // 为了兼容前端静态文件
|
26 |
+
}));
|
27 |
+
|
28 |
+
// 限流中间件
|
29 |
+
const limiter = rateLimit({
|
30 |
+
windowMs: 15 * 60 * 1000, // 15分钟
|
31 |
+
max: 100, // 每个IP每15分钟最多100个请求
|
32 |
+
message: 'Too many requests from this IP, please try again later.'
|
33 |
+
});
|
34 |
+
app.use('/api', limiter);
|
35 |
+
|
36 |
+
// CORS配置
|
37 |
+
app.use(cors({
|
38 |
+
origin: process.env.FRONTEND_URL || '*',
|
39 |
+
credentials: true
|
40 |
+
}));
|
41 |
+
|
42 |
+
app.use(express.json({ limit: '50mb' }));
|
43 |
+
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
|
44 |
+
|
45 |
+
// 提供前端静态文件
|
46 |
+
app.use(express.static(path.join(__dirname, '../../frontend/dist')));
|
47 |
+
|
48 |
+
// API路由
|
49 |
+
app.use('/api/auth', authRoutes);
|
50 |
+
app.use('/api/ppt', authenticateToken, pptRoutes);
|
51 |
+
app.use('/api/public', publicRoutes);
|
52 |
+
|
53 |
+
// 健康检查
|
54 |
+
app.get('/api/health', (req, res) => {
|
55 |
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
56 |
+
});
|
57 |
+
|
58 |
+
// 前端路由处理 - 必须在API路由之后
|
59 |
+
app.get('*', (req, res) => {
|
60 |
+
res.sendFile(path.join(__dirname, '../../frontend/dist/index.html'));
|
61 |
+
});
|
62 |
+
|
63 |
+
// 错误处理中间件
|
64 |
+
app.use(errorHandler);
|
65 |
+
|
66 |
+
app.listen(PORT, '0.0.0.0', () => {
|
67 |
+
console.log(`Server is running on port ${PORT}`);
|
68 |
+
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
69 |
+
});
|
backend/src/config/users.js
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// 内置用户配置
|
2 |
+
export const USERS = [
|
3 |
+
{
|
4 |
+
id: 'PS01',
|
5 |
+
username: 'PS01',
|
6 |
+
password: 'admin_cybercity2025',
|
7 |
+
role: 'admin'
|
8 |
+
},
|
9 |
+
{
|
10 |
+
id: 'PS02',
|
11 |
+
username: 'PS02',
|
12 |
+
password: 'cybercity2025',
|
13 |
+
role: 'user'
|
14 |
+
},
|
15 |
+
{
|
16 |
+
id: 'PS03',
|
17 |
+
username: 'PS03',
|
18 |
+
password: 'cybercity2025',
|
19 |
+
role: 'user'
|
20 |
+
},
|
21 |
+
{
|
22 |
+
id: 'PS04',
|
23 |
+
username: 'PS04',
|
24 |
+
password: 'cybercity2025',
|
25 |
+
role: 'user'
|
26 |
+
}
|
27 |
+
];
|
28 |
+
|
29 |
+
// JWT配置
|
30 |
+
export const JWT_SECRET = process.env.JWT_SECRET || 'pptist-secret-key-2025';
|
31 |
+
export const JWT_EXPIRES_IN = '24h';
|
32 |
+
|
33 |
+
// GitHub配置
|
34 |
+
export const GITHUB_CONFIG = {
|
35 |
+
token: process.env.GITHUB_TOKEN,
|
36 |
+
repositories: (process.env.GITHUB_REPOS || 'https://github.com/CaPaCaptain/PPTist_huggingface_db').split(','),
|
37 |
+
apiUrl: 'https://api.github.com'
|
38 |
+
};
|
backend/src/middleware/auth.js
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import jwt from 'jsonwebtoken';
|
2 |
+
import { JWT_SECRET } from '../config/users.js';
|
3 |
+
|
4 |
+
export const authenticateToken = (req, res, next) => {
|
5 |
+
const authHeader = req.headers['authorization'];
|
6 |
+
const token = authHeader && authHeader.split(' ')[1];
|
7 |
+
|
8 |
+
if (!token) {
|
9 |
+
return res.status(401).json({ error: 'Access token required' });
|
10 |
+
}
|
11 |
+
|
12 |
+
jwt.verify(token, JWT_SECRET, (err, user) => {
|
13 |
+
if (err) {
|
14 |
+
return res.status(403).json({ error: 'Invalid or expired token' });
|
15 |
+
}
|
16 |
+
req.user = user;
|
17 |
+
next();
|
18 |
+
});
|
19 |
+
};
|
backend/src/middleware/errorHandler.js
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export const errorHandler = (err, req, res, next) => {
|
2 |
+
console.error('Error:', err);
|
3 |
+
|
4 |
+
// JWT错误
|
5 |
+
if (err.name === 'JsonWebTokenError') {
|
6 |
+
return res.status(401).json({ error: 'Invalid token' });
|
7 |
+
}
|
8 |
+
|
9 |
+
if (err.name === 'TokenExpiredError') {
|
10 |
+
return res.status(401).json({ error: 'Token expired' });
|
11 |
+
}
|
12 |
+
|
13 |
+
// GitHub API错误
|
14 |
+
if (err.response && err.response.status) {
|
15 |
+
const status = err.response.status;
|
16 |
+
const message = err.response.data?.message || 'GitHub API error';
|
17 |
+
|
18 |
+
if (status === 401) {
|
19 |
+
return res.status(500).json({ error: 'GitHub authentication failed' });
|
20 |
+
}
|
21 |
+
|
22 |
+
if (status === 404) {
|
23 |
+
return res.status(404).json({ error: 'Resource not found in GitHub' });
|
24 |
+
}
|
25 |
+
|
26 |
+
return res.status(500).json({ error: `GitHub API error: ${message}` });
|
27 |
+
}
|
28 |
+
|
29 |
+
// 默认错误
|
30 |
+
res.status(500).json({
|
31 |
+
error: process.env.NODE_ENV === 'production'
|
32 |
+
? 'Internal server error'
|
33 |
+
: err.message
|
34 |
+
});
|
35 |
+
};
|
backend/src/routes/auth.js
ADDED
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import jwt from 'jsonwebtoken';
|
3 |
+
import bcrypt from 'bcryptjs';
|
4 |
+
import { USERS, JWT_SECRET, JWT_EXPIRES_IN } from '../config/users.js';
|
5 |
+
|
6 |
+
const router = express.Router();
|
7 |
+
|
8 |
+
// 登录
|
9 |
+
router.post('/login', async (req, res, next) => {
|
10 |
+
try {
|
11 |
+
const { username, password } = req.body;
|
12 |
+
|
13 |
+
if (!username || !password) {
|
14 |
+
return res.status(400).json({ error: 'Username and password are required' });
|
15 |
+
}
|
16 |
+
|
17 |
+
// 查找用户
|
18 |
+
const user = USERS.find(u => u.username === username);
|
19 |
+
if (!user) {
|
20 |
+
return res.status(401).json({ error: 'Invalid credentials' });
|
21 |
+
}
|
22 |
+
|
23 |
+
// 验证密码
|
24 |
+
if (user.password !== password) {
|
25 |
+
return res.status(401).json({ error: 'Invalid credentials' });
|
26 |
+
}
|
27 |
+
|
28 |
+
// 生成JWT token
|
29 |
+
const token = jwt.sign(
|
30 |
+
{
|
31 |
+
userId: user.id,
|
32 |
+
username: user.username,
|
33 |
+
role: user.role
|
34 |
+
},
|
35 |
+
JWT_SECRET,
|
36 |
+
{ expiresIn: JWT_EXPIRES_IN }
|
37 |
+
);
|
38 |
+
|
39 |
+
res.json({
|
40 |
+
token,
|
41 |
+
user: {
|
42 |
+
id: user.id,
|
43 |
+
username: user.username,
|
44 |
+
role: user.role
|
45 |
+
}
|
46 |
+
});
|
47 |
+
} catch (error) {
|
48 |
+
next(error);
|
49 |
+
}
|
50 |
+
});
|
51 |
+
|
52 |
+
// 验证token
|
53 |
+
router.get('/verify', (req, res, next) => {
|
54 |
+
try {
|
55 |
+
const authHeader = req.headers['authorization'];
|
56 |
+
const token = authHeader && authHeader.split(' ')[1];
|
57 |
+
|
58 |
+
if (!token) {
|
59 |
+
return res.status(401).json({ error: 'No token provided' });
|
60 |
+
}
|
61 |
+
|
62 |
+
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
63 |
+
if (err) {
|
64 |
+
return res.status(401).json({ error: 'Invalid token' });
|
65 |
+
}
|
66 |
+
|
67 |
+
res.json({
|
68 |
+
user: {
|
69 |
+
id: decoded.userId,
|
70 |
+
username: decoded.username,
|
71 |
+
role: decoded.role
|
72 |
+
}
|
73 |
+
});
|
74 |
+
});
|
75 |
+
} catch (error) {
|
76 |
+
next(error);
|
77 |
+
}
|
78 |
+
});
|
79 |
+
|
80 |
+
// 获取用户信息
|
81 |
+
router.get('/user', (req, res, next) => {
|
82 |
+
try {
|
83 |
+
const authHeader = req.headers['authorization'];
|
84 |
+
const token = authHeader && authHeader.split(' ')[1];
|
85 |
+
|
86 |
+
if (!token) {
|
87 |
+
return res.status(401).json({ error: 'No token provided' });
|
88 |
+
}
|
89 |
+
|
90 |
+
jwt.verify(token, JWT_SECRET, (err, decoded) => {
|
91 |
+
if (err) {
|
92 |
+
return res.status(401).json({ error: 'Invalid token' });
|
93 |
+
}
|
94 |
+
|
95 |
+
const user = USERS.find(u => u.id === decoded.userId);
|
96 |
+
if (!user) {
|
97 |
+
return res.status(404).json({ error: 'User not found' });
|
98 |
+
}
|
99 |
+
|
100 |
+
res.json({
|
101 |
+
id: user.id,
|
102 |
+
username: user.username,
|
103 |
+
role: user.role
|
104 |
+
});
|
105 |
+
});
|
106 |
+
} catch (error) {
|
107 |
+
next(error);
|
108 |
+
}
|
109 |
+
});
|
110 |
+
|
111 |
+
export default router;
|
backend/src/routes/ppt.js
ADDED
@@ -0,0 +1,244 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import { v4 as uuidv4 } from 'uuid';
|
3 |
+
import githubService from '../services/githubService.js';
|
4 |
+
import memoryStorageService from '../services/memoryStorageService.js';
|
5 |
+
|
6 |
+
const router = express.Router();
|
7 |
+
|
8 |
+
// 选择存储服务
|
9 |
+
const getStorageService = () => {
|
10 |
+
// 如果GitHub Token未配置,使用内存存储
|
11 |
+
if (!process.env.GITHUB_TOKEN) {
|
12 |
+
return memoryStorageService;
|
13 |
+
}
|
14 |
+
return githubService;
|
15 |
+
};
|
16 |
+
|
17 |
+
// 获取用户的PPT列表
|
18 |
+
router.get('/list', async (req, res, next) => {
|
19 |
+
try {
|
20 |
+
const userId = req.user.userId;
|
21 |
+
const storageService = getStorageService();
|
22 |
+
const pptList = await storageService.getUserPPTList(userId);
|
23 |
+
res.json(pptList);
|
24 |
+
} catch (error) {
|
25 |
+
next(error);
|
26 |
+
}
|
27 |
+
});
|
28 |
+
|
29 |
+
// 获取指定PPT数据
|
30 |
+
router.get('/:pptId', async (req, res, next) => {
|
31 |
+
try {
|
32 |
+
const userId = req.user.userId;
|
33 |
+
const { pptId } = req.params;
|
34 |
+
const fileName = `${pptId}.json`;
|
35 |
+
const storageService = getStorageService();
|
36 |
+
|
37 |
+
let pptData = null;
|
38 |
+
|
39 |
+
// 如果是GitHub服务,尝试所有仓库
|
40 |
+
if (storageService === githubService && storageService.repositories) {
|
41 |
+
for (let i = 0; i < storageService.repositories.length; i++) {
|
42 |
+
try {
|
43 |
+
const result = await storageService.getFile(userId, fileName, i);
|
44 |
+
if (result) {
|
45 |
+
pptData = result.content;
|
46 |
+
break;
|
47 |
+
}
|
48 |
+
} catch (error) {
|
49 |
+
continue;
|
50 |
+
}
|
51 |
+
}
|
52 |
+
} else {
|
53 |
+
// 内存存储服务
|
54 |
+
const result = await storageService.getFile(userId, fileName);
|
55 |
+
if (result) {
|
56 |
+
pptData = result.content;
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
+
if (!pptData) {
|
61 |
+
return res.status(404).json({ error: 'PPT not found' });
|
62 |
+
}
|
63 |
+
|
64 |
+
res.json(pptData);
|
65 |
+
} catch (error) {
|
66 |
+
next(error);
|
67 |
+
}
|
68 |
+
});
|
69 |
+
|
70 |
+
// 保存PPT数据
|
71 |
+
router.post('/save', async (req, res, next) => {
|
72 |
+
try {
|
73 |
+
const userId = req.user.userId;
|
74 |
+
const { pptId, title, slides, theme } = req.body;
|
75 |
+
|
76 |
+
if (!pptId || !slides) {
|
77 |
+
return res.status(400).json({ error: 'PPT ID and slides are required' });
|
78 |
+
}
|
79 |
+
|
80 |
+
const fileName = `${pptId}.json`;
|
81 |
+
const pptData = {
|
82 |
+
id: pptId,
|
83 |
+
title: title || '未命名演示文稿',
|
84 |
+
slides: slides,
|
85 |
+
theme: theme || {},
|
86 |
+
createdAt: new Date().toISOString(),
|
87 |
+
updatedAt: new Date().toISOString()
|
88 |
+
};
|
89 |
+
|
90 |
+
const storageService = getStorageService();
|
91 |
+
|
92 |
+
// 如果是GitHub服务,保存到第一个仓库
|
93 |
+
if (storageService === githubService) {
|
94 |
+
await storageService.saveFile(userId, fileName, pptData, 0);
|
95 |
+
} else {
|
96 |
+
// 内存存储服务
|
97 |
+
await storageService.saveFile(userId, fileName, pptData);
|
98 |
+
}
|
99 |
+
|
100 |
+
res.json({ message: 'PPT saved successfully', pptId });
|
101 |
+
} catch (error) {
|
102 |
+
next(error);
|
103 |
+
}
|
104 |
+
});
|
105 |
+
|
106 |
+
// 创建新PPT
|
107 |
+
router.post('/create', async (req, res, next) => {
|
108 |
+
try {
|
109 |
+
const userId = req.user.userId;
|
110 |
+
const { title } = req.body;
|
111 |
+
|
112 |
+
const pptId = uuidv4();
|
113 |
+
const fileName = `${pptId}.json`;
|
114 |
+
|
115 |
+
// 创建默认PPT数据
|
116 |
+
const defaultSlide = {
|
117 |
+
id: uuidv4(),
|
118 |
+
elements: [],
|
119 |
+
background: {
|
120 |
+
type: 'solid',
|
121 |
+
color: '#ffffff'
|
122 |
+
}
|
123 |
+
};
|
124 |
+
|
125 |
+
const pptData = {
|
126 |
+
id: pptId,
|
127 |
+
title: title || '新建演示文稿',
|
128 |
+
slides: [defaultSlide],
|
129 |
+
theme: {},
|
130 |
+
createdAt: new Date().toISOString(),
|
131 |
+
updatedAt: new Date().toISOString()
|
132 |
+
};
|
133 |
+
|
134 |
+
const storageService = getStorageService();
|
135 |
+
|
136 |
+
// 保存新创建的PPT
|
137 |
+
if (storageService === githubService) {
|
138 |
+
await storageService.saveFile(userId, fileName, pptData, 0);
|
139 |
+
} else {
|
140 |
+
await storageService.saveFile(userId, fileName, pptData);
|
141 |
+
}
|
142 |
+
|
143 |
+
res.json({
|
144 |
+
message: 'PPT created successfully',
|
145 |
+
pptId,
|
146 |
+
ppt: pptData
|
147 |
+
});
|
148 |
+
} catch (error) {
|
149 |
+
next(error);
|
150 |
+
}
|
151 |
+
});
|
152 |
+
|
153 |
+
// 删除PPT
|
154 |
+
router.delete('/:pptId', async (req, res, next) => {
|
155 |
+
try {
|
156 |
+
const userId = req.user.userId;
|
157 |
+
const { pptId } = req.params;
|
158 |
+
const fileName = `${pptId}.json`;
|
159 |
+
const storageService = getStorageService();
|
160 |
+
|
161 |
+
// 如果是GitHub服务,从所有仓库中删除
|
162 |
+
if (storageService === githubService && storageService.repositories) {
|
163 |
+
for (let i = 0; i < storageService.repositories.length; i++) {
|
164 |
+
try {
|
165 |
+
await storageService.deleteFile(userId, fileName, i);
|
166 |
+
} catch (error) {
|
167 |
+
// 继续尝试其他仓库
|
168 |
+
continue;
|
169 |
+
}
|
170 |
+
}
|
171 |
+
} else {
|
172 |
+
// 内存存储服务
|
173 |
+
await storageService.deleteFile(userId, fileName);
|
174 |
+
}
|
175 |
+
|
176 |
+
res.json({ message: 'PPT deleted successfully' });
|
177 |
+
} catch (error) {
|
178 |
+
next(error);
|
179 |
+
}
|
180 |
+
});
|
181 |
+
|
182 |
+
// 复制PPT
|
183 |
+
router.post('/:pptId/copy', async (req, res, next) => {
|
184 |
+
try {
|
185 |
+
const userId = req.user.userId;
|
186 |
+
const { pptId } = req.params;
|
187 |
+
const { title } = req.body;
|
188 |
+
const sourceFileName = `${pptId}.json`;
|
189 |
+
const storageService = getStorageService();
|
190 |
+
|
191 |
+
// 获取源PPT数据
|
192 |
+
let sourcePPT = null;
|
193 |
+
if (storageService === githubService && storageService.repositories) {
|
194 |
+
for (let i = 0; i < storageService.repositories.length; i++) {
|
195 |
+
try {
|
196 |
+
const result = await storageService.getFile(userId, sourceFileName, i);
|
197 |
+
if (result) {
|
198 |
+
sourcePPT = result.content;
|
199 |
+
break;
|
200 |
+
}
|
201 |
+
} catch (error) {
|
202 |
+
continue;
|
203 |
+
}
|
204 |
+
}
|
205 |
+
} else {
|
206 |
+
const result = await storageService.getFile(userId, sourceFileName);
|
207 |
+
if (result) {
|
208 |
+
sourcePPT = result.content;
|
209 |
+
}
|
210 |
+
}
|
211 |
+
|
212 |
+
if (!sourcePPT) {
|
213 |
+
return res.status(404).json({ error: 'Source PPT not found' });
|
214 |
+
}
|
215 |
+
|
216 |
+
// 创建新的PPT ID和数据
|
217 |
+
const newPptId = uuidv4();
|
218 |
+
const newFileName = `${newPptId}.json`;
|
219 |
+
const newPPTData = {
|
220 |
+
...sourcePPT,
|
221 |
+
id: newPptId,
|
222 |
+
title: title || `${sourcePPT.title} - 副本`,
|
223 |
+
createdAt: new Date().toISOString(),
|
224 |
+
updatedAt: new Date().toISOString()
|
225 |
+
};
|
226 |
+
|
227 |
+
// 保存复制的PPT
|
228 |
+
if (storageService === githubService) {
|
229 |
+
await storageService.saveFile(userId, newFileName, newPPTData, 0);
|
230 |
+
} else {
|
231 |
+
await storageService.saveFile(userId, newFileName, newPPTData);
|
232 |
+
}
|
233 |
+
|
234 |
+
res.json({
|
235 |
+
message: 'PPT copied successfully',
|
236 |
+
pptId: newPptId,
|
237 |
+
ppt: newPPTData
|
238 |
+
});
|
239 |
+
} catch (error) {
|
240 |
+
next(error);
|
241 |
+
}
|
242 |
+
});
|
243 |
+
|
244 |
+
export default router;
|
backend/src/routes/public.js
ADDED
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import express from 'express';
|
2 |
+
import githubService from '../services/githubService.js';
|
3 |
+
import memoryStorageService from '../services/memoryStorageService.js';
|
4 |
+
|
5 |
+
const router = express.Router();
|
6 |
+
|
7 |
+
// 选择存储服务
|
8 |
+
const getStorageService = () => {
|
9 |
+
// 如果GitHub Token未配置,使用内存存储
|
10 |
+
if (!process.env.GITHUB_TOKEN) {
|
11 |
+
return memoryStorageService;
|
12 |
+
}
|
13 |
+
return githubService;
|
14 |
+
};
|
15 |
+
|
16 |
+
// 生成HTML页面来显示PPT幻灯片
|
17 |
+
const generateSlideHTML = (pptData, slideIndex, title) => {
|
18 |
+
const slide = pptData.slides[slideIndex];
|
19 |
+
const theme = pptData.theme || {};
|
20 |
+
|
21 |
+
// 渲染幻灯片元素
|
22 |
+
const renderElements = (elements) => {
|
23 |
+
if (!elements || elements.length === 0) return '';
|
24 |
+
|
25 |
+
return elements.map(element => {
|
26 |
+
const style = `
|
27 |
+
position: absolute;
|
28 |
+
left: ${element.left}px;
|
29 |
+
top: ${element.top}px;
|
30 |
+
width: ${element.width}px;
|
31 |
+
height: ${element.height}px;
|
32 |
+
transform: rotate(${element.rotate || 0}deg);
|
33 |
+
z-index: ${element.zIndex || 1};
|
34 |
+
`;
|
35 |
+
|
36 |
+
switch (element.type) {
|
37 |
+
case 'text':
|
38 |
+
return `
|
39 |
+
<div style="${style}
|
40 |
+
font-size: ${element.fontSize || 14}px;
|
41 |
+
font-family: ${element.fontName || 'Arial'};
|
42 |
+
color: ${element.defaultColor || '#000'};
|
43 |
+
font-weight: ${element.bold ? 'bold' : 'normal'};
|
44 |
+
font-style: ${element.italic ? 'italic' : 'normal'};
|
45 |
+
text-decoration: ${element.underline ? 'underline' : 'none'};
|
46 |
+
text-align: ${element.align || 'left'};
|
47 |
+
line-height: ${element.lineHeight || 1.2};
|
48 |
+
padding: 10px;
|
49 |
+
word-wrap: break-word;
|
50 |
+
overflow: hidden;
|
51 |
+
">
|
52 |
+
${element.content || ''}
|
53 |
+
</div>
|
54 |
+
`;
|
55 |
+
case 'image':
|
56 |
+
return `
|
57 |
+
<div style="${style}">
|
58 |
+
<img src="${element.src}" alt="" style="width: 100%; height: 100%; object-fit: ${element.objectFit || 'contain'};" />
|
59 |
+
</div>
|
60 |
+
`;
|
61 |
+
case 'shape':
|
62 |
+
const shapeStyle = element.fill ? `background-color: ${element.fill};` : '';
|
63 |
+
const borderStyle = element.outline ? `border: ${element.outline.width || 1}px ${element.outline.style || 'solid'} ${element.outline.color || '#000'};` : '';
|
64 |
+
return `
|
65 |
+
<div style="${style} ${shapeStyle} ${borderStyle}"></div>
|
66 |
+
`;
|
67 |
+
default:
|
68 |
+
return `<div style="${style}"></div>`;
|
69 |
+
}
|
70 |
+
}).join('');
|
71 |
+
};
|
72 |
+
|
73 |
+
return `
|
74 |
+
<!DOCTYPE html>
|
75 |
+
<html lang="zh-CN">
|
76 |
+
<head>
|
77 |
+
<meta charset="UTF-8">
|
78 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
79 |
+
<title>${title} - 第${slideIndex + 1}页</title>
|
80 |
+
<style>
|
81 |
+
* {
|
82 |
+
margin: 0;
|
83 |
+
padding: 0;
|
84 |
+
box-sizing: border-box;
|
85 |
+
}
|
86 |
+
body {
|
87 |
+
font-family: 'Microsoft YaHei', Arial, sans-serif;
|
88 |
+
background-color: #f5f5f5;
|
89 |
+
display: flex;
|
90 |
+
justify-content: center;
|
91 |
+
align-items: center;
|
92 |
+
min-height: 100vh;
|
93 |
+
padding: 0;
|
94 |
+
}
|
95 |
+
.slide-container {
|
96 |
+
width: 900px;
|
97 |
+
height: 506px;
|
98 |
+
background-color: ${slide.background?.color || theme.backgroundColor || '#ffffff'};
|
99 |
+
position: relative;
|
100 |
+
border-radius: 8px;
|
101 |
+
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
102 |
+
overflow: hidden;
|
103 |
+
}
|
104 |
+
${slide.background?.type === 'image' ? `
|
105 |
+
.slide-container::before {
|
106 |
+
content: '';
|
107 |
+
position: absolute;
|
108 |
+
top: 0;
|
109 |
+
left: 0;
|
110 |
+
right: 0;
|
111 |
+
bottom: 0;
|
112 |
+
background-image: url('${slide.background.image}');
|
113 |
+
background-size: ${slide.background.imageSize || 'cover'};
|
114 |
+
background-position: center;
|
115 |
+
background-repeat: no-repeat;
|
116 |
+
z-index: 0;
|
117 |
+
}
|
118 |
+
` : ''}
|
119 |
+
</style>
|
120 |
+
</head>
|
121 |
+
<body>
|
122 |
+
<div class="slide-container">
|
123 |
+
${renderElements(slide.elements)}
|
124 |
+
</div>
|
125 |
+
</body>
|
126 |
+
</html>
|
127 |
+
`;
|
128 |
+
};
|
129 |
+
|
130 |
+
// 公开访问PPT页面 - 返回HTML页面
|
131 |
+
router.get('/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
|
132 |
+
try {
|
133 |
+
const { userId, pptId, slideIndex = 0 } = req.params;
|
134 |
+
const querySlideIndex = req.query.slide ? parseInt(req.query.slide) : parseInt(slideIndex);
|
135 |
+
const fileName = `${pptId}.json`;
|
136 |
+
const storageService = getStorageService();
|
137 |
+
|
138 |
+
let pptData = null;
|
139 |
+
|
140 |
+
// 如果是GitHub服务,尝试所有仓库
|
141 |
+
if (storageService === githubService && storageService.repositories) {
|
142 |
+
for (let i = 0; i < storageService.repositories.length; i++) {
|
143 |
+
try {
|
144 |
+
const result = await storageService.getFile(userId, fileName, i);
|
145 |
+
if (result) {
|
146 |
+
pptData = result.content;
|
147 |
+
break;
|
148 |
+
}
|
149 |
+
} catch (error) {
|
150 |
+
continue;
|
151 |
+
}
|
152 |
+
}
|
153 |
+
} else {
|
154 |
+
// 内存存储服务
|
155 |
+
const result = await storageService.getFile(userId, fileName);
|
156 |
+
if (result) {
|
157 |
+
pptData = result.content;
|
158 |
+
}
|
159 |
+
}
|
160 |
+
|
161 |
+
if (!pptData) {
|
162 |
+
return res.status(404).send(`
|
163 |
+
<!DOCTYPE html>
|
164 |
+
<html lang="zh-CN">
|
165 |
+
<head>
|
166 |
+
<meta charset="UTF-8">
|
167 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
168 |
+
<title>PPT未找到</title>
|
169 |
+
<style>
|
170 |
+
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
171 |
+
.error { color: #e74c3c; font-size: 18px; }
|
172 |
+
</style>
|
173 |
+
</head>
|
174 |
+
<body>
|
175 |
+
<div class="error">
|
176 |
+
<h2>PPT未找到</h2>
|
177 |
+
<p>请检查链接是否正确</p>
|
178 |
+
</div>
|
179 |
+
</body>
|
180 |
+
</html>
|
181 |
+
`);
|
182 |
+
}
|
183 |
+
|
184 |
+
const slideIdx = querySlideIndex;
|
185 |
+
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
|
186 |
+
return res.status(404).send(`
|
187 |
+
<!DOCTYPE html>
|
188 |
+
<html lang="zh-CN">
|
189 |
+
<head>
|
190 |
+
<meta charset="UTF-8">
|
191 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
192 |
+
<title>幻灯片未找到</title>
|
193 |
+
<style>
|
194 |
+
body { font-family: Arial, sans-serif; text-align: center; padding: 50px; }
|
195 |
+
.error { color: #e74c3c; font-size: 18px; }
|
196 |
+
</style>
|
197 |
+
</head>
|
198 |
+
<body>
|
199 |
+
<div class="error">
|
200 |
+
<h2>幻灯片未找到</h2>
|
201 |
+
<p>请检查幻灯片索引是否正确</p>
|
202 |
+
</div>
|
203 |
+
</body>
|
204 |
+
</html>
|
205 |
+
`);
|
206 |
+
}
|
207 |
+
|
208 |
+
// 返回HTML页面
|
209 |
+
const htmlContent = generateSlideHTML(pptData, slideIdx, pptData.title);
|
210 |
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
211 |
+
res.send(htmlContent);
|
212 |
+
} catch (error) {
|
213 |
+
next(error);
|
214 |
+
}
|
215 |
+
});
|
216 |
+
|
217 |
+
// API接口:获取PPT数据和指定幻灯片(JSON格式)
|
218 |
+
router.get('/api/view/:userId/:pptId/:slideIndex?', async (req, res, next) => {
|
219 |
+
try {
|
220 |
+
const { userId, pptId, slideIndex = 0 } = req.params;
|
221 |
+
const fileName = `${pptId}.json`;
|
222 |
+
const storageService = getStorageService();
|
223 |
+
|
224 |
+
let pptData = null;
|
225 |
+
|
226 |
+
// 如果是GitHub服务,尝试所有仓库
|
227 |
+
if (storageService === githubService && storageService.repositories) {
|
228 |
+
for (let i = 0; i < storageService.repositories.length; i++) {
|
229 |
+
try {
|
230 |
+
const result = await storageService.getFile(userId, fileName, i);
|
231 |
+
if (result) {
|
232 |
+
pptData = result.content;
|
233 |
+
break;
|
234 |
+
}
|
235 |
+
} catch (error) {
|
236 |
+
continue;
|
237 |
+
}
|
238 |
+
}
|
239 |
+
} else {
|
240 |
+
// 内存存储服务
|
241 |
+
const result = await storageService.getFile(userId, fileName);
|
242 |
+
if (result) {
|
243 |
+
pptData = result.content;
|
244 |
+
}
|
245 |
+
}
|
246 |
+
|
247 |
+
if (!pptData) {
|
248 |
+
return res.status(404).json({ error: 'PPT not found' });
|
249 |
+
}
|
250 |
+
|
251 |
+
const slideIdx = parseInt(slideIndex);
|
252 |
+
if (slideIdx >= pptData.slides.length || slideIdx < 0) {
|
253 |
+
return res.status(404).json({ error: 'Slide not found' });
|
254 |
+
}
|
255 |
+
|
256 |
+
// 返回PPT数据和指定幻灯片
|
257 |
+
res.json({
|
258 |
+
id: pptData.id,
|
259 |
+
title: pptData.title,
|
260 |
+
theme: pptData.theme,
|
261 |
+
currentSlide: pptData.slides[slideIdx],
|
262 |
+
slideIndex: slideIdx,
|
263 |
+
totalSlides: pptData.slides.length,
|
264 |
+
isPublicView: true
|
265 |
+
});
|
266 |
+
} catch (error) {
|
267 |
+
next(error);
|
268 |
+
}
|
269 |
+
});
|
270 |
+
|
271 |
+
// 获取完整PPT数据(只读模式)
|
272 |
+
router.get('/ppt/:userId/:pptId', async (req, res, next) => {
|
273 |
+
try {
|
274 |
+
const { userId, pptId } = req.params;
|
275 |
+
const fileName = `${pptId}.json`;
|
276 |
+
const storageService = getStorageService();
|
277 |
+
|
278 |
+
let pptData = null;
|
279 |
+
|
280 |
+
// 如果是GitHub服务,尝试所有仓库
|
281 |
+
if (storageService === githubService && storageService.repositories) {
|
282 |
+
for (let i = 0; i < storageService.repositories.length; i++) {
|
283 |
+
try {
|
284 |
+
const result = await storageService.getFile(userId, fileName, i);
|
285 |
+
if (result) {
|
286 |
+
pptData = result.content;
|
287 |
+
break;
|
288 |
+
}
|
289 |
+
} catch (error) {
|
290 |
+
continue;
|
291 |
+
}
|
292 |
+
}
|
293 |
+
} else {
|
294 |
+
// 内存存储服务
|
295 |
+
const result = await storageService.getFile(userId, fileName);
|
296 |
+
if (result) {
|
297 |
+
pptData = result.content;
|
298 |
+
}
|
299 |
+
}
|
300 |
+
|
301 |
+
if (!pptData) {
|
302 |
+
return res.status(404).json({ error: 'PPT not found' });
|
303 |
+
}
|
304 |
+
|
305 |
+
// 返回���读版本的PPT数据
|
306 |
+
res.json({
|
307 |
+
...pptData,
|
308 |
+
isPublicView: true,
|
309 |
+
readOnly: true
|
310 |
+
});
|
311 |
+
} catch (error) {
|
312 |
+
next(error);
|
313 |
+
}
|
314 |
+
});
|
315 |
+
|
316 |
+
// 生成PPT分享链接
|
317 |
+
router.post('/generate-share-link', async (req, res, next) => {
|
318 |
+
try {
|
319 |
+
const { userId, pptId, slideIndex = 0 } = req.body;
|
320 |
+
|
321 |
+
if (!userId || !pptId) {
|
322 |
+
return res.status(400).json({ error: 'User ID and PPT ID are required' });
|
323 |
+
}
|
324 |
+
|
325 |
+
// 验证PPT是否存在
|
326 |
+
const fileName = `${pptId}.json`;
|
327 |
+
const storageService = getStorageService();
|
328 |
+
let pptExists = false;
|
329 |
+
|
330 |
+
// 如果是GitHub服务,尝试所有仓库
|
331 |
+
if (storageService === githubService && storageService.repositories) {
|
332 |
+
for (let i = 0; i < storageService.repositories.length; i++) {
|
333 |
+
try {
|
334 |
+
const result = await storageService.getFile(userId, fileName, i);
|
335 |
+
if (result) {
|
336 |
+
pptExists = true;
|
337 |
+
break;
|
338 |
+
}
|
339 |
+
} catch (error) {
|
340 |
+
continue;
|
341 |
+
}
|
342 |
+
}
|
343 |
+
} else {
|
344 |
+
// 内存存储服务
|
345 |
+
const result = await storageService.getFile(userId, fileName);
|
346 |
+
if (result) {
|
347 |
+
pptExists = true;
|
348 |
+
}
|
349 |
+
}
|
350 |
+
|
351 |
+
if (!pptExists) {
|
352 |
+
return res.status(404).json({ error: 'PPT not found' });
|
353 |
+
}
|
354 |
+
|
355 |
+
const baseUrl = process.env.PUBLIC_URL || req.get('host');
|
356 |
+
const protocol = process.env.NODE_ENV === 'production' ? 'https' : req.protocol;
|
357 |
+
|
358 |
+
const shareLinks = {
|
359 |
+
// 单页分享链接
|
360 |
+
slideUrl: `${protocol}://${baseUrl}/api/public/view/${userId}/${pptId}/${slideIndex}`,
|
361 |
+
// 完整PPT分享链接
|
362 |
+
pptUrl: `${protocol}://${baseUrl}/api/public/ppt/${userId}/${pptId}`,
|
363 |
+
// 前端查看链接
|
364 |
+
viewUrl: `${protocol}://${baseUrl}/public/${userId}/${pptId}/${slideIndex}`
|
365 |
+
};
|
366 |
+
|
367 |
+
res.json(shareLinks);
|
368 |
+
} catch (error) {
|
369 |
+
next(error);
|
370 |
+
}
|
371 |
+
});
|
372 |
+
|
373 |
+
export default router;
|
backend/src/services/githubService.js
ADDED
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import axios from 'axios';
|
2 |
+
import { GITHUB_CONFIG } from '../config/users.js';
|
3 |
+
import memoryStorageService from './memoryStorageService.js';
|
4 |
+
|
5 |
+
class GitHubService {
|
6 |
+
constructor() {
|
7 |
+
this.apiUrl = GITHUB_CONFIG.apiUrl;
|
8 |
+
this.token = GITHUB_CONFIG.token;
|
9 |
+
this.repositories = GITHUB_CONFIG.repositories;
|
10 |
+
this.useMemoryStorage = !this.token; // 如果没有token,使用内存存储
|
11 |
+
|
12 |
+
if (!this.token) {
|
13 |
+
console.warn('GitHub token not configured, using memory storage for development');
|
14 |
+
}
|
15 |
+
}
|
16 |
+
|
17 |
+
// 获取仓库信息
|
18 |
+
parseRepoUrl(repoUrl) {
|
19 |
+
const match = repoUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/);
|
20 |
+
if (!match) throw new Error('Invalid GitHub repository URL');
|
21 |
+
return { owner: match[1], repo: match[2] };
|
22 |
+
}
|
23 |
+
|
24 |
+
// 获取文件内容
|
25 |
+
async getFile(userId, fileName, repoIndex = 0) {
|
26 |
+
// 如果使用内存存储
|
27 |
+
if (this.useMemoryStorage) {
|
28 |
+
return await memoryStorageService.getFile(userId, fileName);
|
29 |
+
}
|
30 |
+
|
31 |
+
// 原有的GitHub逻辑
|
32 |
+
try {
|
33 |
+
const repoUrl = this.repositories[repoIndex];
|
34 |
+
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
35 |
+
const path = `users/${userId}/${fileName}`;
|
36 |
+
|
37 |
+
const response = await axios.get(
|
38 |
+
`${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
|
39 |
+
{
|
40 |
+
headers: {
|
41 |
+
'Authorization': `token ${this.token}`,
|
42 |
+
'Accept': 'application/vnd.github.v3+json'
|
43 |
+
}
|
44 |
+
}
|
45 |
+
);
|
46 |
+
|
47 |
+
const content = Buffer.from(response.data.content, 'base64').toString('utf8');
|
48 |
+
return {
|
49 |
+
content: JSON.parse(content),
|
50 |
+
sha: response.data.sha
|
51 |
+
};
|
52 |
+
} catch (error) {
|
53 |
+
if (error.response?.status === 404) {
|
54 |
+
return null;
|
55 |
+
}
|
56 |
+
throw error;
|
57 |
+
}
|
58 |
+
}
|
59 |
+
|
60 |
+
// 保存文件
|
61 |
+
async saveFile(userId, fileName, data, repoIndex = 0) {
|
62 |
+
// 如果使用内存存储
|
63 |
+
if (this.useMemoryStorage) {
|
64 |
+
return await memoryStorageService.saveFile(userId, fileName, data);
|
65 |
+
}
|
66 |
+
|
67 |
+
// 原有的GitHub逻辑
|
68 |
+
const repoUrl = this.repositories[repoIndex];
|
69 |
+
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
70 |
+
const path = `users/${userId}/${fileName}`;
|
71 |
+
|
72 |
+
// 先尝试获取现有文件的SHA
|
73 |
+
let sha = null;
|
74 |
+
try {
|
75 |
+
const existing = await this.getFile(userId, fileName, repoIndex);
|
76 |
+
if (existing) {
|
77 |
+
sha = existing.sha;
|
78 |
+
}
|
79 |
+
} catch (error) {
|
80 |
+
// 忽略获取SHA的错误
|
81 |
+
}
|
82 |
+
|
83 |
+
const content = Buffer.from(JSON.stringify(data, null, 2)).toString('base64');
|
84 |
+
|
85 |
+
const payload = {
|
86 |
+
message: `Update ${fileName} for user ${userId}`,
|
87 |
+
content: content,
|
88 |
+
branch: 'main'
|
89 |
+
};
|
90 |
+
|
91 |
+
if (sha) {
|
92 |
+
payload.sha = sha;
|
93 |
+
}
|
94 |
+
|
95 |
+
const response = await axios.put(
|
96 |
+
`${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
|
97 |
+
payload,
|
98 |
+
{
|
99 |
+
headers: {
|
100 |
+
'Authorization': `token ${this.token}`,
|
101 |
+
'Accept': 'application/vnd.github.v3+json'
|
102 |
+
}
|
103 |
+
}
|
104 |
+
);
|
105 |
+
|
106 |
+
return response.data;
|
107 |
+
}
|
108 |
+
|
109 |
+
// 获取用户的所有PPT列表
|
110 |
+
async getUserPPTList(userId) {
|
111 |
+
// 如果使用内存存储
|
112 |
+
if (this.useMemoryStorage) {
|
113 |
+
return await memoryStorageService.getUserPPTList(userId);
|
114 |
+
}
|
115 |
+
|
116 |
+
// 原有的GitHub逻辑
|
117 |
+
const results = [];
|
118 |
+
|
119 |
+
for (let i = 0; i < this.repositories.length; i++) {
|
120 |
+
try {
|
121 |
+
const repoUrl = this.repositories[i];
|
122 |
+
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
123 |
+
const path = `users/${userId}`;
|
124 |
+
|
125 |
+
const response = await axios.get(
|
126 |
+
`${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
|
127 |
+
{
|
128 |
+
headers: {
|
129 |
+
'Authorization': `token ${this.token}`,
|
130 |
+
'Accept': 'application/vnd.github.v3+json'
|
131 |
+
}
|
132 |
+
}
|
133 |
+
);
|
134 |
+
|
135 |
+
const files = response.data
|
136 |
+
.filter(item => item.type === 'file' && item.name.endsWith('.json'));
|
137 |
+
|
138 |
+
// 获取每个PPT文件的实际内容以读取标题
|
139 |
+
for (const file of files) {
|
140 |
+
try {
|
141 |
+
const pptId = file.name.replace('.json', '');
|
142 |
+
const fileContent = await this.getFile(userId, file.name, i);
|
143 |
+
|
144 |
+
if (fileContent && fileContent.content) {
|
145 |
+
results.push({
|
146 |
+
name: pptId,
|
147 |
+
title: fileContent.content.title || '未命名演示文稿',
|
148 |
+
lastModified: fileContent.content.updatedAt || fileContent.content.createdAt,
|
149 |
+
repoIndex: i,
|
150 |
+
repoUrl: repoUrl
|
151 |
+
});
|
152 |
+
}
|
153 |
+
} catch (error) {
|
154 |
+
// 如果读取单个文件失败,使用文件名作为标题
|
155 |
+
console.warn(`Failed to read PPT content for ${file.name}:`, error.message);
|
156 |
+
results.push({
|
157 |
+
name: file.name.replace('.json', ''),
|
158 |
+
title: file.name.replace('.json', ''),
|
159 |
+
lastModified: new Date().toISOString(),
|
160 |
+
repoIndex: i,
|
161 |
+
repoUrl: repoUrl
|
162 |
+
});
|
163 |
+
}
|
164 |
+
}
|
165 |
+
} catch (error) {
|
166 |
+
if (error.response?.status !== 404) {
|
167 |
+
console.error(`Error fetching files from repo ${i}:`, error.message);
|
168 |
+
}
|
169 |
+
}
|
170 |
+
}
|
171 |
+
|
172 |
+
return results;
|
173 |
+
}
|
174 |
+
|
175 |
+
// 删除文件
|
176 |
+
async deleteFile(userId, fileName, repoIndex = 0) {
|
177 |
+
// 如果使用内存存储
|
178 |
+
if (this.useMemoryStorage) {
|
179 |
+
return await memoryStorageService.deleteFile(userId, fileName);
|
180 |
+
}
|
181 |
+
|
182 |
+
// 原有的GitHub逻辑
|
183 |
+
const existing = await this.getFile(userId, fileName, repoIndex);
|
184 |
+
if (!existing) {
|
185 |
+
throw new Error('File not found');
|
186 |
+
}
|
187 |
+
|
188 |
+
const repoUrl = this.repositories[repoIndex];
|
189 |
+
const { owner, repo } = this.parseRepoUrl(repoUrl);
|
190 |
+
const path = `users/${userId}/${fileName}`;
|
191 |
+
|
192 |
+
const response = await axios.delete(
|
193 |
+
`${this.apiUrl}/repos/${owner}/${repo}/contents/${path}`,
|
194 |
+
{
|
195 |
+
data: {
|
196 |
+
message: `Delete ${fileName} for user ${userId}`,
|
197 |
+
sha: existing.sha,
|
198 |
+
branch: 'main'
|
199 |
+
},
|
200 |
+
headers: {
|
201 |
+
'Authorization': `token ${this.token}`,
|
202 |
+
'Accept': 'application/vnd.github.v3+json'
|
203 |
+
}
|
204 |
+
}
|
205 |
+
);
|
206 |
+
|
207 |
+
return response.data;
|
208 |
+
}
|
209 |
+
}
|
210 |
+
|
211 |
+
export default new GitHubService();
|
backend/src/services/memoryStorageService.js
ADDED
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { v4 as uuidv4 } from 'uuid';
|
2 |
+
|
3 |
+
// 内存存储,用于开发和测试
|
4 |
+
class MemoryStorageService {
|
5 |
+
constructor() {
|
6 |
+
this.storage = new Map(); // userId -> Map<pptId, pptData>
|
7 |
+
}
|
8 |
+
|
9 |
+
// 获取用户的所有PPT列表
|
10 |
+
async getUserPPTList(userId) {
|
11 |
+
const userStorage = this.storage.get(userId) || new Map();
|
12 |
+
const pptList = Array.from(userStorage.entries()).map(([pptId, pptData]) => ({
|
13 |
+
name: pptId,
|
14 |
+
title: pptData.title || '未命名演示文稿',
|
15 |
+
lastModified: pptData.updatedAt,
|
16 |
+
repoIndex: 0,
|
17 |
+
repoUrl: 'memory://local'
|
18 |
+
}));
|
19 |
+
return pptList;
|
20 |
+
}
|
21 |
+
|
22 |
+
// 获取PPT数据
|
23 |
+
async getFile(userId, fileName) {
|
24 |
+
const pptId = fileName.replace('.json', '');
|
25 |
+
const userStorage = this.storage.get(userId) || new Map();
|
26 |
+
const pptData = userStorage.get(pptId);
|
27 |
+
|
28 |
+
if (!pptData) {
|
29 |
+
return null;
|
30 |
+
}
|
31 |
+
|
32 |
+
return {
|
33 |
+
content: pptData,
|
34 |
+
sha: 'memory-sha'
|
35 |
+
};
|
36 |
+
}
|
37 |
+
|
38 |
+
// 保存PPT数据
|
39 |
+
async saveFile(userId, fileName, data) {
|
40 |
+
const pptId = fileName.replace('.json', '');
|
41 |
+
|
42 |
+
if (!this.storage.has(userId)) {
|
43 |
+
this.storage.set(userId, new Map());
|
44 |
+
}
|
45 |
+
|
46 |
+
const userStorage = this.storage.get(userId);
|
47 |
+
userStorage.set(pptId, {
|
48 |
+
...data,
|
49 |
+
updatedAt: new Date().toISOString()
|
50 |
+
});
|
51 |
+
|
52 |
+
return { success: true };
|
53 |
+
}
|
54 |
+
|
55 |
+
// 删除PPT
|
56 |
+
async deleteFile(userId, fileName) {
|
57 |
+
const pptId = fileName.replace('.json', '');
|
58 |
+
const userStorage = this.storage.get(userId);
|
59 |
+
|
60 |
+
if (!userStorage || !userStorage.has(pptId)) {
|
61 |
+
throw new Error('File not found');
|
62 |
+
}
|
63 |
+
|
64 |
+
userStorage.delete(pptId);
|
65 |
+
return { success: true };
|
66 |
+
}
|
67 |
+
}
|
68 |
+
|
69 |
+
export default new MemoryStorageService();
|