团队工作进展:

- UI设计: 文件列表页 Draw.io 设计稿
- 架构: 数据库设计文档
- 后端: 用户认证接口实现 (login/register)

待完成:
- 前端 Electron 主进程开发
- 文件管理 API 开发
- 测试用例编写
This commit is contained in:
Team 2026-03-10 07:24:38 +00:00
parent e8733a1de2
commit f339513be8
5 changed files with 302 additions and 0 deletions

57
architecture/database.md Normal file
View 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
View 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
View 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;

View 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;

View 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="我的文件&#xa;最近访问&#xa;共享文件&#xa;回收站&#xa;──────&#xa;存储空间: 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&#xa;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&#xa;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&#xa;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&#xa;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>