修复网址数据服务器持久化存储

This commit is contained in:
rjb
2025-12-23 11:09:02 +08:00
parent e5a9b31a6c
commit cd8b30e0d4
15 changed files with 1755 additions and 20 deletions

8
.dockerignore Normal file
View File

@@ -0,0 +1,8 @@
# 忽略不需要的文件
Dockerfile
docker-compose.yml
.dockerignore
README.md
.git
.gitignore

26
Dockerfile Normal file
View File

@@ -0,0 +1,26 @@
# 使用 Node.js 作为基础镜像
FROM node:18-alpine
WORKDIR /app
# 复制 package.json 并安装依赖
COPY package.json .
RUN npm install
# 复制服务器文件
COPY server.js .
# 创建 public 目录并复制静态文件
RUN mkdir -p public
COPY index.html public/
COPY script.js public/
COPY style.css public/
# 创建数据目录
RUN mkdir -p data
# 暴露端口
EXPOSE 3000
# 启动服务器
CMD ["node", "server.js"]

71
README.md Normal file
View File

@@ -0,0 +1,71 @@
# Android开发网址管理器
一个简洁美观的网址管理工具专为Android开发者设计。
## 功能特性
- 📚 网址分类管理
- 🔍 搜索和过滤
- ⭐ 收藏功能
- 🏷️ 标签系统
- 📊 统计信息
- 🌓 深色/浅色主题切换
- 💾 数据导入/导出
## Docker部署
### 方式一使用docker-compose推荐
```bash
# 构建并启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f
# 停止服务
docker-compose down
```
服务将在 `http://localhost:3006` 启动
### 方式二使用Docker命令
```bash
# 构建镜像
docker build -t workdizhi:latest .
# 运行容器
docker run -d -p 3006:80 --name workdizhi-web workdizhi:latest
# 查看日志
docker logs -f workdizhi-web
# 停止容器
docker stop workdizhi-web
# 删除容器
docker rm workdizhi-web
```
### 修改端口
如果需要使用其他端口例如80端口可以修改 `docker-compose.yml` 中的端口映射:
```yaml
ports:
- "80:80" # 改为80端口
```
## 技术栈
- 纯前端实现HTML + CSS + JavaScript
- 使用localStorage进行数据持久化
- 响应式设计,支持移动端
## 注意事项
- 数据存储在浏览器的localStorage中清除浏览器数据会丢失
- 建议定期使用导出功能备份数据
- 如需多用户或云端同步,需要后端支持

184
TROUBLESHOOTING.md Normal file
View File

@@ -0,0 +1,184 @@
# 故障排查指南
## 连接超时问题排查
如果访问 `http://101.43.95.130:3006` 时出现连接超时ERR_CONNECTION_TIMED_OUT请按以下步骤排查
### 1. 检查容器是否运行
```bash
# 查看容器状态
docker ps -a | grep workdizhi-web
# 或使用docker-compose
docker-compose ps
```
**如果容器没有运行:**
```bash
# 启动容器
docker-compose up -d
# 或使用docker命令
docker start workdizhi-web
```
### 2. 检查容器日志
```bash
# 查看容器日志,检查是否有错误
docker-compose logs -f
# 或
docker logs -f workdizhi-web
```
### 3. 检查端口监听状态
```bash
# 检查3006端口是否在监听
netstat -tlnp | grep 3006
# 或
ss -tlnp | grep 3006
# 或
lsof -i :3006
```
**如果端口没有监听,检查:**
- 容器是否正常启动
- docker-compose.yml 中的端口映射是否正确
### 4. 检查防火墙配置
#### CentOS/RHEL 7+ (firewalld)
```bash
# 查看防火墙状态
systemctl status firewalld
# 如果防火墙开启需要开放3006端口
firewall-cmd --permanent --add-port=3006/tcp
firewall-cmd --reload
# 验证端口是否开放
firewall-cmd --list-ports
```
#### Ubuntu/Debian (ufw)
```bash
# 查看防火墙状态
ufw status
# 开放3006端口
ufw allow 3006/tcp
# 重新加载
ufw reload
```
#### iptables
```bash
# 开放3006端口
iptables -A INPUT -p tcp --dport 3006 -j ACCEPT
iptables-save
```
### 5. 检查云服务器安全组(重要!)
如果使用的是云服务器阿里云、腾讯云、AWS等需要在控制台配置安全组规则
1. 登录云服务器控制台
2. 找到对应的服务器实例
3. 进入"安全组"配置
4. 添加入站规则:
- 协议TCP
- 端口3006
-0.0.0.0/0允许所有IP访问或指定特定IP
- 动作:允许
### 6. 本地测试
在服务器上测试本地访问:
```bash
# 测试本地3006端口
curl http://localhost:3006
# 或
curl http://127.0.0.1:3006
# 如果本地可以访问,说明容器正常,问题在防火墙或安全组
```
### 7. 检查Docker服务状态
```bash
# 检查Docker服务是否运行
systemctl status docker
# 如果未运行启动Docker
systemctl start docker
systemctl enable docker
```
### 8. 重新构建和启动
如果以上都正常,尝试重新构建和启动:
```bash
# 停止并删除旧容器
docker-compose down
# 重新构建镜像
docker-compose build --no-cache
# 启动服务
docker-compose up -d
# 查看日志
docker-compose logs -f
```
### 9. 验证部署
部署成功后,应该能看到:
- 容器状态为 `Up`
- 端口映射正确:`0.0.0.0:3006->80/tcp`
- 日志无错误信息
## 快速检查脚本
可以运行以下命令快速检查:
```bash
#!/bin/bash
echo "=== 检查容器状态 ==="
docker ps -a | grep workdizhi-web
echo -e "\n=== 检查端口监听 ==="
netstat -tlnp | grep 3006 || ss -tlnp | grep 3006
echo -e "\n=== 检查防火墙 ==="
if command -v firewall-cmd &> /dev/null; then
firewall-cmd --list-ports | grep 3006
elif command -v ufw &> /dev/null; then
ufw status | grep 3006
fi
echo -e "\n=== 测试本地访问 ==="
curl -I http://localhost:3006 2>&1 | head -1
```
## 常见问题
### Q: 容器启动后立即退出
**A:** 检查日志:`docker logs workdizhi-web`,可能是镜像构建失败或文件缺失
### Q: 本地可以访问,外网无法访问
**A:** 检查防火墙和安全组配置确保3006端口已开放
### Q: 端口被占用
**A:** 检查是否有其他服务占用3006端口或修改docker-compose.yml使用其他端口
### Q: 容器运行但无法访问
**A:** 检查Nginx配置确保容器内Nginx正常启动

287
data/urls.json Normal file
View File

@@ -0,0 +1,287 @@
{
"urls": [
{
"id": "1",
"title": "Android开发者文档",
"url": "https://developer.android.com",
"description": "官方Android开发文档和API参考",
"category": "dev-docs",
"icon": "fas fa-book",
"tags": [
"文档",
"官方",
"Android"
],
"favorite": true,
"createdAt": "2025-12-20T11:23:23.411Z",
"lastAccessed": "2025-12-20T11:34:32.393Z",
"accessCount": 2
},
{
"id": "2",
"title": "GitHub",
"url": "https://github.com",
"description": "代码托管和版本控制平台",
"category": "tools",
"icon": "fas fa-code",
"tags": [
"工具",
"代码",
"版本控制"
],
"favorite": true,
"createdAt": "2025-12-20T11:23:23.411Z",
"lastAccessed": "2025-12-21T14:19:54.525Z",
"accessCount": 5
},
{
"id": "3",
"title": "Stack Overflow",
"url": "https://stackoverflow.com",
"description": "开发者问答社区",
"category": "community",
"icon": "fas fa-comments",
"tags": [
"社区",
"问答",
"帮助"
],
"favorite": false,
"createdAt": "2025-12-20T11:23:23.411Z",
"lastAccessed": "2025-12-20T11:23:31.944Z",
"accessCount": 1
},
{
"id": "4",
"title": "Material Design",
"url": "https://material.io",
"description": "Google的设计系统",
"category": "design",
"icon": "fas fa-palette",
"tags": [
"设计",
"UI",
"Material"
],
"favorite": false,
"createdAt": "2025-12-20T11:23:23.411Z",
"lastAccessed": "2025-12-20T12:31:00.584Z",
"accessCount": 2
},
{
"id": "1766232957678",
"createdAt": "2025-12-20T12:15:57.678Z",
"lastAccessed": "2025-12-21T15:13:28.465Z",
"accessCount": 6,
"title": "gitea",
"url": "http://101.43.95.130:3001/",
"description": "公司内部代码托管和版本管理平台",
"category": "tools",
"icon": "fas fa-globe",
"tags": [
"代码版本管理"
],
"favorite": false
},
{
"id": "1766233366850",
"createdAt": "2025-12-20T12:22:46.850Z",
"lastAccessed": "2025-12-21T14:55:51.738Z",
"accessCount": 3,
"title": "gerrit(3.13.1)",
"url": "http://101.43.95.130:8082/",
"description": "gerrit版本使用docker容器部署的一款代码review平台。",
"category": "tools",
"icon": "fas fa-globe",
"tags": [
"代码审核"
],
"favorite": false
},
{
"id": "1766233826313",
"createdAt": "2025-12-20T12:30:26.313Z",
"lastAccessed": "2025-12-21T14:58:25.837Z",
"accessCount": 1,
"title": "Drone",
"url": "http://101.43.95.130:3002/",
"description": "流水线CI工具不仅能构建APK,还能做:自动备份\n自动部署后端服务\n自动生成文档\n自动通知飞书/钉钉\n自动执行脚本",
"category": "tools",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766233993733",
"createdAt": "2025-12-20T12:33:13.733Z",
"lastAccessed": "2025-12-21T14:58:46.376Z",
"accessCount": 2,
"title": "AI提示词优化",
"url": "http://101.43.95.130:5002/",
"description": "优化提示词",
"category": "tools",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766234237652",
"createdAt": "2025-12-20T12:37:17.652Z",
"lastAccessed": "2025-12-21T14:55:27.836Z",
"accessCount": 2,
"title": "gerrit(3.9.0)",
"url": "http://101.43.95.130:8080/",
"description": "原生方式部署的gerrit",
"category": "tools",
"icon": "fas fa-globe",
"tags": [
"代码审核工具"
],
"favorite": false
},
{
"id": "1766234344830",
"createdAt": "2025-12-20T12:39:04.830Z",
"lastAccessed": null,
"accessCount": 0,
"title": "蒲公英",
"url": "https://www.pgyer.com/",
"description": "",
"category": "tools",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766235165350",
"createdAt": "2025-12-20T12:52:45.350Z",
"lastAccessed": "2025-12-20T12:52:50.097Z",
"accessCount": 1,
"title": "知你后台网址",
"url": "https://xingyao.xunpaisoft.com/LMPTECaZKX.php/dashboard?ref=addtabs",
"description": "知你后台管理系统",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766235263277",
"createdAt": "2025-12-20T12:54:23.277Z",
"lastAccessed": "2025-12-20T12:54:29.783Z",
"accessCount": 1,
"title": "友盟统计(知你)",
"url": "https://mobile.umeng.com/platform/apps/list",
"description": "",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766235418609",
"createdAt": "2025-12-20T12:56:58.609Z",
"lastAccessed": "2025-12-20T13:52:05.611Z",
"accessCount": 1,
"title": "腾讯云",
"url": "https://console.cloud.tencent.com/",
"description": "",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766235454814",
"createdAt": "2025-12-20T12:57:34.814Z",
"lastAccessed": "2025-12-21T14:58:36.671Z",
"accessCount": 1,
"title": "智能饭菜推荐系统",
"url": "http://101.43.95.130:5003/",
"description": "",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766235507121",
"createdAt": "2025-12-20T12:58:27.121Z",
"lastAccessed": "2025-12-20T14:23:52.126Z",
"accessCount": 1,
"title": "使用 PromptForge 构建更智能的AI应用",
"url": "http://101.43.95.130:3000/",
"description": "",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766235612635",
"createdAt": "2025-12-20T13:00:12.635Z",
"lastAccessed": "2025-12-20T13:00:17.586Z",
"accessCount": 1,
"title": "知你蓝胡",
"url": "https://lanhuapp.com/web/#/item/project/stage?pid=8562bbb3-7986-4478-ad9d-725d1d22a2f4&image_id=ca8a08af-16b7-48f7-8ec2-5334a2ab70c6&tid=0be10d44-29d1-429f-9a42-53814cda6ad7",
"description": "",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766235853976",
"createdAt": "2025-12-20T13:04:13.976Z",
"lastAccessed": null,
"accessCount": 0,
"title": "腾讯bugly",
"url": "https://bugly.tds.qq.com/v2/index/main?from=buglyqq",
"description": "",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
},
{
"id": "1766244183937",
"createdAt": "2025-12-20T15:23:03.937Z",
"lastAccessed": "2025-12-20T15:23:07.021Z",
"accessCount": 1,
"title": "码云",
"url": "https://gitee.com/",
"description": "代码托管",
"category": "",
"icon": "fas fa-globe",
"tags": [],
"favorite": false
}
],
"categories": [
{
"id": "dev-docs",
"name": "开发文档",
"color": "#3498db",
"icon": "fas fa-book"
},
{
"id": "tools",
"name": "工具",
"color": "#2ecc71",
"icon": "fas fa-tools"
},
{
"id": "community",
"name": "社区",
"color": "#9b59b6",
"icon": "fas fa-comments"
},
{
"id": "design",
"name": "设计",
"color": "#e74c3c",
"icon": "fas fa-palette"
}
],
"lastUpdated": "2025-12-22T14:53:21.453Z"
}

21
docker-compose.yml Normal file
View File

@@ -0,0 +1,21 @@
version: '3.8'
services:
web:
build: .
container_name: workdizhi-web
ports:
- "3006:3000" # 将容器的3000端口映射到主机的3006端口
restart: unless-stopped
volumes:
# 挂载数据目录,实现数据持久化
- ./data:/app/data
environment:
- PORT=3000
networks:
- workdizhi-network
networks:
workdizhi-network:
driver: bridge

21
docker-compose.yml.fix Normal file
View File

@@ -0,0 +1,21 @@
version: '3.8'
services:
web:
build: .
container_name: workdizhi-web
ports:
- "3006:80" # 将容器的80端口映射到主机的3006端口
restart: unless-stopped
volumes:
# 挂载数据目录,用于持久化存储
- ./data:/usr/share/nginx/html/data:ro
# 如果需要,也可以挂载整个 html 目录以便更新
# - ./:/usr/share/nginx/html:ro
networks:
- workdizhi-network
networks:
workdizhi-network:
driver: bridge

21
package.json Normal file
View File

@@ -0,0 +1,21 @@
{
"name": "workdizhi",
"version": "1.0.0",
"description": "Android开发网址管理器 - 带服务器端数据存储",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"body-parser": "^1.20.2"
},
"keywords": [
"url-manager",
"android-dev"
],
"author": "",
"license": "MIT"
}

117
script.js
View File

@@ -9,20 +9,35 @@ class UrlManager {
this.currentTag = ''; this.currentTag = '';
this.viewMode = 'grid'; this.viewMode = 'grid';
this.editingUrlId = null; this.editingUrlId = null;
this.init();
} }
init() { async init() {
this.loadData(); await this.loadData();
this.setupEventListeners(); this.setupEventListeners();
this.render(); this.render();
this.setupKeyboardShortcuts(); this.setupKeyboardShortcuts();
} }
// 数据存储 // 数据存储
loadData() { async loadData() {
// 加载默认数据或从localStorage加载 try {
// 优先从服务器加载数据
const response = await fetch('/api/urls');
if (response.ok) {
const data = await response.json();
this.urls = data.urls && data.urls.length > 0 ? data.urls : this.getDefaultUrls();
this.categories = data.categories && data.categories.length > 0 ? data.categories : this.getDefaultCategories();
// 同时保存到 localStorage 作为备份
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
return;
}
} catch (error) {
console.warn('从服务器加载数据失败,尝试从 localStorage 加载:', error);
}
// 降级到 localStorage
const savedUrls = localStorage.getItem('urlManager_urls'); const savedUrls = localStorage.getItem('urlManager_urls');
const savedCategories = localStorage.getItem('urlManager_categories'); const savedCategories = localStorage.getItem('urlManager_categories');
@@ -40,15 +55,49 @@ class UrlManager {
} }
} }
saveData() { async saveData() {
try { try {
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls)); const dataToSave = {
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories)); urls: this.urls || [],
categories: this.categories || []
};
console.log('保存数据:', {
urlsCount: dataToSave.urls.length,
categoriesCount: dataToSave.categories.length
});
// 优先保存到服务器
const response = await fetch('/api/urls', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dataToSave)
});
if (response.ok) {
const result = await response.json();
console.log('服务器保存成功:', result);
// 服务器保存成功,同时保存到 localStorage 作为备份
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
return true;
} else {
const errorText = await response.text();
console.error('服务器返回错误:', response.status, errorText);
throw new Error(`服务器返回错误: ${response.status} - ${errorText}`);
}
} catch (error) { } catch (error) {
console.error('保存数据到localStorage失败:', error); console.error('保存到服务器失败:', error);
// 如果localStorage已满或其他错误,只记录错误,不抛出异常 // 降级到 localStorage
// 因为数据已经在内存中了,只是无法持久化 try {
// 抛出异常会导致误报"保存失败" localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
console.warn('已降级保存到 localStorage');
} catch (e) {
console.error('保存数据到localStorage也失败:', e);
}
throw error; // 重新抛出错误,让调用者知道保存失败
} }
} }
@@ -809,20 +858,47 @@ class UrlManager {
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = async (e) => {
try { try {
const data = JSON.parse(e.target.result); const data = JSON.parse(e.target.result);
console.log('导入数据:', data);
if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) { if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) {
if (data.urls) this.urls = data.urls; // 更新数据
if (data.categories) this.categories = data.categories; if (data.urls) {
this.saveData(); this.urls = data.urls;
console.log('导入网址数量:', this.urls.length);
} else {
this.urls = [];
}
if (data.categories) {
this.categories = data.categories;
console.log('导入分类数量:', this.categories.length);
} else {
this.categories = [];
}
// 等待数据保存到服务器
console.log('开始保存数据到服务器...');
await this.saveData();
console.log('数据保存完成');
// 验证保存是否成功
const verifyResponse = await fetch('/api/urls');
if (verifyResponse.ok) {
const savedData = await verifyResponse.json();
console.log('服务器上的数据:', savedData);
console.log('服务器网址数量:', savedData.urls ? savedData.urls.length : 0);
}
this.render(); this.render();
this.closeAllModals(); this.closeAllModals();
alert('数据导入成功!'); alert(`数据导入成功!已导入 ${this.urls.length} 个网址,${this.categories.length} 个分类,并已保存到服务器!`);
} }
} catch (error) { } catch (error) {
alert('文件格式错误请选择有效的JSON文件'); console.error('导入数据失败:', error);
alert('文件格式错误或保存失败: ' + error.message);
} }
}; };
reader.readAsText(file); reader.readAsText(file);
@@ -1019,6 +1095,7 @@ class UrlManager {
} }
// 初始化应用 // 初始化应用
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', async () => {
window.urlManager = new UrlManager(); window.urlManager = new UrlManager();
await window.urlManager.init();
}); });

86
server.js Normal file
View File

@@ -0,0 +1,86 @@
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const cors = require('cors');
const bodyParser = require('body-parser');
const app = express();
const DATA_DIR = path.join(__dirname, 'data');
const DATA_FILE = path.join(DATA_DIR, 'urls.json');
const PORT = process.env.PORT || 3000;
// 中间件
app.use(cors());
app.use(bodyParser.json({ limit: '10mb' }));
app.use(express.static('public'));
// 确保数据目录存在
async function ensureDataDir() {
try {
await fs.mkdir(DATA_DIR, { recursive: true });
} catch (error) {
console.error('创建数据目录失败:', error);
}
}
// 读取数据
app.get('/api/urls', async (req, res) => {
try {
await ensureDataDir();
const data = await fs.readFile(DATA_FILE, 'utf8');
res.json(JSON.parse(data));
} catch (error) {
if (error.code === 'ENOENT') {
// 文件不存在,返回空数据
res.json({ urls: [], categories: [] });
} else {
console.error('读取数据失败:', error);
res.status(500).json({ error: '读取数据失败', message: error.message });
}
}
});
// 保存数据
app.post('/api/urls', async (req, res) => {
try {
await ensureDataDir();
const data = {
urls: req.body.urls || [],
categories: req.body.categories || [],
lastUpdated: new Date().toISOString()
};
console.log('收到保存请求:', {
urlsCount: data.urls.length,
categoriesCount: data.categories.length,
timestamp: data.lastUpdated
});
await fs.writeFile(DATA_FILE, JSON.stringify(data, null, 2), 'utf8');
// 验证文件是否写入成功
const verifyData = await fs.readFile(DATA_FILE, 'utf8');
const verifyJson = JSON.parse(verifyData);
console.log('数据已保存,验证:', {
urlsCount: verifyJson.urls.length,
categoriesCount: verifyJson.categories.length
});
res.json({ success: true, message: '数据保存成功', savedCount: { urls: data.urls.length, categories: data.categories.length } });
} catch (error) {
console.error('保存数据失败:', error);
res.status(500).json({ error: '保存数据失败', message: error.message });
}
});
// 健康检查
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 启动服务器
app.listen(PORT, () => {
console.log(`Workdizhi 服务器运行在端口 ${PORT}`);
console.log(`数据文件位置: ${DATA_FILE}`);
});

View File

@@ -0,0 +1,297 @@
# Workdizhi 数据持久化修复方案
## 问题分析
**当前问题**
- 数据存储在浏览器的 `localStorage`
- `docker-compose.yml` 中没有数据卷挂载
- 在新电脑打开时,浏览器的 localStorage 是空的,导致数据丢失
**根本原因**
- `localStorage` 是浏览器本地存储,不是服务器端存储
- 容器中没有数据持久化配置
- 数据没有保存到服务器文件系统
## 解决方案
### 方案 1: 添加后端 API + 文件存储(推荐)
这是最彻底的解决方案,需要修改代码添加后端 API。
#### 步骤 1: 创建简单的 Node.js 后端
创建 `server.js`
```javascript
const express = require('express');
const fs = require('fs').promises;
const path = require('path');
const cors = require('cors');
const app = express();
const DATA_FILE = path.join(__dirname, 'data', 'urls.json');
app.use(cors());
app.use(express.json());
app.use(express.static('public'));
// 确保数据目录存在
async function ensureDataDir() {
const dataDir = path.dirname(DATA_FILE);
await fs.mkdir(dataDir, { recursive: true });
}
// 读取数据
app.get('/api/urls', async (req, res) => {
try {
await ensureDataDir();
const data = await fs.readFile(DATA_FILE, 'utf8');
res.json(JSON.parse(data));
} catch (error) {
if (error.code === 'ENOENT') {
res.json({ urls: [], categories: [] });
} else {
res.status(500).json({ error: error.message });
}
}
});
// 保存数据
app.post('/api/urls', async (req, res) => {
try {
await ensureDataDir();
await fs.writeFile(DATA_FILE, JSON.stringify(req.body, null, 2));
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
```
#### 步骤 2: 修改前端代码
修改 `script.js` 中的数据加载和保存方法:
```javascript
// 修改 loadData 方法
async loadData() {
try {
const response = await fetch('/api/urls');
const data = await response.json();
this.urls = data.urls || this.getDefaultUrls();
this.categories = data.categories || this.getDefaultCategories();
} catch (error) {
console.error('从服务器加载数据失败,使用默认数据:', error);
// 降级到 localStorage
const savedUrls = localStorage.getItem('urlManager_urls');
const savedCategories = localStorage.getItem('urlManager_categories');
if (savedUrls) {
this.urls = JSON.parse(savedUrls);
} else {
this.urls = this.getDefaultUrls();
}
if (savedCategories) {
this.categories = JSON.parse(savedCategories);
} else {
this.categories = this.getDefaultCategories();
}
}
}
// 修改 saveData 方法
async saveData() {
try {
// 先保存到服务器
const response = await fetch('/api/urls', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
urls: this.urls,
categories: this.categories
})
});
if (!response.ok) throw new Error('服务器保存失败');
} catch (error) {
console.error('保存到服务器失败,降级到 localStorage:', error);
// 降级到 localStorage
try {
localStorage.setItem('urlManager_urls', JSON.stringify(this.urls));
localStorage.setItem('urlManager_categories', JSON.stringify(this.categories));
} catch (e) {
console.error('保存到 localStorage 也失败:', e);
}
}
}
```
#### 步骤 3: 更新 Dockerfile
```dockerfile
FROM node:18-alpine
WORKDIR /app
# 安装依赖
COPY package.json .
RUN npm install
# 复制文件
COPY server.js .
COPY index.html public/
COPY script.js public/
COPY style.css public/
# 创建数据目录
RUN mkdir -p data
EXPOSE 3000
CMD ["node", "server.js"]
```
#### 步骤 4: 更新 docker-compose.yml
```yaml
version: '3.8'
services:
web:
build: .
container_name: workdizhi-web
ports:
- "3006:3000"
restart: unless-stopped
volumes:
# 挂载数据目录,持久化存储
- ./data:/app/data
networks:
- workdizhi-network
networks:
workdizhi-network:
driver: bridge
```
### 方案 2: 使用 Nginx + 文件存储(简单方案)
如果不想添加后端,可以使用 Nginx 的静态文件服务 + JavaScript 文件操作。
#### 步骤 1: 修改 script.js 使用 fetch API 保存到文件
```javascript
// 添加文件保存方法
async saveToFile() {
try {
const data = {
urls: this.urls,
categories: this.categories,
timestamp: new Date().toISOString()
};
// 使用 Blob 和下载方式保存(需要用户手动操作)
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'workdizhi_backup.json';
a.click();
URL.revokeObjectURL(url);
} catch (error) {
console.error('保存文件失败:', error);
}
}
// 添加文件加载方法
async loadFromFile(file) {
try {
const text = await file.text();
const data = JSON.parse(text);
this.urls = data.urls || this.getDefaultUrls();
this.categories = data.categories || this.getDefaultCategories();
this.saveData();
this.render();
} catch (error) {
console.error('加载文件失败:', error);
alert('文件格式错误或损坏');
}
}
```
### 方案 3: 导出/导入功能(临时方案)
添加导出和导入功能,让用户可以手动备份和恢复数据。
#### 修改 script.js 添加导出/导入功能
```javascript
// 导出数据
exportData() {
const data = {
urls: this.urls,
categories: this.categories,
exportDate: new Date().toISOString()
};
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `workdizhi_backup_${new Date().toISOString().split('T')[0]}.json`;
a.click();
URL.revokeObjectURL(url);
}
// 导入数据
importData(file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const data = JSON.parse(e.target.result);
this.urls = data.urls || [];
this.categories = data.categories || [];
this.saveData();
this.render();
alert('数据导入成功!');
} catch (error) {
alert('文件格式错误,导入失败');
}
};
reader.readAsText(file);
}
```
## 推荐实施步骤
### 快速修复(方案 3 - 导出/导入)
1. 添加导出/导入按钮到界面
2. 用户可以定期导出数据备份
3. 在新电脑上导入备份文件
### 长期方案(方案 1 - 后端 API
1. 创建 Node.js 后端服务
2. 修改前端代码使用 API
3. 添加数据卷挂载
4. 重新构建和部署
## 当前数据位置
- **浏览器 localStorage**: `urlManager_urls`, `urlManager_categories`
- **服务器**: 无持久化存储
## 修复后的数据位置
- **服务器文件**: `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
- **数据卷**: 挂载到容器内的 `/app/data``/usr/share/nginx/html/data`
## 注意事项
1. **备份现有数据**: 在修改前,先导出当前 localStorage 中的数据
2. **测试**: 修改后充分测试数据保存和加载功能
3. **迁移**: 如果已有数据,需要提供迁移脚本

106
导入功能说明.md Normal file
View File

@@ -0,0 +1,106 @@
# 导入功能持久化说明
## ✅ 导入数据会持久化
**是的,导入数据后会持久化到服务器!**
## 🔄 导入流程
1. **选择文件**: 用户选择 JSON 文件
2. **解析数据**: 读取并解析 JSON 文件
3. **确认导入**: 用户确认覆盖当前数据
4. **更新内存**: 将导入的数据加载到内存
5. **保存到服务器**: 调用 `saveData()` 方法,数据会:
- ✅ 优先保存到服务器 (`/api/urls` POST 请求)
- ✅ 保存到服务器文件 (`/app/data/urls.json`)
- ✅ 同时保存到 localStorage 作为备份
6. **渲染界面**: 更新页面显示
7. **提示成功**: 显示"数据导入成功并已保存到服务器!"
## 📋 代码实现
```javascript
handleFileImport(file) {
reader.onload = async (e) => {
const data = JSON.parse(e.target.result);
if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) {
// 更新数据
if (data.urls) this.urls = data.urls;
if (data.categories) this.categories = data.categories;
// 等待保存到服务器(关键步骤)
await this.saveData();
this.render();
alert('数据导入成功并已保存到服务器!');
}
};
}
```
## ✅ 验证方法
### 方法 1: 检查服务器文件
```bash
# 导入数据后,检查服务器文件
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
```
### 方法 2: 检查 API
```bash
# 导入数据后,通过 API 获取数据
curl http://localhost:3006/api/urls
```
### 方法 3: 重启容器测试
```bash
# 导入数据后,重启容器
docker restart workdizhi-web
# 访问网站,数据应该还在
```
## 🔍 数据流向
```
导入 JSON 文件
解析数据
更新内存 (this.urls, this.categories)
调用 saveData()
POST /api/urls (保存到服务器)
写入 /app/data/urls.json (持久化)
同时保存到 localStorage (备份)
✅ 数据已持久化
```
## ⚠️ 注意事项
1. **异步操作**: `saveData()` 是异步方法,导入时会等待保存完成
2. **数据覆盖**: 导入会覆盖当前所有数据,请先备份
3. **文件格式**: 必须是有效的 JSON 格式
4. **网络问题**: 如果服务器保存失败,会降级到 localStorage
## 🎯 总结
-**导入数据会持久化**: 数据会保存到服务器文件
-**自动保存**: 导入后自动调用 `saveData()`
-**多级备份**: 服务器文件 + localStorage
-**跨设备同步**: 所有设备访问同一服务器,数据同步
## 📝 使用建议
1. **定期导出**: 建议定期导出数据作为备份
2. **导入前备份**: 导入前先导出当前数据
3. **验证导入**: 导入后检查数据是否正确
4. **重启测试**: 重启容器验证数据是否持久化

201
导入问题排查指南.md Normal file
View File

@@ -0,0 +1,201 @@
# 导入数据问题排查指南
## 🔍 问题现象
导入数据后,在另一台电脑只能看到一条测试网址,说明导入的数据没有正确保存到服务器。
## 🔧 已实施的修复
### 1. 增强错误处理
- 添加了详细的控制台日志
- 保存失败时会显示具体错误信息
- 导入成功后会显示导入的数据数量
### 2. 添加数据验证
- 导入后会自动验证服务器上的数据
- 显示导入的网址和分类数量
- 保存失败时会抛出错误
### 3. 增强服务器日志
- 服务器会记录每次保存请求的详细信息
- 记录保存的网址和分类数量
- 保存后会验证文件内容
## 📋 排查步骤
### 步骤 1: 检查浏览器控制台
1. 打开网站http://101.43.95.130:3006
2. 按 F12 打开开发者工具
3. 切换到 Console 标签
4. 执行导入操作
5. 查看控制台输出,应该看到:
- `导入数据: {...}`
- `导入网址数量: X`
- `开始保存数据到服务器...`
- `服务器保存成功: {...}`
- `服务器上的数据: {...}`
### 步骤 2: 检查网络请求
1. 在开发者工具中切换到 Network 标签
2. 执行导入操作
3. 查找 POST 请求到 `/api/urls`
4. 检查:
- 请求状态码(应该是 200
- 请求体(应该包含所有导入的数据)
- 响应内容(应该显示成功)
### 步骤 3: 检查服务器日志
```bash
# 查看容器日志
docker logs -f workdizhi-web
# 执行导入操作,应该看到:
# - "收到保存请求: { urlsCount: X, categoriesCount: Y }"
# - "数据已保存,验证: { urlsCount: X, categoriesCount: Y }"
```
### 步骤 4: 检查数据文件
```bash
# 检查服务器上的数据文件
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
# 检查容器内的数据文件
docker exec workdizhi-web cat /app/data/urls.json
# 通过 API 获取数据
curl http://localhost:3006/api/urls
```
## 🐛 常见问题
### 问题 1: 导入的数据格式不正确
**症状**: 控制台显示 "文件格式错误"
**解决**:
- 确保 JSON 文件格式正确
- 文件应包含 `urls``categories` 字段
- 示例格式:
```json
{
"urls": [
{"id": "1", "title": "网址1", "url": "https://example.com"}
],
"categories": []
}
```
### 问题 2: 保存请求失败
**症状**: 控制台显示 "保存到服务器失败"
**检查**:
1. 网络连接是否正常
2. 服务器是否运行:`docker ps | grep workdizhi`
3. API 是否可访问:`curl http://localhost:3006/api/health`
**解决**:
```bash
# 重启服务
cd /home/renjianbo/devops/workdizhi/workdizhi
docker-compose restart
```
### 问题 3: 数据保存但读取时为空
**症状**: 保存成功,但读取时只有默认数据
**检查**:
1. 数据文件是否存在
2. 文件内容是否正确
3. 文件权限是否正确
**解决**:
```bash
# 检查文件
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
# 修复权限(如果需要)
sudo chown -R renjianbo:renjianbo /home/renjianbo/devops/workdizhi/workdizhi/data/
```
### 问题 4: 导入后数据被覆盖
**症状**: 导入成功,但之后添加的数据覆盖了导入的数据
**原因**: 可能是 localStorage 中的旧数据覆盖了服务器数据
**解决**:
1. 清除浏览器 localStorage
2. 重新加载页面
3. 数据会从服务器重新加载
## 🔍 调试命令
### 查看当前数据
```bash
# 服务器文件
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
# API 获取
curl http://localhost:3006/api/urls | python -m json.tool
# 容器内文件
docker exec workdizhi-web cat /app/data/urls.json
```
### 测试保存功能
```bash
# 测试保存
curl -X POST http://localhost:3006/api/urls \
-H "Content-Type: application/json" \
-d '{
"urls": [
{"id": "1", "title": "测试1", "url": "https://test1.com"},
{"id": "2", "title": "测试2", "url": "https://test2.com"}
],
"categories": []
}'
# 验证保存
curl http://localhost:3006/api/urls
```
### 查看日志
```bash
# 实时查看日志
docker logs -f workdizhi-web
# 查看最近 50 行
docker logs --tail 50 workdizhi-web
```
## ✅ 验证导入成功
导入数据后,执行以下验证:
1. **浏览器控制台**: 应该看到 "数据导入成功!已导入 X 个网址..."
2. **服务器文件**: `cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json` 应该包含所有导入的数据
3. **API 获取**: `curl http://localhost:3006/api/urls` 应该返回所有数据
4. **重启测试**: 重启容器后,数据应该还在
## 🎯 下一步
如果问题仍然存在,请:
1. 打开浏览器开发者工具F12
2. 切换到 Console 标签
3. 执行导入操作
4. 复制所有控制台输出
5. 检查 Network 标签中的 POST 请求
6. 查看请求和响应的详细信息
这些信息可以帮助进一步诊断问题。

View File

@@ -0,0 +1,115 @@
# Workdizhi 数据持久化问题分析
## 🔍 问题诊断
### 当前状态
1. **数据存储方式**: 浏览器的 `localStorage`
2. **Docker 配置**: 没有数据卷挂载
3. **问题表现**: 在新电脑打开后,添加的网址都消失了
### 根本原因
- `localStorage` 是浏览器本地存储,存储在用户的浏览器中
- 数据没有保存到服务器文件系统
- 容器中没有数据持久化配置
- 换电脑或清除浏览器数据时,数据会丢失
## ✅ 当前已有的功能
项目已经实现了导出/导入功能:
- ✅ 导出按钮:可以将数据导出为 JSON 文件
- ✅ 导入按钮:可以从 JSON 文件导入数据
- ✅ 快捷键Ctrl+S 快速导出
## 🔧 解决方案
### 方案 1: 使用导出/导入功能(临时方案)
**优点**
- 无需修改代码
- 立即可用
- 用户可以手动备份
**使用方法**
1. 定期点击"导出"按钮,保存 JSON 文件
2. 在新电脑上,点击"导入"按钮,选择之前导出的 JSON 文件
3. 数据即可恢复
**缺点**
- 需要手动操作
- 容易忘记备份
### 方案 2: 添加服务器端存储(推荐)
需要修改代码,添加后端 API 来保存数据到服务器文件系统。
#### 实施步骤
1. **创建简单的后端服务**Node.js + Express
2. **修改前端代码**,使用 API 保存和加载数据
3. **添加数据卷挂载**到 docker-compose.yml
4. **重新构建和部署**
详细方案请参考:`workdizhi数据持久化修复方案.md`
### 方案 3: 使用浏览器同步(如果支持)
如果浏览器支持同步功能(如 Chrome 同步),可以:
- 启用浏览器同步
- localStorage 数据会同步到云端
- 在新电脑登录同一账号即可恢复
**缺点**
- 依赖浏览器服务
- 可能不适用于所有浏览器
## 📋 当前 Docker 配置
```yaml
version: '3.8'
services:
web:
build: .
container_name: workdizhi-web
ports:
- "3006:80"
restart: unless-stopped
networks:
- workdizhi-network
# ❌ 缺少 volumes 配置
```
## 🔨 快速修复建议
### 立即行动
1. **使用导出功能备份当前数据**
- 打开网站http://101.43.95.130:3006
- 点击"导出"按钮
- 保存 JSON 文件到安全位置
2. **在新电脑上导入数据**
- 打开网站
- 点击"导入"按钮
- 选择之前导出的 JSON 文件
### 长期方案
实施方案 2添加服务器端存储实现真正的数据持久化。
## 📊 数据位置对比
| 存储方式 | 当前 | 修复后 |
|---------|------|--------|
| **浏览器 localStorage** | ✅ 使用中 | ⚠️ 降级使用API 失败时) |
| **服务器文件** | ❌ 无 | ✅ `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json` |
| **Docker 卷** | ❌ 无 | ✅ 挂载到容器 |
## ⚠️ 注意事项
1. **立即备份**: 在修改前,先导出当前所有数据
2. **测试**: 修改后充分测试数据保存和加载
3. **迁移**: 如果实施服务器端存储,需要提供数据迁移方案

214
部署说明.md Normal file
View File

@@ -0,0 +1,214 @@
# Workdizhi 服务器端存储部署说明
## ✅ 部署完成
服务器端数据持久化已成功实施!
## 📋 部署内容
### 新增文件
1. **server.js** - Node.js Express 后端服务器
2. **package.json** - Node.js 依赖配置
3. **Dockerfile** - 更新为 Node.js 镜像
4. **docker-compose.yml** - 添加数据卷挂载
### 修改文件
1. **script.js** - 修改数据加载和保存方法,支持服务器 API
## 🔧 技术架构
### 后端
- **框架**: Express.js
- **端口**: 3000容器内
- **数据存储**: `/app/data/urls.json`
- **API 端点**:
- `GET /api/urls` - 获取数据
- `POST /api/urls` - 保存数据
- `GET /api/health` - 健康检查
### 前端
- **数据加载**: 优先从服务器加载,失败时降级到 localStorage
- **数据保存**: 优先保存到服务器,失败时降级到 localStorage
- **兼容性**: 完全向后兼容,支持旧数据迁移
### 数据持久化
- **服务器端**: `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
- **Docker 卷**: 挂载到容器的 `/app/data`
- **备份**: localStorage 作为备用存储
## 🚀 使用方法
### 访问应用
- **Web 界面**: http://101.43.95.130:3006
- **API 端点**: http://101.43.95.130:3006/api/urls
### 数据管理
1. **自动保存**: 添加、编辑、删除网址时自动保存到服务器
2. **自动加载**: 打开页面时自动从服务器加载数据
3. **数据迁移**: 如果 localStorage 中有旧数据,会自动迁移到服务器
### 备份和恢复
数据文件位置:`/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
```bash
# 备份数据
cp /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json /path/to/backup/
# 恢复数据
cp /path/to/backup/urls.json /home/renjianbo/devops/workdizhi/workdizhi/data/
```
## 📊 数据存储位置
| 位置 | 路径 | 说明 |
|------|------|------|
| **服务器文件** | `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json` | 主数据存储 |
| **容器内** | `/app/data/urls.json` | 容器内的数据文件 |
| **浏览器** | localStorage | 备用存储(降级使用) |
## 🔄 服务管理
### 启动服务
```bash
cd /home/renjianbo/devops/workdizhi/workdizhi
docker-compose up -d
```
### 停止服务
```bash
cd /home/renjianbo/devops/workdizhi/workdizhi
docker-compose down
```
### 重启服务
```bash
cd /home/renjianbo/devops/workdizhi/workdizhi
docker-compose restart
```
### 查看日志
```bash
docker logs -f workdizhi-web
```
### 重新构建
```bash
cd /home/renjianbo/devops/workdizhi/workdizhi
docker-compose build
docker-compose up -d
```
## ✅ 验证部署
### 1. 检查服务状态
```bash
docker ps | grep workdizhi
# 应该显示容器运行中
```
### 2. 测试 API
```bash
# 健康检查
curl http://localhost:3006/api/health
# 获取数据
curl http://localhost:3006/api/urls
# 保存数据
curl -X POST http://localhost:3006/api/urls \
-H "Content-Type: application/json" \
-d '{"urls":[],"categories":[]}'
```
### 3. 检查数据文件
```bash
ls -la /home/renjianbo/devops/workdizhi/workdizhi/data/
cat /home/renjianbo/devops/workdizhi/workdizhi/data/urls.json
```
## 🔍 故障排查
### 问题 1: 数据未保存
**检查**:
1. 查看容器日志:`docker logs workdizhi-web`
2. 检查数据目录权限:`ls -la /home/renjianbo/devops/workdizhi/workdizhi/data/`
3. 检查 API 响应:浏览器开发者工具 Network 标签
**解决**:
```bash
# 修复权限
chmod 755 /home/renjianbo/devops/workdizhi/workdizhi/data
chown -R renjianbo:renjianbo /home/renjianbo/devops/workdizhi/workdizhi/data
```
### 问题 2: 无法访问
**检查**:
1. 端口是否监听:`netstat -tlnp | grep 3006`
2. 防火墙是否开放:`sudo firewall-cmd --list-ports`
**解决**:
```bash
# 开放端口
sudo firewall-cmd --permanent --add-port=3006/tcp
sudo firewall-cmd --reload
```
### 问题 3: 数据丢失
**恢复**:
1. 检查数据文件是否存在
2. 如果有备份,恢复备份文件
3. 如果没有备份,检查 localStorage浏览器开发者工具
## 📝 数据迁移
### 从 localStorage 迁移到服务器
1. 打开网站http://101.43.95.130:3006
2. 如果 localStorage 中有数据,会自动加载
3. 进行任何操作(添加、编辑、删除)会自动保存到服务器
4. 数据迁移完成
### 从导出文件迁移
1. 使用导入功能导入 JSON 文件
2. 数据会自动保存到服务器
## 🎯 优势
**数据持久化**: 数据保存在服务器,不会因换电脑而丢失
**多设备同步**: 所有设备访问同一服务器,数据同步
**自动备份**: 数据文件可以定期备份
**向后兼容**: 支持 localStorage 降级,确保兼容性
**易于维护**: 数据文件位置明确,易于备份和恢复
## 📌 注意事项
1. **定期备份**: 建议定期备份 `/home/renjianbo/devops/workdizhi/workdizhi/data/urls.json`
2. **权限管理**: 确保数据目录有正确的读写权限
3. **磁盘空间**: 监控数据文件大小,避免占用过多空间
4. **安全**: 如果暴露在公网,考虑添加认证机制
## 🔗 相关文件
- **后端服务**: `server.js`
- **前端代码**: `script.js`
- **Docker 配置**: `docker-compose.yml`
- **数据文件**: `data/urls.json`