From cd8b30e0d4be202073b837b2bd57ea67d3fd1fa3 Mon Sep 17 00:00:00 2001 From: rjb <263303411@qq.com> Date: Tue, 23 Dec 2025 11:09:02 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=BD=91=E5=9D=80=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=9C=8D=E5=8A=A1=E5=99=A8=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 8 + Dockerfile | 26 +++ README.md | 71 ++++++++ TROUBLESHOOTING.md | 184 ++++++++++++++++++++ data/urls.json | 287 +++++++++++++++++++++++++++++++ docker-compose.yml | 21 +++ docker-compose.yml.fix | 21 +++ package.json | 21 +++ script.js | 117 ++++++++++--- server.js | 86 ++++++++++ workdizhi数据持久化修复方案.md | 297 +++++++++++++++++++++++++++++++++ 导入功能说明.md | 106 ++++++++++++ 导入问题排查指南.md | 201 ++++++++++++++++++++++ 数据持久化问题分析.md | 115 +++++++++++++ 部署说明.md | 214 ++++++++++++++++++++++++ 15 files changed, 1755 insertions(+), 20 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 TROUBLESHOOTING.md create mode 100644 data/urls.json create mode 100644 docker-compose.yml create mode 100644 docker-compose.yml.fix create mode 100644 package.json create mode 100644 server.js create mode 100644 workdizhi数据持久化修复方案.md create mode 100644 导入功能说明.md create mode 100644 导入问题排查指南.md create mode 100644 数据持久化问题分析.md create mode 100644 部署说明.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a47e915 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +# 忽略不需要的文件 +Dockerfile +docker-compose.yml +.dockerignore +README.md +.git +.gitignore + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e2f89b4 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..23b0692 --- /dev/null +++ b/README.md @@ -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中,清除浏览器数据会丢失 +- 建议定期使用导出功能备份数据 +- 如需多用户或云端同步,需要后端支持 + diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..b833280 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -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正常启动 + diff --git a/data/urls.json b/data/urls.json new file mode 100644 index 0000000..465c4f5 --- /dev/null +++ b/data/urls.json @@ -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" +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..11373f5 --- /dev/null +++ b/docker-compose.yml @@ -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 + diff --git a/docker-compose.yml.fix b/docker-compose.yml.fix new file mode 100644 index 0000000..f107a81 --- /dev/null +++ b/docker-compose.yml.fix @@ -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 + diff --git a/package.json b/package.json new file mode 100644 index 0000000..e2830ec --- /dev/null +++ b/package.json @@ -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" +} + diff --git a/script.js b/script.js index cd09351..545fe32 100644 --- a/script.js +++ b/script.js @@ -9,20 +9,35 @@ class UrlManager { this.currentTag = ''; this.viewMode = 'grid'; this.editingUrlId = null; - - this.init(); } - init() { - this.loadData(); + async init() { + await this.loadData(); this.setupEventListeners(); this.render(); this.setupKeyboardShortcuts(); } // 数据存储 - loadData() { - // 加载默认数据或从localStorage加载 + async loadData() { + 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 savedCategories = localStorage.getItem('urlManager_categories'); @@ -40,15 +55,49 @@ class UrlManager { } } - saveData() { + async saveData() { try { - localStorage.setItem('urlManager_urls', JSON.stringify(this.urls)); - localStorage.setItem('urlManager_categories', JSON.stringify(this.categories)); + const dataToSave = { + 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) { - console.error('保存数据到localStorage失败:', error); - // 如果localStorage已满或其他错误,只记录错误,不抛出异常 - // 因为数据已经在内存中了,只是无法持久化 - // 抛出异常会导致误报"保存失败" + console.error('保存到服务器失败:', error); + // 降级到 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; const reader = new FileReader(); - reader.onload = (e) => { + reader.onload = async (e) => { try { const data = JSON.parse(e.target.result); + console.log('导入数据:', data); if (confirm('导入数据将覆盖当前的所有数据,确定要继续吗?')) { - if (data.urls) this.urls = data.urls; - if (data.categories) this.categories = data.categories; - this.saveData(); + // 更新数据 + if (data.urls) { + 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.closeAllModals(); - alert('数据导入成功!'); + alert(`数据导入成功!已导入 ${this.urls.length} 个网址,${this.categories.length} 个分类,并已保存到服务器!`); } } catch (error) { - alert('文件格式错误,请选择有效的JSON文件'); + console.error('导入数据失败:', error); + alert('文件格式错误或保存失败: ' + error.message); } }; reader.readAsText(file); @@ -1019,6 +1095,7 @@ class UrlManager { } // 初始化应用 -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener('DOMContentLoaded', async () => { window.urlManager = new UrlManager(); + await window.urlManager.init(); }); diff --git a/server.js b/server.js new file mode 100644 index 0000000..3dff74d --- /dev/null +++ b/server.js @@ -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}`); +}); + diff --git a/workdizhi数据持久化修复方案.md b/workdizhi数据持久化修复方案.md new file mode 100644 index 0000000..2a5a41e --- /dev/null +++ b/workdizhi数据持久化修复方案.md @@ -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. **迁移**: 如果已有数据,需要提供迁移脚本 + diff --git a/导入功能说明.md b/导入功能说明.md new file mode 100644 index 0000000..c6efe5b --- /dev/null +++ b/导入功能说明.md @@ -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. **重启测试**: 重启容器验证数据是否持久化 + diff --git a/导入问题排查指南.md b/导入问题排查指南.md new file mode 100644 index 0000000..4f522c9 --- /dev/null +++ b/导入问题排查指南.md @@ -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. 查看请求和响应的详细信息 + +这些信息可以帮助进一步诊断问题。 + diff --git a/数据持久化问题分析.md b/数据持久化问题分析.md new file mode 100644 index 0000000..4349bab --- /dev/null +++ b/数据持久化问题分析.md @@ -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. **迁移**: 如果实施服务器端存储,需要提供数据迁移方案 + diff --git a/部署说明.md b/部署说明.md new file mode 100644 index 0000000..69d61dd --- /dev/null +++ b/部署说明.md @@ -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` +