团队工作进展:
- UI设计: 文件列表页 Draw.io 设计稿 - 架构: 数据库设计文档 - 后端: 用户认证接口实现 (login/register) 待完成: - 前端 Electron 主进程开发 - 文件管理 API 开发 - 测试用例编写
This commit is contained in:
parent
e8733a1de2
commit
f339513be8
57
architecture/database.md
Normal file
57
architecture/database.md
Normal file
@ -0,0 +1,57 @@
|
||||
# 数据库设计
|
||||
|
||||
## 表结构
|
||||
|
||||
### users (用户表)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER PRIMARY KEY | 用户ID |
|
||||
| username | VARCHAR(50) UNIQUE | 用户名 |
|
||||
| password_hash | VARCHAR(255) | 密码哈希 |
|
||||
| email | VARCHAR(100) | 邮箱 |
|
||||
| created_at | DATETIME | 创建时间 |
|
||||
| updated_at | DATETIME | 更新时间 |
|
||||
| storage_used | BIGINT | 已用存储(字节) |
|
||||
| storage_limit | BIGINT | 存储上限(字节) |
|
||||
|
||||
### files (文件表)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER PRIMARY KEY | 文件ID |
|
||||
| user_id | INTEGER | 所属用户ID |
|
||||
| parent_id | INTEGER | 父文件夹ID |
|
||||
| name | VARCHAR(255) | 文件名 |
|
||||
| type | VARCHAR(20) | 文件类型 |
|
||||
| size | BIGINT | 文件大小 |
|
||||
| path | VARCHAR(500) | 存储路径 |
|
||||
| hash | VARCHAR(64) | 文件哈希 |
|
||||
| is_folder | BOOLEAN | 是否文件夹 |
|
||||
| created_at | DATETIME | 创建时间 |
|
||||
| updated_at | DATETIME | 更新时间 |
|
||||
| deleted_at | DATETIME | 删除时间 |
|
||||
|
||||
### shares (分享表)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER PRIMARY KEY | 分享ID |
|
||||
| file_id | INTEGER | 文件ID |
|
||||
| share_token | VARCHAR(64) | 分享令牌 |
|
||||
| password | VARCHAR(255) | 访问密码 |
|
||||
| expires_at | DATETIME | 过期时间 |
|
||||
| view_count | INTEGER | 查看次数 |
|
||||
| created_at | DATETIME | 创建时间 |
|
||||
|
||||
### sync_logs (同步日志)
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | INTEGER PRIMARY KEY | 日志ID |
|
||||
| user_id | INTEGER | 用户ID |
|
||||
| file_id | INTEGER | 文件ID |
|
||||
| action | VARCHAR(20) | 操作类型 |
|
||||
| status | VARCHAR(20) | 状态 |
|
||||
| created_at | DATETIME | 创建时间 |
|
||||
|
||||
## 索引
|
||||
- files: user_id, parent_id, hash
|
||||
- shares: share_token
|
||||
- sync_logs: user_id, created_at
|
||||
59
backend/src/db.js
Normal file
59
backend/src/db.js
Normal file
@ -0,0 +1,59 @@
|
||||
// Simple SQLite database wrapper
|
||||
// In production, use better-sqlite3 or similar
|
||||
|
||||
const Database = require('better-sqlite3');
|
||||
const db = new Database('clouddisk.db');
|
||||
|
||||
// Initialize tables
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password_hash TEXT NOT NULL,
|
||||
email TEXT,
|
||||
storage_used INTEGER DEFAULT 0,
|
||||
storage_limit INTEGER DEFAULT 10737418240,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
parent_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT,
|
||||
size INTEGER DEFAULT 0,
|
||||
path TEXT,
|
||||
hash TEXT,
|
||||
is_folder INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at DATETIME,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (parent_id) REFERENCES files(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS shares (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
file_id INTEGER NOT NULL,
|
||||
share_token TEXT UNIQUE NOT NULL,
|
||||
password TEXT,
|
||||
expires_at DATETIME,
|
||||
view_count INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (file_id) REFERENCES files(id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sync_logs (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
file_id INTEGER,
|
||||
action TEXT,
|
||||
status TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
);
|
||||
`);
|
||||
|
||||
module.exports = db;
|
||||
25
backend/src/index.js
Normal file
25
backend/src/index.js
Normal file
@ -0,0 +1,25 @@
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const authRoutes = require('./routes/auth');
|
||||
const fileRoutes = require('./routes/files');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
|
||||
// Routes
|
||||
app.use('/api/auth', authRoutes);
|
||||
app.use('/api/files', fileRoutes);
|
||||
|
||||
// Health check
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`CloudDisk API running on port ${PORT}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
86
backend/src/routes/auth.js
Normal file
86
backend/src/routes/auth.js
Normal file
@ -0,0 +1,86 @@
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const db = require('../db');
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'clouddisk-secret-key';
|
||||
|
||||
// Register
|
||||
router.post('/register', async (req, res) => {
|
||||
try {
|
||||
const { username, password, email } = req.body;
|
||||
|
||||
// Check if user exists
|
||||
const existingUser = db.query('SELECT id FROM users WHERE username = ?', [username]);
|
||||
if (existingUser.length > 0) {
|
||||
return res.status(400).json({ error: 'Username already exists' });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const passwordHash = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create user
|
||||
const result = db.run(
|
||||
'INSERT INTO users (username, password_hash, email, storage_limit) VALUES (?, ?, ?, ?)',
|
||||
[username, passwordHash, email, 10 * 1024 * 1024 * 1024] // 10GB
|
||||
);
|
||||
|
||||
const token = jwt.sign({ userId: result.lastInsertRowid }, JWT_SECRET);
|
||||
res.json({ token, userId: result.lastInsertRowid });
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Login
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
|
||||
const user = db.query('SELECT * FROM users WHERE username = ?', [username]);
|
||||
if (user.length === 0) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const validPassword = await bcrypt.compare(password, user[0].password_hash);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ error: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const token = jwt.sign({ userId: user[0].id }, JWT_SECRET);
|
||||
res.json({
|
||||
token,
|
||||
user: {
|
||||
id: user[0].id,
|
||||
username: user[0].username,
|
||||
email: user[0].email,
|
||||
storageUsed: user[0].storage_used,
|
||||
storageLimit: user[0].storage_limit
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current user
|
||||
router.get('/me', (req, res) => {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'No token provided' });
|
||||
}
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
const user = db.query('SELECT id, username, email, storage_used, storage_limit FROM users WHERE id = ?', [decoded.userId]);
|
||||
if (user.length === 0) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
res.json({ user: user[0] });
|
||||
} catch (error) {
|
||||
res.status(401).json({ error: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
75
ui-design/designs/file-list-page.drawio
Normal file
75
ui-design/designs/file-list-page.drawio
Normal file
@ -0,0 +1,75 @@
|
||||
<mxfile host="app.diagrams.net" modified="2026-03-10T07:20:00Z" agent="OpenClaw" version="21.0.0">
|
||||
<diagram name="文件列表页">
|
||||
<mxGraphModel dx="1000" dy="700" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1200" pageHeight="700" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
|
||||
<!-- 顶部导航栏 -->
|
||||
<mxCell id="header" value="CloudDisk - 我的网盘" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#1890ff;strokeColor=#096dd9;fontColor=#ffffff;fontSize=16" vertex="1" parent="1">
|
||||
<mxGeometry x="0" y="0" width="1200" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<mxCell id="search" value="🔍 搜索文件..." style="rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="400" y="10" width="300" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<mxCell id="sidebar" value="我的文件
最近访问
共享文件
回收站
──────
存储空间: 2.1GB/10GB" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#fafafa;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="0" y="50" width="200" height="650" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 工具栏 -->
|
||||
<mxCell id="toolbar" value="⬆ 上传 📁 新建文件夹 ⬜ 全选 🗑 删除" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="50" width="1000" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 面包屑 -->
|
||||
<mxCell id="breadcrumb" value="全部文件 > Documents > Project" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="90" width="1000" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 文件列表区域 -->
|
||||
<mxCell id="fileArea" value="" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#ffffff" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="120" width="1000" height="580" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 文件1 -->
|
||||
<mxCell id="file1" value="📁 Project" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff7e6;strokeColor=#ffd591" vertex="1" parent="1">
|
||||
<mxGeometry x="220" y="140" width="180" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 文件2 -->
|
||||
<mxCell id="file2" value="📄 report.pdf
2.5MB · 2026-03-01" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="420" y="140" width="180" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 文件3 -->
|
||||
<mxCell id="file3" value="🖼 photo.jpg
3.2MB · 2026-02-28" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="620" y="140" width="180" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 文件4 -->
|
||||
<mxCell id="file4" value="📄 notes.docx
156KB · 2026-02-25" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="820" y="140" width="180" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 文件5 -->
|
||||
<mxCell id="file5" value="📁 Documents" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#fff7e6;strokeColor=#ffd591" vertex="1" parent="1">
|
||||
<mxGeometry x="220" y="240" width="180" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 文件6 -->
|
||||
<mxCell id="file6" value="🎵 music.mp3
5.1MB · 2026-02-20" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ffffff;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="420" y="240" width="180" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
|
||||
<!-- 底部状态栏 -->
|
||||
<mxCell id="status" value="已选择 3 个文件 · 共 15.2GB" style="rounded=0;whiteSpace=wrap;html=1;fillColor=#f5f5f5;strokeColor=#d9d9d9" vertex="1" parent="1">
|
||||
<mxGeometry x="200" y="650" width="1000" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
||||
Loading…
Reference in New Issue
Block a user