2025-12-2genxin
This commit is contained in:
27
.gitignore
vendored
27
.gitignore
vendored
@@ -1,27 +0,0 @@
|
||||
# ---> Go
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
|
||||
211
README.md
211
README.md
@@ -1,3 +1,210 @@
|
||||
# yixiaogao
|
||||
# 🔍 易搜高 - 微信公众号文章爬虫系统
|
||||
|
||||
易小稿网站微信公众号文章爬取
|
||||
一个功能完整的微信公众号文章爬虫与自媒体监控系统,支持文章采集、数据分析、用户管理等功能。
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
### 📱 自媒体监控系统
|
||||
- **文章监控**:实时追踪公众号、头条号、百家号等平台的最新文章
|
||||
- **数据分析**:统计阅读量、点赞数、评论数、分享数等关键指标
|
||||
- **历史文章**:浏览和管理已采集的文章列表
|
||||
- **文章详情**:查看文章完整内容、统计数据和元信息
|
||||
- **用户中心**:个人信息管理、收藏夹、浏览历史
|
||||
|
||||
### 🚀 爬虫系统
|
||||
- **提取公众号主页**:从文章链接获取公众号主页
|
||||
- **获取文章列表**:批量获取公众号所有历史文章
|
||||
- **批量下载文章**:下载文章内容和图片资源
|
||||
- **获取文章详情**:获取阅读量、点赞数、评论等详细数据
|
||||
|
||||
### 👥 用户管理
|
||||
- **用户注册/登录**:完整的用户认证系统
|
||||
- **登录状态同步**:所有页面统一的登录状态管理
|
||||
- **安全认证**:基于 Token 的安全认证机制
|
||||
|
||||
## 🏗️ 项目架构
|
||||
|
||||
```
|
||||
Access_wechat_article/
|
||||
├── backend/ # 后端服务(Go)
|
||||
│ ├── api/ # API 接口
|
||||
│ ├── cmd/ # 命令行工具
|
||||
│ ├── configs/ # 配置文件
|
||||
│ ├── pkg/ # 核心包
|
||||
│ └── tools/ # 工具函数
|
||||
├── frontend/ # 前端页面(HTML/CSS/JS)
|
||||
│ ├── css/ # 样式文件
|
||||
│ ├── js/ # JavaScript 文件
|
||||
│ ├── frontend.html # 自媒体监控首页
|
||||
│ ├── index.html # 爬虫系统页面
|
||||
│ ├── login.html # 登录页面
|
||||
│ ├── register.html # 注册页面
|
||||
│ ├── user-center.html # 用户中心
|
||||
│ ├── history-articles.html # 历史文章列表
|
||||
│ └── article-detail.html # 文章详情页
|
||||
├── database/ # 数据库文件
|
||||
└── data/ # 数据存储目录
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- **Go**: 1.24.0 或更高版本
|
||||
- **Python**: 3.x(用于前端服务器)
|
||||
- **浏览器**: Chrome、Firefox、Edge 等现代浏览器
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. **克隆项目**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd Access_wechat_article
|
||||
```
|
||||
|
||||
2. **安装后端依赖**
|
||||
```bash
|
||||
cd backend
|
||||
go mod download
|
||||
```
|
||||
|
||||
3. **启动系统**
|
||||
|
||||
**方式一:使用启动脚本(推荐)**
|
||||
```bash
|
||||
# Windows 系统
|
||||
双击运行 "启动Web系统.bat"
|
||||
```
|
||||
|
||||
**方式二:手动启动**
|
||||
```bash
|
||||
# 启动 API 服务器
|
||||
cd backend/api
|
||||
go run start_api.go
|
||||
|
||||
# 新开终端,启动前端服务器
|
||||
cd frontend
|
||||
python -m http.server 8000
|
||||
```
|
||||
|
||||
4. **访问系统**
|
||||
- 前端界面:http://localhost:8000
|
||||
- API 服务器:http://localhost:8080
|
||||
|
||||
## 📖 使用说明
|
||||
|
||||
### 1. 自媒体监控系统
|
||||
|
||||
访问 `http://localhost:8000/frontend.html`
|
||||
|
||||
- **首页**:查看最新文章列表和热门内容
|
||||
- **历史文章**:浏览和搜索已采集的文章
|
||||
- **文章详情**:查看文章完整内容和数据分析
|
||||
- **用户中心**:管理个人信息和收藏
|
||||
|
||||
### 2. 爬虫系统
|
||||
|
||||
访问 `http://localhost:8000/index.html`
|
||||
|
||||
#### 提取公众号主页
|
||||
1. 输入公众号下任意一篇文章链接
|
||||
2. 点击"提取主页链接"
|
||||
3. 获取公众号主页 URL
|
||||
|
||||
#### 获取文章列表
|
||||
1. 使用 Fiddler 抓取包含认证信息的 URL
|
||||
2. 粘贴完整的 Access Token URL
|
||||
3. 设置获取页数(留空表示全部)
|
||||
4. 点击"开始获取"
|
||||
|
||||
#### 批量下载文章
|
||||
1. 输入公众号名称或文章链接
|
||||
2. 选择是否保存图片和内容
|
||||
3. 点击"开始批量下载"
|
||||
|
||||
#### 获取文章详情
|
||||
1. 粘贴 Access Token URL
|
||||
2. 设置获取页数
|
||||
3. 点击"开始获取"
|
||||
4. 数据保存在 `data/公众号名称/文章详细/` 目录
|
||||
|
||||
### 3. 用户系统
|
||||
|
||||
#### 注册账号
|
||||
1. 访问注册页面:`http://localhost:8000/register.html`
|
||||
2. 填写用户名、邮箱、密码
|
||||
3. 点击"立即注册"
|
||||
|
||||
#### 登录
|
||||
1. 访问登录页面:`http://localhost:8000/login.html`
|
||||
2. 输入用户名和密码
|
||||
3. 选择"记住我"可保持登录状态
|
||||
4. 登录成功后自动跳转到首页
|
||||
|
||||
## 🛠️ 技术栈
|
||||
|
||||
### 后端
|
||||
- **语言**: Go 1.24.0
|
||||
- **HTTP 客户端**: go-resty/resty v2.17.0
|
||||
- **数据库**: SQLite (modernc.org/sqlite v1.40.1)
|
||||
|
||||
### 前端
|
||||
- **HTML5/CSS3**: 响应式布局
|
||||
- **JavaScript**: 原生 JS + jQuery 3.6.0
|
||||
- **UI 设计**: 渐变配色、卡片式布局、动画效果
|
||||
|
||||
## 📊 API 接口
|
||||
|
||||
### 用户相关
|
||||
- `POST /api/user/register` - 用户注册
|
||||
- `POST /api/user/login` - 用户登录
|
||||
- `POST /api/user/logout` - 用户登出
|
||||
- `GET /api/user/info` - 获取用户信息
|
||||
|
||||
### 文章相关
|
||||
- `GET /api/articles` - 获取文章列表
|
||||
- `GET /api/articles/:id` - 获取文章详情
|
||||
- `POST /api/articles/collect` - 收藏文章
|
||||
|
||||
### 爬虫相关
|
||||
- `POST /api/crawler/homepage` - 提取公众号主页
|
||||
- `POST /api/crawler/articles` - 获取文章列表
|
||||
- `POST /api/crawler/batch` - 批量下载文章
|
||||
- `POST /api/crawler/detail` - 获取文章详情
|
||||
|
||||
## 📝 注意事项
|
||||
|
||||
1. **网络要求**:需要稳定的网络连接访问微信公众号平台
|
||||
2. **认证信息**:部分功能需要有效的 Access Token
|
||||
3. **数据存储**:采集的数据保存在 `data/` 目录下
|
||||
4. **浏览器兼容**:建议使用 Chrome、Firefox、Edge 等现代浏览器
|
||||
5. **服务器端口**:确保 8000 和 8080 端口未被占用
|
||||
|
||||
## 🔒 安全说明
|
||||
|
||||
- 用户密码经过加密存储
|
||||
- 使用 Token 认证机制保护 API
|
||||
- 所有敏感操作需要登录验证
|
||||
- 支持会话管理和自动过期
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目仅供学习研究使用,请勿用于商业用途。
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 📧 联系方式
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
- 提交 Issue
|
||||
- 发送邮件
|
||||
|
||||
---
|
||||
|
||||
**⚠️ 免责声明**:本工具仅供学习交流使用,使用者需遵守相关法律法规和平台规则,不得用于非法用途。使用本工具产生的任何后果由使用者自行承担。
|
||||
|
||||
@@ -1,460 +0,0 @@
|
||||
# 📡 微信公众号文章爬虫 - API 接口文档
|
||||
|
||||
## 服务器信息
|
||||
|
||||
- **服务地址**: http://localhost:8080
|
||||
- **协议**: HTTP/1.1
|
||||
- **数据格式**: JSON
|
||||
- **字符编码**: UTF-8
|
||||
- **CORS**: 已启用(允许所有来源)
|
||||
|
||||
## 统一响应格式
|
||||
|
||||
所有API接口返回格式统一为:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true, // 请求是否成功
|
||||
"message": "操作成功", // 提示信息
|
||||
"data": {} // 数据内容(可选)
|
||||
}
|
||||
```
|
||||
|
||||
## 接口列表
|
||||
|
||||
### 1. 提取公众号主页
|
||||
|
||||
**接口地址**: `/api/homepage/extract`
|
||||
**请求方法**: POST
|
||||
**功能说明**: 从文章链接中提取公众号主页链接
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://mp.weixin.qq.com/s?__biz=xxx&mid=xxx"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| url | string | 是 | 公众号文章链接 |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功响应**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "提取成功",
|
||||
"data": {
|
||||
"homepage": "https://mp.weixin.qq.com/mp/profile_ext?action=home&__biz=xxx&scene=124",
|
||||
"output": "完整的命令行输出信息"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应**:
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "未能提取到主页链接"
|
||||
}
|
||||
```
|
||||
|
||||
#### 调用示例
|
||||
|
||||
**jQuery**:
|
||||
```javascript
|
||||
$.ajax({
|
||||
url: 'http://localhost:8080/api/homepage/extract',
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
url: 'https://mp.weixin.qq.com/s?__biz=xxx&mid=xxx'
|
||||
}),
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
console.log('主页链接:', response.data.homepage);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**curl**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/homepage/extract \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"url":"https://mp.weixin.qq.com/s?__biz=xxx&mid=xxx"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. 下载单篇文章
|
||||
|
||||
**接口地址**: `/api/article/download`
|
||||
**请求方法**: POST
|
||||
**功能说明**: 下载指定的单篇文章
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://mp.weixin.qq.com/s?__biz=xxx",
|
||||
"save_image": true,
|
||||
"save_content": true
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| url | string | 是 | 文章链接 |
|
||||
| save_image | boolean | 否 | 是否保存图片(默认false) |
|
||||
| save_content | boolean | 否 | 是否保存内容(默认true) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "下载任务已启动",
|
||||
"data": {
|
||||
"url": "https://mp.weixin.qq.com/s?__biz=xxx"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 获取文章列表
|
||||
|
||||
**接口地址**: `/api/article/list`
|
||||
**请求方法**: POST
|
||||
**功能说明**: 批量获取公众号的文章列表
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "https://mp.weixin.qq.com/mp/profile_ext?action=xxx&appmsg_token=xxx",
|
||||
"pages": 0
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| access_token | string | 是 | 包含appmsg_token的URL |
|
||||
| pages | integer | 否 | 获取页数,0表示全部(默认0) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "任务已启动"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. 批量下载文章
|
||||
|
||||
**接口地址**: `/api/article/batch`
|
||||
**请求方法**: POST
|
||||
**功能说明**: 批量下载公众号的所有文章
|
||||
|
||||
#### 请求参数
|
||||
|
||||
```json
|
||||
{
|
||||
"official_account": "公众号名称或文章链接",
|
||||
"save_image": true,
|
||||
"save_content": true
|
||||
}
|
||||
```
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| official_account | string | 是 | 公众号名称或任意文章链接 |
|
||||
| save_image | boolean | 否 | 是否保存图片(默认false) |
|
||||
| save_content | boolean | 否 | 是否保存内容(默认true) |
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "任务已启动"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 获取数据列表
|
||||
|
||||
**接口地址**: `/api/data/list`
|
||||
**请求方法**: GET
|
||||
**功能说明**: 获取已下载的公众号数据列表
|
||||
|
||||
#### 请求参数
|
||||
|
||||
无
|
||||
|
||||
#### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": [
|
||||
{
|
||||
"name": "研招网资讯",
|
||||
"article_count": 125,
|
||||
"path": "D:\\workspace\\Access_wechat_article\\backend\\data\\研招网资讯",
|
||||
"last_update": "2025-11-27"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| name | string | 公众号名称 |
|
||||
| article_count | integer | 文章数量 |
|
||||
| path | string | 存储路径 |
|
||||
| last_update | string | 最后更新时间 |
|
||||
|
||||
#### 调用示例
|
||||
|
||||
**jQuery**:
|
||||
```javascript
|
||||
$.get('http://localhost:8080/api/data/list', function(response) {
|
||||
if (response.success) {
|
||||
console.log('数据列表:', response.data);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**curl**:
|
||||
```bash
|
||||
curl http://localhost:8080/api/data/list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. 获取任务状态
|
||||
|
||||
**接口地址**: `/api/task/status`
|
||||
**请求方法**: GET
|
||||
**功能说明**: 获取当前任务的执行状态
|
||||
|
||||
#### 请求参数
|
||||
|
||||
无
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**任务运行中**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"running": true,
|
||||
"progress": 45,
|
||||
"message": "正在下载第10篇文章..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**无任务运行**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"running": false,
|
||||
"progress": 0,
|
||||
"message": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| running | boolean | 是否有任务运行中 |
|
||||
| progress | integer | 任务进度(0-100) |
|
||||
| message | string | 任务状态描述 |
|
||||
| error | string | 错误信息(可选) |
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
### HTTP状态码
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 请求成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
### 业务错误码
|
||||
|
||||
所有业务错误通过响应中的 `success` 字段和 `message` 字段返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "具体的错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
常见错误信息:
|
||||
|
||||
| 错误信息 | 说明 | 解决方法 |
|
||||
|----------|------|----------|
|
||||
| 请求参数错误 | JSON格式不正确或缺少必填参数 | 检查请求参数格式 |
|
||||
| 执行失败 | 后端程序执行出错 | 查看详细错误信息 |
|
||||
| 未能提取到主页链接 | 文章链接格式错误或解析失败 | 使用有效的文章链接 |
|
||||
| 读取数据目录失败 | data目录不存在或无权限 | 检查目录权限 |
|
||||
|
||||
---
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 本地测试
|
||||
|
||||
1. **启动API服务器**:
|
||||
```bash
|
||||
cd backend\api
|
||||
start_api.bat
|
||||
```
|
||||
|
||||
2. **测试接口**:
|
||||
```bash
|
||||
# 测试提取主页
|
||||
curl -X POST http://localhost:8080/api/homepage/extract \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"url\":\"文章链接\"}"
|
||||
|
||||
# 测试获取数据列表
|
||||
curl http://localhost:8080/api/data/list
|
||||
```
|
||||
|
||||
### 跨域配置
|
||||
|
||||
API服务器已启用CORS,允许所有来源访问:
|
||||
|
||||
```go
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
```
|
||||
|
||||
如需限制特定域名,修改 `server.go` 中的 `corsMiddleware` 函数。
|
||||
|
||||
### 超时设置
|
||||
|
||||
默认HTTP超时时间:30秒
|
||||
|
||||
如需修改,在 `server.go` 中添加:
|
||||
|
||||
```go
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
}
|
||||
```
|
||||
|
||||
### 日志记录
|
||||
|
||||
API服务器使用标准输出记录日志:
|
||||
|
||||
```go
|
||||
log.Printf("[%s] %s - %s", r.Method, r.URL.Path, message)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 接口更新计划
|
||||
|
||||
### v1.1.0(计划中)
|
||||
- [ ] 添加用户认证机制
|
||||
- [ ] 支持任务队列管理
|
||||
- [ ] 增加下载进度推送(WebSocket)
|
||||
- [ ] 提供文章搜索接口
|
||||
|
||||
### v1.2.0(计划中)
|
||||
- [ ] 数据统计分析接口
|
||||
- [ ] 导出功能(PDF/Word)
|
||||
- [ ] 批量任务管理
|
||||
- [ ] 定时任务支持
|
||||
|
||||
---
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **语言**: Go 1.20+
|
||||
- **Web框架**: net/http (标准库)
|
||||
- **数据格式**: JSON
|
||||
- **并发模型**: Goroutine
|
||||
|
||||
---
|
||||
|
||||
## 性能说明
|
||||
|
||||
### 并发能力
|
||||
- 支持多客户端同时访问
|
||||
- 但同一时间只能执行一个爬虫任务(`currentTask`)
|
||||
|
||||
### 资源占用
|
||||
- CPU: 低(主要I/O操作)
|
||||
- 内存: <50MB
|
||||
- 磁盘: 取决于下载的文章数量
|
||||
|
||||
### 性能优化建议
|
||||
1. 使用连接池管理HTTP请求
|
||||
2. 实现任务队列机制
|
||||
3. 添加结果缓存
|
||||
4. 启用gzip压缩
|
||||
|
||||
---
|
||||
|
||||
## 安全建议
|
||||
|
||||
### 1. 生产环境部署
|
||||
- 添加HTTPS支持
|
||||
- 实现API认证(JWT/OAuth)
|
||||
- 限制跨域来源
|
||||
- 添加请求频率限制
|
||||
|
||||
### 2. 数据安全
|
||||
- 不要暴露敏感信息(Cookie)
|
||||
- 定期清理临时文件
|
||||
- 备份重要数据
|
||||
|
||||
### 3. 访问控制
|
||||
- 添加IP白名单
|
||||
- 实现用户权限管理
|
||||
- 记录操作日志
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 为什么任务启动后没有响应?
|
||||
A: 检查后端 `wechat-crawler.exe` 是否存在并有执行权限。
|
||||
|
||||
### Q2: 如何查看详细的错误信息?
|
||||
A: 查看API服务器窗口的控制台输出。
|
||||
|
||||
### Q3: 能同时执行多个下载任务吗?
|
||||
A: 当前版本不支持,同时只能执行一个任务。
|
||||
|
||||
### Q4: 如何停止正在运行的任务?
|
||||
A: 关闭API服务器窗口或重启服务器。
|
||||
|
||||
---
|
||||
|
||||
**文档版本**: v1.0.0
|
||||
**最后更新**: 2025-11-27
|
||||
**维护者**: AI Assistant
|
||||
BIN
backend/api/api-server.exe
Normal file
BIN
backend/api/api-server.exe
Normal file
Binary file not shown.
BIN
backend/api/api-server.exe~
Normal file
BIN
backend/api/api-server.exe~
Normal file
Binary file not shown.
@@ -5,8 +5,8 @@ echo 📦 编译 API 服务器
|
||||
echo ===============================================
|
||||
echo.
|
||||
|
||||
echo 🔨 正在编译 api_server.exe...
|
||||
go build -o api_server.exe server.go
|
||||
echo 🔨 正在编译 api-server.exe...
|
||||
go build -o api-server.exe server.go
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo.
|
||||
@@ -18,7 +18,7 @@ if %errorlevel% neq 0 (
|
||||
|
||||
echo.
|
||||
echo ✅ 编译成功!
|
||||
echo 📁 输出文件: api_server.exe
|
||||
echo 📁 输出文件: api-server.exe
|
||||
echo.
|
||||
echo ===============================================
|
||||
echo 编译完成
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,15 +1,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/wechat-crawler/pkg/wechat"
|
||||
)
|
||||
|
||||
// Response 统一响应结构
|
||||
@@ -17,6 +23,7 @@ type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Code int `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// 任务状态
|
||||
@@ -27,7 +34,28 @@ type TaskStatus struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// 用户登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// 用户注册请求
|
||||
type RegisterRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// Session存储
|
||||
type Session struct {
|
||||
Token string
|
||||
UserID int
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
var currentTask = &TaskStatus{Running: false}
|
||||
var sessions = make(map[string]*Session)
|
||||
|
||||
func main() {
|
||||
// 启用CORS
|
||||
@@ -36,10 +64,18 @@ func main() {
|
||||
http.HandleFunc("/api/article/download", corsMiddleware(downloadArticleHandler))
|
||||
http.HandleFunc("/api/article/list", corsMiddleware(getArticleListHandler))
|
||||
http.HandleFunc("/api/article/batch", corsMiddleware(batchDownloadHandler))
|
||||
http.HandleFunc("/api/article/detail", corsMiddleware(getArticleDetailHandler))
|
||||
http.HandleFunc("/api/data/list", corsMiddleware(getDataListHandler))
|
||||
http.HandleFunc("/api/task/status", corsMiddleware(getTaskStatusHandler))
|
||||
http.HandleFunc("/api/download/", corsMiddleware(downloadFileHandler))
|
||||
|
||||
// 用户认证接口
|
||||
http.HandleFunc("/api/user/register", corsMiddleware(registerHandler))
|
||||
http.HandleFunc("/api/user/login", corsMiddleware(loginHandler))
|
||||
http.HandleFunc("/api/user/logout", corsMiddleware(logoutHandler))
|
||||
http.HandleFunc("/api/user/info", corsMiddleware(getUserInfoHandler))
|
||||
http.HandleFunc("/api/user/update", corsMiddleware(updateUserHandler))
|
||||
|
||||
port := ":8080"
|
||||
fmt.Println("===============================================")
|
||||
fmt.Println(" 🚀 微信公众号文章爬虫 API 服务器")
|
||||
@@ -58,7 +94,7 @@ func corsMiddleware(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -98,6 +134,9 @@ func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
<div class="endpoint">
|
||||
<span class="method">POST</span> /api/article/list - 获取文章列表
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="method">POST</span> /api/article/detail - 获取文章详情(阅读量、点赞数、评论等)
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<span class="method">POST</span> /api/article/batch - 批量下载文章
|
||||
</div>
|
||||
@@ -216,12 +255,12 @@ func getArticleListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
currentTask.Progress = 0
|
||||
currentTask.Message = "正在获取文章列表..."
|
||||
|
||||
// 同步执行爬虫程序(功能3)
|
||||
// 同步执行爬虫程序(功能2:获取文章列表)
|
||||
exePath := filepath.Join("..", "wechat-crawler.exe")
|
||||
absPath, _ := filepath.Abs(exePath)
|
||||
workDir, _ := filepath.Abs("..")
|
||||
|
||||
log.Printf("启动功能3: %s, 工作目录: %s", absPath, workDir)
|
||||
log.Printf("启动功能2: %s, 工作目录: %s", absPath, workDir)
|
||||
cmd := exec.Command(absPath)
|
||||
cmd.Dir = workDir
|
||||
|
||||
@@ -242,8 +281,8 @@ func getArticleListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// 发送选项"3"(功能3:通过access_token获取文章列表)
|
||||
fmt.Fprintln(stdin, "3")
|
||||
// 发送选项"2"(功能2:通过access_token获取文章列表)
|
||||
fmt.Fprintln(stdin, "2")
|
||||
fmt.Fprintln(stdin, req.AccessToken)
|
||||
if req.Pages > 0 {
|
||||
fmt.Fprintf(stdin, "%d\n", req.Pages)
|
||||
@@ -445,6 +484,304 @@ func batchDownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取文章详情(功能4:包括阅读量、点赞数、评论等)
|
||||
func getArticleDetailHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
log.Printf("❌ 解析请求失败: %v", err)
|
||||
writeJSON(w, Response{Success: false, Message: "请求参数错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.AccessToken == "" {
|
||||
log.Printf("❌ Access Token 为空")
|
||||
writeJSON(w, Response{Success: false, Message: "请输入Access Token URL"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("\n" + strings.Repeat("=", 60))
|
||||
log.Printf("📊 开始获取文章详情功能")
|
||||
log.Printf("接收到的 Access Token: %s", req.AccessToken[:min(100, len(req.AccessToken))])
|
||||
log.Printf("获取页数: %d (0表示全部)", req.Pages)
|
||||
|
||||
currentTask.Running = true
|
||||
currentTask.Progress = 0
|
||||
currentTask.Message = "正在解析Access Token参数..."
|
||||
|
||||
// 从Access Token URL中提取参数
|
||||
params, err := parseAccessToken(req.AccessToken)
|
||||
if err != nil {
|
||||
log.Printf("❌ 解析Access Token失败: %v", err)
|
||||
currentTask.Running = false
|
||||
writeJSON(w, Response{Success: false, Message: "Access Token 参数格式错误: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ 参数解析成功:")
|
||||
log.Printf(" - biz: %s", params["biz"][:min(20, len(params["biz"]))])
|
||||
log.Printf(" - uin: %s", params["uin"])
|
||||
log.Printf(" - key: %s", params["key"][:min(20, len(params["key"]))])
|
||||
log.Printf(" - pass_ticket: %s", params["pass_ticket"][:min(20, len(params["pass_ticket"]))])
|
||||
|
||||
// 创建爬虫实例
|
||||
log.Printf("🔧 创建爬虫实例...")
|
||||
crawler, err := wechat.NewWechatCrawler(
|
||||
params["biz"],
|
||||
params["uin"],
|
||||
params["key"],
|
||||
params["pass_ticket"],
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("❌ 创建爬虫实例失败: %v", err)
|
||||
currentTask.Running = false
|
||||
writeJSON(w, Response{Success: false, Message: "创建爬虫实例失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 爬虫实例创建成功")
|
||||
|
||||
currentTask.Progress = 20
|
||||
currentTask.Message = "正在获取公众号名称..."
|
||||
|
||||
// 获取公众号名称
|
||||
log.Printf("📱 获取公众号名称...")
|
||||
officialName, err := crawler.GetOfficialAccountName()
|
||||
if err != nil {
|
||||
log.Printf("❌ 获取公众号名称失败: %v", err)
|
||||
currentTask.Running = false
|
||||
writeJSON(w, Response{Success: false, Message: "获取公众号名称失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("✅ 公众号名称: %s", officialName)
|
||||
|
||||
currentTask.Progress = 40
|
||||
currentTask.Message = "正在获取文章列表..."
|
||||
|
||||
// 获取文章列表
|
||||
log.Printf("📋 获取文章列表...")
|
||||
var articleList [][]string
|
||||
|
||||
if req.Pages > 0 {
|
||||
// 只获取指定页数
|
||||
log.Printf("📄 限制获取前 %d 页", req.Pages)
|
||||
for offset := 0; offset < req.Pages; offset++ {
|
||||
result, e := crawler.GetNextList(offset)
|
||||
if e != nil {
|
||||
log.Printf("❌ 获取第 %d 页失败: %v", offset+1, e)
|
||||
err = e
|
||||
break
|
||||
}
|
||||
|
||||
// 检查是否有数据
|
||||
mFlag, ok := result["m_flag"].(int)
|
||||
if !ok {
|
||||
if mFlagFloat, ok := result["m_flag"].(float64); ok {
|
||||
mFlag = int(mFlagFloat)
|
||||
}
|
||||
}
|
||||
if mFlag == 0 {
|
||||
log.Printf("ℹ️ 第 %d 页无更多数据", offset+1)
|
||||
break
|
||||
}
|
||||
|
||||
// 获取当前页的文章列表
|
||||
log.Printf("📝 尝试从 result 中提取 passage_list...")
|
||||
|
||||
// 先尝试 [][]string 类型(GetNextList 实际返回的类型)
|
||||
if passageListStr, ok := result["passage_list"].([][]string); ok {
|
||||
log.Printf("✅ passage_list 提取成功([][]string),包含 %d 个元素", len(passageListStr))
|
||||
for idx, strArr := range passageListStr {
|
||||
articleList = append(articleList, strArr)
|
||||
log.Printf("✅ 添加第 %d 篇文章: %v", idx+1, strArr)
|
||||
}
|
||||
} else if passageList, ok := result["passage_list"].([]interface{}); ok {
|
||||
// 备用:尝试 []interface{} 类型
|
||||
log.Printf("✅ passage_list 提取成功([]interface{}),包含 %d 个元素", len(passageList))
|
||||
for idx, item := range passageList {
|
||||
if arr, ok := item.([]interface{}); ok {
|
||||
strArr := make([]string, len(arr))
|
||||
for i, v := range arr {
|
||||
if s, ok := v.(string); ok {
|
||||
strArr[i] = s
|
||||
}
|
||||
}
|
||||
articleList = append(articleList, strArr)
|
||||
log.Printf("✅ 添加第 %d 篇文章: %v", idx+1, strArr)
|
||||
} else {
|
||||
log.Printf("❌ 第 %d 个 item 不是 []interface{} 类型,实际类型: %T", idx+1, item)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("❌ passage_list 类型断言失败,实际类型: %T", result["passage_list"])
|
||||
}
|
||||
|
||||
log.Printf("✅ 已获取第 %d/%d 页,当前累计 %d 篇文章", offset+1, req.Pages, len(articleList))
|
||||
|
||||
// 添加延迟
|
||||
if offset < req.Pages-1 {
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// 转换链接
|
||||
log.Printf("🔗 转换文章链接...转换前共 %d 篇", len(articleList))
|
||||
articleList = crawler.TransformLinks(articleList)
|
||||
log.Printf("✅ 链接转换完成,共 %d 篇文章", len(articleList))
|
||||
} else {
|
||||
// 获取全部文章
|
||||
log.Printf("📄 获取全部文章")
|
||||
articleList, err = crawler.GetArticleList()
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("❌ 获取文章列表失败: %v", err)
|
||||
currentTask.Running = false
|
||||
writeJSON(w, Response{Success: false, Message: "获取文章列表失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if len(articleList) == 0 {
|
||||
log.Printf("⚠️ 文章列表为空")
|
||||
currentTask.Running = false
|
||||
writeJSON(w, Response{Success: false, Message: "公众号文章列表为空,可能是 Access Token 无效或公众号无文章"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ 获取到 %d 篇文章", len(articleList))
|
||||
|
||||
currentTask.Progress = 60
|
||||
currentTask.Message = fmt.Sprintf("正在获取文章详情 (0/%d)...", len(articleList))
|
||||
|
||||
// 创建保存目录
|
||||
dataDir := "../data"
|
||||
officialPath := filepath.Join(dataDir, officialName)
|
||||
log.Printf("📁 创建保存目录: %s", officialPath)
|
||||
if err := os.MkdirAll(officialPath, 0755); err != nil {
|
||||
log.Printf("❌ 创建保存目录失败: %v", err)
|
||||
currentTask.Running = false
|
||||
writeJSON(w, Response{Success: false, Message: "创建保存目录失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文章详情
|
||||
log.Printf("📊 开始获取文章详情数据...")
|
||||
err = crawler.GetDetailList(articleList, officialPath)
|
||||
if err != nil {
|
||||
log.Printf("❌ 获取文章详情失败: %v", err)
|
||||
currentTask.Running = false
|
||||
writeJSON(w, Response{Success: false, Message: "获取文章详情失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ 文章详情获取完成")
|
||||
|
||||
currentTask.Running = false
|
||||
currentTask.Progress = 100
|
||||
currentTask.Message = "文章详情获取完成"
|
||||
|
||||
// 统计文章详情文件数量
|
||||
detailPath := filepath.Join(officialPath, "文章详细")
|
||||
var detailFiles []string
|
||||
if entries, err := os.ReadDir(detailPath); err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), "_文章详情.txt") {
|
||||
detailFiles = append(detailFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(detailFiles) == 0 {
|
||||
// 检查主目录
|
||||
log.Printf("⚠️ 文章详细目录下未找到文件,检查主目录...")
|
||||
if entries, err := os.ReadDir(officialPath); err == nil {
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDir() && strings.HasSuffix(entry.Name(), "_文章详情.txt") {
|
||||
detailFiles = append(detailFiles, entry.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("✅ 找到 %d 个文章详情文件", len(detailFiles))
|
||||
log.Printf(strings.Repeat("=", 60) + "\n")
|
||||
|
||||
writeJSON(w, Response{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("文章详情获取成功,共 %d 篇文章", len(detailFiles)),
|
||||
Data: map[string]interface{}{
|
||||
"account": officialName,
|
||||
"articleCount": len(detailFiles),
|
||||
"path": officialPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// min 返回两个整数中的较小值
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// parseAccessToken 从URL中解析access token参数
|
||||
func parseAccessToken(accessToken string) (map[string]string, error) {
|
||||
params := make(map[string]string)
|
||||
|
||||
// 如果是完整URL,解析参数
|
||||
if strings.HasPrefix(accessToken, "http://") || strings.HasPrefix(accessToken, "https://") {
|
||||
parsedURL, err := url.Parse(accessToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("URL格式错误: %v", err)
|
||||
}
|
||||
query := parsedURL.Query()
|
||||
params["biz"] = query.Get("__biz")
|
||||
params["uin"] = query.Get("uin")
|
||||
params["key"] = query.Get("key")
|
||||
params["pass_ticket"] = query.Get("pass_ticket")
|
||||
} else {
|
||||
// 尝试使用正则表达式提取参数
|
||||
bizRegex := regexp.MustCompile(`__biz=([^&]+)`)
|
||||
if match := bizRegex.FindStringSubmatch(accessToken); len(match) > 1 {
|
||||
params["biz"] = match[1]
|
||||
}
|
||||
|
||||
uinRegex := regexp.MustCompile(`uin=([^&]+)`)
|
||||
if match := uinRegex.FindStringSubmatch(accessToken); len(match) > 1 {
|
||||
params["uin"] = match[1]
|
||||
}
|
||||
|
||||
keyRegex := regexp.MustCompile(`key=([^&]+)`)
|
||||
if match := keyRegex.FindStringSubmatch(accessToken); len(match) > 1 {
|
||||
params["key"] = match[1]
|
||||
}
|
||||
|
||||
passTicketRegex := regexp.MustCompile(`pass_ticket=([^&]+)`)
|
||||
if match := passTicketRegex.FindStringSubmatch(accessToken); len(match) > 1 {
|
||||
params["pass_ticket"] = match[1]
|
||||
}
|
||||
}
|
||||
|
||||
// 验证必需参数
|
||||
if params["biz"] == "" {
|
||||
return nil, fmt.Errorf("缺少__biz参数")
|
||||
}
|
||||
if params["uin"] == "" {
|
||||
return nil, fmt.Errorf("缺少uin参数")
|
||||
}
|
||||
if params["key"] == "" {
|
||||
return nil, fmt.Errorf("缺少key参数")
|
||||
}
|
||||
if params["pass_ticket"] == "" {
|
||||
return nil, fmt.Errorf("缺少pass_ticket参数")
|
||||
}
|
||||
|
||||
return params, nil
|
||||
}
|
||||
|
||||
// 获取数据列表
|
||||
func getDataListHandler(w http.ResponseWriter, r *http.Request) {
|
||||
dataDir := "../data"
|
||||
@@ -541,3 +878,348 @@ func writeJSON(w http.ResponseWriter, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
// 生成随机Token
|
||||
func generateToken() string {
|
||||
b := make([]byte, 32)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// 调用Python脚本
|
||||
func callPythonScript(scriptPath string, args ...string) (string, error) {
|
||||
// 构建Python命令
|
||||
cmdArgs := append([]string{scriptPath}, args...)
|
||||
cmd := exec.Command("python", cmdArgs...)
|
||||
|
||||
// 设置工作目录为数据库目录
|
||||
dbDir, _ := filepath.Abs(filepath.Join("..", "..", "database"))
|
||||
cmd.Dir = dbDir
|
||||
|
||||
// 执行命令
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s: %s", err, string(output))
|
||||
}
|
||||
|
||||
return string(output), nil
|
||||
}
|
||||
|
||||
// 用户注册处理
|
||||
func registerHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, Response{Success: false, Message: "仅支持POST请求", Code: 405})
|
||||
return
|
||||
}
|
||||
|
||||
var req RegisterRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, Response{Success: false, Message: "请求参数错误", Code: 400})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
if req.Username == "" || req.Password == "" || req.Email == "" {
|
||||
writeJSON(w, Response{Success: false, Message: "用户名、密码和邮箱不能为空", Code: 400})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Python脚本创建用户
|
||||
scriptPath := "user_cli.py"
|
||||
args := []string{"create", req.Username, req.Password, req.Email}
|
||||
|
||||
output, err := callPythonScript(scriptPath, args...)
|
||||
if err != nil {
|
||||
log.Printf("注册失败: %v, 输出: %s", err, output)
|
||||
|
||||
// 判断错误类型
|
||||
if strings.Contains(output, "用户名已存在") || strings.Contains(output, "邮箱已被注册") {
|
||||
writeJSON(w, Response{Success: false, Message: "用户名或邮箱已存在", Code: 409})
|
||||
} else if strings.Contains(output, "验证错误") {
|
||||
writeJSON(w, Response{Success: false, Message: output, Code: 400})
|
||||
} else {
|
||||
writeJSON(w, Response{Success: false, Message: "注册失败", Code: 500})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("用户注册成功: %s", req.Username)
|
||||
writeJSON(w, Response{
|
||||
Success: true,
|
||||
Message: "注册成功",
|
||||
Code: 200,
|
||||
Data: map[string]interface{}{
|
||||
"username": req.Username,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 用户登录处理
|
||||
func loginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, Response{Success: false, Message: "仅支持POST请求", Code: 405})
|
||||
return
|
||||
}
|
||||
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeJSON(w, Response{Success: false, Message: "请求参数错误", Code: 400})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证输入
|
||||
if req.Username == "" || req.Password == "" {
|
||||
writeJSON(w, Response{Success: false, Message: "用户名和密码不能为空", Code: 400})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Python脚本验证用户
|
||||
scriptPath := "user_cli.py"
|
||||
args := []string{"verify", req.Username, req.Password}
|
||||
|
||||
output, err := callPythonScript(scriptPath, args...)
|
||||
log.Printf("🔍 Python输出: %s", output)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ 登录失败: %v", err)
|
||||
writeJSON(w, Response{Success: false, Message: "用户名或密码错误", Code: 401})
|
||||
return
|
||||
}
|
||||
|
||||
// 生成token
|
||||
token := generateToken()
|
||||
|
||||
// 从输出中解析user_id和用户信息
|
||||
var userData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(output), &userData); err != nil {
|
||||
log.Printf("❌ 解析用户数据失败: %v, 输出: %s", err, output)
|
||||
writeJSON(w, Response{Success: false, Message: "服务器内部错误", Code: 500})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否成功
|
||||
if success, ok := userData["success"].(bool); !ok || !success {
|
||||
log.Printf("❌ 用户验证失败: %v", userData)
|
||||
writeJSON(w, Response{Success: false, Message: "用户名或密码错误", Code: 401})
|
||||
return
|
||||
}
|
||||
|
||||
userID := 0
|
||||
if uid, ok := userData["user_id"].(float64); ok {
|
||||
userID = int(uid)
|
||||
}
|
||||
|
||||
// 存储session
|
||||
sessions[token] = &Session{
|
||||
Token: token,
|
||||
UserID: userID,
|
||||
Expiry: time.Now().Add(24 * time.Hour), // 24小时过期
|
||||
}
|
||||
|
||||
log.Printf("✅ 用户登录成功: %s, token: %s", req.Username, token)
|
||||
|
||||
// 构建user_info,不包含密码相关和success标记
|
||||
userInfo := make(map[string]interface{})
|
||||
for k, v := range userData {
|
||||
if k != "password_hash" && k != "success" {
|
||||
userInfo[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
writeJSON(w, Response{
|
||||
Success: true,
|
||||
Message: "登录成功",
|
||||
Code: 200,
|
||||
Data: map[string]interface{}{
|
||||
"token": token,
|
||||
"user_id": userID,
|
||||
"user_info": userInfo,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// 用户登出处理
|
||||
func logoutHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, Response{Success: false, Message: "仅支持POST请求", Code: 405})
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求头中获取token
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
var req struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
json.NewDecoder(r.Body).Decode(&req)
|
||||
token = req.Token
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
writeJSON(w, Response{Success: false, Message: "Token不能为空", Code: 400})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除session
|
||||
delete(sessions, token)
|
||||
|
||||
log.Printf("用户登出成功, token: %s", token)
|
||||
|
||||
writeJSON(w, Response{
|
||||
Success: true,
|
||||
Message: "登出成功",
|
||||
Code: 200,
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户信息处理
|
||||
func getUserInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
writeJSON(w, Response{Success: false, Message: "仅支持GET请求", Code: 405})
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求头中获取token
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
token = r.URL.Query().Get("token")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
writeJSON(w, Response{Success: false, Message: "Token不能为空", Code: 401})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证session
|
||||
session, ok := sessions[token]
|
||||
if !ok || session.Expiry.Before(time.Now()) {
|
||||
if ok {
|
||||
delete(sessions, token) // 删除过期session
|
||||
}
|
||||
writeJSON(w, Response{Success: false, Message: "Token无效或已过期", Code: 401})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Python脚本获取用户信息
|
||||
scriptPath := "user_cli.py"
|
||||
args := []string{"get", fmt.Sprintf("%d", session.UserID)}
|
||||
|
||||
output, err := callPythonScript(scriptPath, args...)
|
||||
if err != nil {
|
||||
log.Printf("获取用户信息失败: %v", err)
|
||||
writeJSON(w, Response{Success: false, Message: "获取用户信息失败", Code: 500})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析用户信息
|
||||
var userData map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(output), &userData); err != nil {
|
||||
log.Printf("解析用户信息失败: %v", err)
|
||||
writeJSON(w, Response{Success: false, Message: "解析用户信息失败", Code: 500})
|
||||
return
|
||||
}
|
||||
|
||||
// 删除密码哈希
|
||||
delete(userData, "password_hash")
|
||||
|
||||
writeJSON(w, Response{
|
||||
Success: true,
|
||||
Message: "获取成功",
|
||||
Code: 200,
|
||||
Data: userData,
|
||||
})
|
||||
}
|
||||
|
||||
// 更新用户信息处理
|
||||
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "POST" {
|
||||
writeJSON(w, Response{Success: false, Message: "仅支持POST请求", Code: 405})
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求头中获取token
|
||||
token := r.Header.Get("Authorization")
|
||||
if token == "" {
|
||||
writeJSON(w, Response{Success: false, Message: "Token不能为空", Code: 401})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证session
|
||||
session, ok := sessions[token]
|
||||
if !ok || session.Expiry.Before(time.Now()) {
|
||||
if ok {
|
||||
delete(sessions, token) // 删除过期session
|
||||
}
|
||||
writeJSON(w, Response{Success: false, Message: "Token无效或已过期", Code: 401})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析请求体
|
||||
var req struct {
|
||||
UserID int `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Bio string `json:"bio"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
log.Printf("❌ 解析请求体失败: %v", err)
|
||||
writeJSON(w, Response{Success: false, Message: "请求参数错误", Code: 400})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("🔍 更新用户信息: user_id=%d, email=%s", req.UserID, req.Email)
|
||||
|
||||
// 验证用户ID与session一致
|
||||
if req.UserID != session.UserID {
|
||||
log.Printf("❌ 用户ID不匹配: req=%d, session=%d", req.UserID, session.UserID)
|
||||
writeJSON(w, Response{Success: false, Message: "无权操作", Code: 403})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用Python脚本更新用户信息
|
||||
scriptPath := "user_cli.py"
|
||||
args := []string{"update", fmt.Sprintf("%d", req.UserID)}
|
||||
|
||||
// 添加需要更新的字段
|
||||
if req.Email != "" {
|
||||
args = append(args, "--email", req.Email)
|
||||
}
|
||||
if req.Bio != "" {
|
||||
args = append(args, "--bio", req.Bio)
|
||||
}
|
||||
|
||||
output, err := callPythonScript(scriptPath, args...)
|
||||
log.Printf("🔍 Python输出: %s", output)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("❌ 更新用户信息失败: %v", err)
|
||||
writeJSON(w, Response{Success: false, Message: "更新失败", Code: 500})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(output), &result); err != nil {
|
||||
log.Printf("❌ 解析响应失败: %v", err)
|
||||
writeJSON(w, Response{Success: false, Message: "服务器内部错误", Code: 500})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否成功
|
||||
if success, ok := result["success"].(bool); !ok || !success {
|
||||
errMsg := "更新失败"
|
||||
if msg, ok := result["error"].(string); ok {
|
||||
errMsg = msg
|
||||
}
|
||||
writeJSON(w, Response{Success: false, Message: errMsg, Code: 500})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ 用户信息更新成功: user_id=%d", req.UserID)
|
||||
|
||||
writeJSON(w, Response{
|
||||
Success: true,
|
||||
Message: "更新成功",
|
||||
Code: 200,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
chcp 65001 >nul
|
||||
title 微信公众号文章爬虫 - API服务器
|
||||
|
||||
:: 检查api_server.exe是否存在
|
||||
if not exist "api_server.exe" (
|
||||
:: 检查api-server.exe是否存在
|
||||
if not exist "api-server.exe" (
|
||||
echo ===============================================
|
||||
echo ⚠️ API服务器未编译
|
||||
echo ===============================================
|
||||
@@ -20,4 +20,4 @@ if not exist "api_server.exe" (
|
||||
|
||||
:: 启动API服务器
|
||||
cls
|
||||
api_server.exe
|
||||
api-server.exe
|
||||
|
||||
@@ -205,7 +205,6 @@ func startCrawling(cfg *configs.Config) {
|
||||
ReadCount: stats["read_num"],
|
||||
LikeCount: stats["old_like_num"],
|
||||
ShareCount: stats["share_num"],
|
||||
ShowRead: stats["show_read"],
|
||||
Comments: comments,
|
||||
CommentLikes: commentLikes,
|
||||
CommentID: commentID,
|
||||
|
||||
BIN
backend/cmd/wechat-crawler.exe
Normal file
BIN
backend/cmd/wechat-crawler.exe
Normal file
Binary file not shown.
22
backend/go.mod
Normal file
22
backend/go.mod
Normal file
@@ -0,0 +1,22 @@
|
||||
module github.com/wechat-crawler
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/go-resty/resty/v2 v2.17.0
|
||||
modernc.org/sqlite v1.40.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
modernc.org/libc v1.66.10 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
57
backend/go.sum
Normal file
57
backend/go.sum
Normal file
@@ -0,0 +1,57 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-resty/resty/v2 v2.17.0 h1:pW9DeXcaL4Rrym4EZ8v7L19zZiIlWPg5YXAcVmt+gN0=
|
||||
github.com/go-resty/resty/v2 v2.17.0/go.mod h1:kCKZ3wWmwJaNc7S29BRtUhJwy7iqmn+2mLtQrOyQlVA=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
|
||||
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
|
||||
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
|
||||
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.40.1 h1:VfuXcxcUWWKRBuP8+BR9L7VnmusMgBNNnBYGEe9w/iY=
|
||||
modernc.org/sqlite v1.40.1/go.mod h1:9fjQZ0mB1LLP0GYrp39oOJXx/I2sxEnZtzCmEQIKvGE=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
BIN
backend/output/wechat-crawler.exe
Normal file
BIN
backend/output/wechat-crawler.exe
Normal file
Binary file not shown.
@@ -25,7 +25,6 @@ type ArticleDetail struct {
|
||||
ReadCount string `json:"read_count"`
|
||||
LikeCount string `json:"like_count"`
|
||||
ShareCount string `json:"share_count"`
|
||||
ShowRead string `json:"show_read"`
|
||||
Comments []string `json:"comments"`
|
||||
CommentLikes []string `json:"comment_likes"`
|
||||
CommentID string `json:"comment_id"`
|
||||
@@ -1624,7 +1623,6 @@ func (w *WechatCrawler) GetArticleDetail(link string) (*ArticleDetail, error) {
|
||||
ReadCount: stats["read_num"],
|
||||
LikeCount: stats["old_like_num"],
|
||||
ShareCount: stats["share_num"],
|
||||
ShowRead: stats["show_read"],
|
||||
Comments: comments,
|
||||
CommentLikes: commentLikes,
|
||||
CommentID: commentID,
|
||||
@@ -1731,7 +1729,6 @@ func (c *WechatCrawler) SaveArticleDetailToExcel(article *ArticleDetail, filePat
|
||||
content.WriteString(fmt.Sprintf("阅读量: %s\n", article.ReadCount))
|
||||
content.WriteString(fmt.Sprintf("点赞数: %s\n", article.LikeCount))
|
||||
content.WriteString(fmt.Sprintf("转发数: %s\n", article.ShareCount))
|
||||
content.WriteString(fmt.Sprintf("在看数: %s\n", article.ShowRead))
|
||||
content.WriteString(strings.Repeat("=", 80))
|
||||
content.WriteString("\n\n")
|
||||
|
||||
|
||||
BIN
backend/wechat-crawler.exe
Normal file
BIN
backend/wechat-crawler.exe
Normal file
Binary file not shown.
BIN
data/wechat_articles.db
Normal file
BIN
data/wechat_articles.db
Normal file
Binary file not shown.
BIN
database/__pycache__/user_database.cpython-312.pyc
Normal file
BIN
database/__pycache__/user_database.cpython-312.pyc
Normal file
Binary file not shown.
94
database/test_user_db.py
Normal file
94
database/test_user_db.py
Normal file
@@ -0,0 +1,94 @@
|
||||
import os
|
||||
import sys
|
||||
from user_database import UserDatabase
|
||||
|
||||
def test_user_database():
|
||||
# 获取数据库路径
|
||||
db_path = os.path.join(os.path.dirname(__file__), 'users.db')
|
||||
print(f"测试数据库路径: {db_path}")
|
||||
|
||||
try:
|
||||
# 初始化数据库
|
||||
db = UserDatabase(db_path)
|
||||
print("✅ 数据库初始化成功")
|
||||
|
||||
# 测试用户创建
|
||||
test_username = "testuser"
|
||||
test_email = "test@example.com"
|
||||
test_password = "Test123456"
|
||||
|
||||
# 先尝试删除测试用户(如果存在)
|
||||
try:
|
||||
user = db.get_user_by_username(test_username)
|
||||
if user:
|
||||
print(f"⚠️ 测试用户 '{test_username}' 已存在,尝试创建新的测试用户")
|
||||
test_username = "testuser_new"
|
||||
test_email = "test_new@example.com"
|
||||
except Exception as e:
|
||||
print(f"ℹ️ 检查用户时发生错误: {e}")
|
||||
|
||||
# 创建新用户
|
||||
print(f"\n🔄 创建测试用户: {test_username}")
|
||||
user_id = db.create_user(test_username, test_password, test_email)
|
||||
print(f"✅ 用户创建成功! 用户ID: {user_id}")
|
||||
|
||||
# 测试获取用户信息
|
||||
print(f"\n🔄 测试获取用户信息")
|
||||
user = db.get_user_by_username(test_username)
|
||||
if user:
|
||||
print(f"✅ 用户信息获取成功!")
|
||||
print(f" - 用户ID: {user['user_id']}")
|
||||
print(f" - 用户名: {user['username']}")
|
||||
print(f" - 邮箱: {user['email']}")
|
||||
print(f" - 创建时间: {user['created_at']}")
|
||||
else:
|
||||
print(f"❌ 无法获取用户信息")
|
||||
|
||||
# 测试密码验证
|
||||
print(f"\n🔄 测试密码验证")
|
||||
# 正确密码
|
||||
valid_user = db.verify_password(test_username, test_password)
|
||||
if valid_user:
|
||||
print(f"✅ 正确密码验证成功")
|
||||
else:
|
||||
print(f"❌ 正确密码验证失败")
|
||||
|
||||
# 错误密码
|
||||
invalid_user = db.verify_password(test_username, "WrongPassword")
|
||||
if not invalid_user:
|
||||
print(f"✅ 错误密码验证正确(返回None)")
|
||||
else:
|
||||
print(f"❌ 错误密码验证失败(应返回None)")
|
||||
|
||||
# 测试更新登录时间
|
||||
print(f"\n🔄 测试更新登录时间")
|
||||
if db.update_login_time(user_id):
|
||||
print(f"✅ 登录时间更新成功")
|
||||
# 验证更新是否成功
|
||||
updated_user = db.get_user_by_id(user_id)
|
||||
print(f" - 更新后的最后登录时间: {updated_user['last_login_at']}")
|
||||
else:
|
||||
print(f"❌ 登录时间更新失败")
|
||||
|
||||
print("\n🎉 所有测试完成!")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 测试失败: {str(e)}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return False
|
||||
finally:
|
||||
if 'db' in locals():
|
||||
try:
|
||||
db.close()
|
||||
print("ℹ️ 数据库连接已关闭")
|
||||
except:
|
||||
pass
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("====================================")
|
||||
print("用户数据库功能测试")
|
||||
print("====================================")
|
||||
success = test_user_database()
|
||||
sys.exit(0 if success else 1)
|
||||
145
database/user_cli.py
Normal file
145
database/user_cli.py
Normal file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
用户数据库命令行接口
|
||||
用于Go后端调用执行用户操作
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
from user_database import UserDatabase
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"error": "缺少命令参数"}))
|
||||
sys.exit(1)
|
||||
|
||||
command = sys.argv[1]
|
||||
db = UserDatabase()
|
||||
|
||||
try:
|
||||
if command == "create":
|
||||
# 创建用户: python user_cli.py create <username> <password> <email>
|
||||
if len(sys.argv) < 5:
|
||||
print(json.dumps({"error": "参数不足: 需要用户名、密码和邮箱"}))
|
||||
sys.exit(1)
|
||||
|
||||
username = sys.argv[2]
|
||||
password = sys.argv[3]
|
||||
email = sys.argv[4]
|
||||
|
||||
user_id = db.create_user(username, password, email)
|
||||
print(json.dumps({
|
||||
"success": True,
|
||||
"user_id": user_id,
|
||||
"username": username
|
||||
}))
|
||||
|
||||
elif command == "verify":
|
||||
# 验证密码: python user_cli.py verify <username> <password>
|
||||
if len(sys.argv) < 4:
|
||||
print(json.dumps({"error": "参数不足: 需要用户名和密码"}))
|
||||
sys.exit(1)
|
||||
|
||||
username = sys.argv[2]
|
||||
password = sys.argv[3]
|
||||
|
||||
user = db.verify_password(username, password)
|
||||
if user:
|
||||
# 删除密码哈希
|
||||
user_data = {k: v for k, v in user.items() if k != 'password_hash'}
|
||||
# 转换datetime为字符串
|
||||
if 'created_at' in user_data and user_data['created_at']:
|
||||
user_data['created_at'] = str(user_data['created_at'])
|
||||
if 'last_login_at' in user_data and user_data['last_login_at']:
|
||||
user_data['last_login_at'] = str(user_data['last_login_at'])
|
||||
|
||||
# 更新登录时间
|
||||
db.update_login_time(user['user_id'])
|
||||
|
||||
print(json.dumps({
|
||||
"success": True,
|
||||
**user_data
|
||||
}))
|
||||
else:
|
||||
print(json.dumps({"error": "用户名或密码错误"}))
|
||||
sys.exit(1)
|
||||
|
||||
elif command == "get":
|
||||
# 获取用户信息: python user_cli.py get <user_id>
|
||||
if len(sys.argv) < 3:
|
||||
print(json.dumps({"error": "参数不足: 需要用户ID"}))
|
||||
sys.exit(1)
|
||||
|
||||
user_id = int(sys.argv[2])
|
||||
user = db.get_user_by_id(user_id)
|
||||
|
||||
if user:
|
||||
# 删除密码哈希
|
||||
user_data = {k: v for k, v in user.items() if k != 'password_hash'}
|
||||
# 转换datetime为字符串
|
||||
if 'created_at' in user_data and user_data['created_at']:
|
||||
user_data['created_at'] = str(user_data['created_at'])
|
||||
if 'last_login_at' in user_data and user_data['last_login_at']:
|
||||
user_data['last_login_at'] = str(user_data['last_login_at'])
|
||||
|
||||
print(json.dumps(user_data))
|
||||
else:
|
||||
print(json.dumps({"error": "用户不存在"}))
|
||||
sys.exit(1)
|
||||
|
||||
elif command == "update":
|
||||
# 更新用户信息: python user_cli.py update <user_id> [--email <email>] [--bio <bio>]
|
||||
if len(sys.argv) < 3:
|
||||
print(json.dumps({"error": "参数不足: 需要用户ID"}))
|
||||
sys.exit(1)
|
||||
|
||||
user_id = int(sys.argv[2])
|
||||
kwargs = {}
|
||||
|
||||
# 解析命令行参数
|
||||
i = 3
|
||||
while i < len(sys.argv):
|
||||
if sys.argv[i] == "--email" and i + 1 < len(sys.argv):
|
||||
kwargs['email'] = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif sys.argv[i] == "--bio" and i + 1 < len(sys.argv):
|
||||
kwargs['bio'] = sys.argv[i + 1]
|
||||
i += 2
|
||||
elif sys.argv[i] == "--password" and i + 1 < len(sys.argv):
|
||||
kwargs['password'] = sys.argv[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if not kwargs:
|
||||
print(json.dumps({"error": "没有提供需要更新的字段"}))
|
||||
sys.exit(1)
|
||||
|
||||
success = db.update_user(user_id, **kwargs)
|
||||
|
||||
if success:
|
||||
print(json.dumps({"success": True, "message": "更新成功"}))
|
||||
else:
|
||||
print(json.dumps({"error": "更新失败"}))
|
||||
sys.exit(1)
|
||||
|
||||
else:
|
||||
print(json.dumps({"error": f"未知命令: {command}"}))
|
||||
sys.exit(1)
|
||||
|
||||
except db.UserExistsError as e:
|
||||
print(json.dumps({"error": str(e), "type": "UserExistsError"}))
|
||||
sys.exit(1)
|
||||
except db.ValidationError as e:
|
||||
print(json.dumps({"error": str(e), "type": "ValidationError"}))
|
||||
sys.exit(1)
|
||||
except db.UserNotFoundError as e:
|
||||
print(json.dumps({"error": str(e), "type": "UserNotFoundError"}))
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": str(e), "type": "Exception"}))
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
556
database/user_database.py
Normal file
556
database/user_database.py
Normal file
@@ -0,0 +1,556 @@
|
||||
import sqlite3
|
||||
import datetime
|
||||
import os
|
||||
import bcrypt
|
||||
import re
|
||||
import logging
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
filename=os.path.join(os.path.dirname(__file__), 'user_db.log')
|
||||
)
|
||||
logger = logging.getLogger('UserDatabase')
|
||||
|
||||
class UserDatabase:
|
||||
def __init__(self, db_path='users.db'):
|
||||
"""
|
||||
初始化数据库配置
|
||||
:param db_path: 数据库文件路径
|
||||
"""
|
||||
self.db_path = os.path.join(os.path.dirname(__file__), db_path)
|
||||
# 初始化时只创建表,不保持连接
|
||||
self._setup_adapters() # 设置全局适配器
|
||||
self._create_tables_at_init() # 初始化时创建表
|
||||
|
||||
def _setup_adapters(self):
|
||||
"""设置SQLite适配器和转换器(全局设置)"""
|
||||
# 添加自定义datetime适配器以解决Python 3.12弃用警告
|
||||
def adapt_datetime(dt):
|
||||
return dt.isoformat()
|
||||
|
||||
# 注册适配器
|
||||
sqlite3.register_adapter(datetime.datetime, adapt_datetime)
|
||||
|
||||
# 添加转换器(从字符串转换回datetime)
|
||||
def convert_datetime(val):
|
||||
try:
|
||||
return datetime.datetime.fromisoformat(val.decode())
|
||||
except (ValueError, AttributeError):
|
||||
return val
|
||||
|
||||
# 注册转换器
|
||||
sqlite3.register_converter('TIMESTAMP', convert_datetime)
|
||||
|
||||
def _get_connection(self):
|
||||
"""
|
||||
获取新的数据库连接(线程安全)
|
||||
:return: 数据库连接对象
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row # 允许通过列名访问结果
|
||||
return conn
|
||||
except sqlite3.Error as e:
|
||||
print(f"数据库连接错误: {e}")
|
||||
raise
|
||||
|
||||
def _create_tables_at_init(self):
|
||||
"""
|
||||
初始化时创建用户表
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
# 创建用户表,添加适当的主键和索引
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at TIMESTAMP,
|
||||
status INTEGER DEFAULT 1,
|
||||
profile_image TEXT,
|
||||
bio TEXT,
|
||||
UNIQUE(username),
|
||||
UNIQUE(email)
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建索引以提高查询效率
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_username ON users(username)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email ON users(email)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON users(status)')
|
||||
|
||||
conn.commit()
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
print(f"创建表错误: {e}")
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def _hash_password(self, password):
|
||||
"""
|
||||
对密码进行哈希加密
|
||||
:param password: 原始密码
|
||||
:return: 加密后的密码哈希
|
||||
"""
|
||||
# 使用bcrypt进行哈希,更安全的密码存储方式
|
||||
salt = bcrypt.gensalt()
|
||||
return bcrypt.hashpw(password.encode(), salt).decode()
|
||||
|
||||
# 定义自定义异常类
|
||||
class DatabaseError(Exception):
|
||||
"""数据库操作异常基类"""
|
||||
pass
|
||||
|
||||
class UserExistsError(DatabaseError):
|
||||
"""用户已存在异常"""
|
||||
pass
|
||||
|
||||
class UserNotFoundError(DatabaseError):
|
||||
"""用户不存在异常"""
|
||||
pass
|
||||
|
||||
class ValidationError(DatabaseError):
|
||||
"""数据验证异常"""
|
||||
pass
|
||||
|
||||
def _validate_input(self, username, email, password=None):
|
||||
"""
|
||||
验证输入数据的有效性
|
||||
:param username: 用户名
|
||||
:param email: 邮箱
|
||||
:param password: 密码(可选)
|
||||
:return: 验证通过返回True,否则抛出异常
|
||||
"""
|
||||
# 验证用户名
|
||||
if not username or not isinstance(username, str):
|
||||
raise self.ValidationError("用户名不能为空")
|
||||
if len(username) < 3:
|
||||
raise self.ValidationError("用户名必须至少包含3个字符")
|
||||
if len(username) > 50:
|
||||
raise self.ValidationError("用户名不能超过50个字符")
|
||||
if not re.match(r'^[a-zA-Z0-9_]+$', username):
|
||||
raise self.ValidationError("用户名只能包含字母、数字和下划线")
|
||||
|
||||
# 验证邮箱 - 使用更严格的正则表达式
|
||||
if not email or not isinstance(email, str):
|
||||
raise self.ValidationError("邮箱不能为空")
|
||||
email_pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
|
||||
if not re.match(email_pattern, email):
|
||||
raise self.ValidationError("邮箱格式不正确")
|
||||
if len(email) > 255:
|
||||
raise self.ValidationError("邮箱不能超过255个字符")
|
||||
|
||||
# 验证密码
|
||||
if password is not None:
|
||||
if not password:
|
||||
raise self.ValidationError("密码不能为空")
|
||||
if len(password) < 6:
|
||||
raise self.ValidationError("密码必须至少包含6个字符")
|
||||
if len(password) > 128:
|
||||
raise self.ValidationError("密码不能超过128个字符")
|
||||
|
||||
return True
|
||||
|
||||
def create_user(self, username, password, email, nickname=''):
|
||||
"""
|
||||
创建新用户
|
||||
:param username: 用户名
|
||||
:param password: 密码
|
||||
:param email: 邮箱
|
||||
:param nickname: 昵称(可选)
|
||||
:return: 创建成功返回用户ID,否则抛出异常
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
# 验证输入
|
||||
self._validate_input(username, email, password)
|
||||
|
||||
# 检查用户名是否已存在
|
||||
cursor.execute("SELECT user_id FROM users WHERE username = ?", (username,))
|
||||
if cursor.fetchone():
|
||||
raise self.UserExistsError("用户名已存在")
|
||||
|
||||
# 检查邮箱是否已存在
|
||||
cursor.execute("SELECT user_id FROM users WHERE email = ?", (email,))
|
||||
if cursor.fetchone():
|
||||
raise self.UserExistsError("邮箱已被注册")
|
||||
|
||||
# 哈希密码
|
||||
password_hash = self._hash_password(password)
|
||||
|
||||
# 插入用户记录
|
||||
cursor.execute('''
|
||||
INSERT INTO users (username, email, password_hash, created_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
''', (username, email, password_hash, datetime.datetime.now()))
|
||||
|
||||
conn.commit()
|
||||
user_id = cursor.lastrowid
|
||||
logger.info(f"创建用户成功,ID: {user_id}, 用户名: {username}")
|
||||
return user_id
|
||||
except self.DatabaseError:
|
||||
raise
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
error_msg = f"创建用户失败: {str(e)}"
|
||||
logger.error(f"创建用户错误: {e}, 用户名: {username}, 邮箱: {email}")
|
||||
raise self.DatabaseError(error_msg)
|
||||
|
||||
def get_user_by_id(self, user_id):
|
||||
"""
|
||||
根据用户ID获取用户信息
|
||||
:param user_id: 用户ID
|
||||
:return: 用户信息字典,如果不存在返回None
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE user_id = ?", (user_id,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
except sqlite3.Error as e:
|
||||
print(f"查询用户错误: {e}")
|
||||
return None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_user_by_username(self, username):
|
||||
"""
|
||||
根据用户名获取用户信息
|
||||
:param username: 用户名
|
||||
:return: 用户信息字典,如果不存在返回None
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE username = ?", (username,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
except sqlite3.Error as e:
|
||||
print(f"查询用户错误: {e}")
|
||||
return None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def get_user_by_email(self, email):
|
||||
"""
|
||||
根据邮箱获取用户信息
|
||||
:param email: 邮箱
|
||||
:return: 用户信息字典,如果不存在返回None
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT * FROM users WHERE email = ?", (email,))
|
||||
row = cursor.fetchone()
|
||||
if row:
|
||||
return dict(row)
|
||||
return None
|
||||
except sqlite3.Error as e:
|
||||
print(f"查询用户错误: {e}")
|
||||
return None
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def verify_password(self, username, password):
|
||||
"""
|
||||
验证用户密码
|
||||
:param username: 用户名
|
||||
:param password: 密码
|
||||
:return: 验证成功返回用户信息字典,失败返回None
|
||||
"""
|
||||
try:
|
||||
# 获取用户信息(get_user_by_username已经使用独立连接)
|
||||
user = self.get_user_by_username(username)
|
||||
if not user:
|
||||
logger.warning(f"密码验证失败: 用户不存在 - {username}")
|
||||
return None
|
||||
|
||||
# 检查账号状态
|
||||
if user['status'] != 1:
|
||||
logger.warning(f"密码验证失败: 账号状态异常 - 用户名: {username}, 状态: {user['status']}")
|
||||
return None
|
||||
|
||||
# 验证密码 - 使用bcrypt的checkpw方法
|
||||
is_valid = bcrypt.checkpw(password.encode('utf-8'), user['password_hash'].encode('utf-8'))
|
||||
|
||||
if is_valid:
|
||||
logger.info(f"密码验证成功: {username}")
|
||||
return user # 验证成功返回用户信息
|
||||
else:
|
||||
logger.warning(f"密码验证失败: 密码错误 - {username}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"密码验证错误: {e}, 用户名: {username}")
|
||||
return None
|
||||
|
||||
def update_login_time(self, user_id):
|
||||
"""
|
||||
更新用户最后登录时间
|
||||
:param user_id: 用户ID
|
||||
:return: 更新成功返回True,否则返回False
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"UPDATE users SET last_login_at = ? WHERE user_id = ?",
|
||||
(datetime.datetime.now(), user_id)
|
||||
)
|
||||
conn.commit()
|
||||
return cursor.rowcount > 0
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
print(f"更新登录时间错误: {e}")
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def update_user(self, user_id, **kwargs):
|
||||
"""
|
||||
更新用户信息
|
||||
:param user_id: 用户ID
|
||||
:param kwargs: 要更新的字段
|
||||
:return: 更新成功返回True,否则返回False
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
# 检查用户是否存在
|
||||
if not self.get_user_by_id(user_id):
|
||||
raise self.UserNotFoundError("用户不存在")
|
||||
|
||||
# 准备更新字段
|
||||
update_fields = []
|
||||
update_values = []
|
||||
|
||||
# 记录要更新的字段用于日志
|
||||
updated_fields_log = []
|
||||
|
||||
if 'username' in kwargs:
|
||||
update_fields.append("username = ?")
|
||||
update_values.append(kwargs['username'])
|
||||
updated_fields_log.append('username')
|
||||
# 验证用户名
|
||||
self._validate_input(kwargs['username'], "dummy@example.com")
|
||||
# 检查用户名是否被其他用户使用
|
||||
cursor.execute(
|
||||
"SELECT user_id FROM users WHERE username = ? AND user_id != ?",
|
||||
(kwargs['username'], user_id)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise self.UserExistsError("用户名已存在")
|
||||
|
||||
if 'email' in kwargs:
|
||||
update_fields.append("email = ?")
|
||||
update_values.append(kwargs['email'])
|
||||
updated_fields_log.append('email')
|
||||
# 验证邮箱
|
||||
self._validate_input("dummy", kwargs['email'])
|
||||
# 检查邮箱是否被其他用户使用
|
||||
cursor.execute(
|
||||
"SELECT user_id FROM users WHERE email = ? AND user_id != ?",
|
||||
(kwargs['email'], user_id)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
raise self.UserExistsError("邮箱已被注册")
|
||||
|
||||
if 'password' in kwargs:
|
||||
# 验证密码
|
||||
self._validate_input("dummy", "dummy@example.com", kwargs['password'])
|
||||
# 哈希密码
|
||||
password_hash = self._hash_password(kwargs['password'])
|
||||
update_fields.append("password_hash = ?")
|
||||
update_values.append(password_hash)
|
||||
updated_fields_log.append('password')
|
||||
|
||||
if 'status' in kwargs:
|
||||
# 验证状态值
|
||||
if not isinstance(kwargs['status'], int) or kwargs['status'] not in (0, 1, 2):
|
||||
raise self.ValidationError("无效的用户状态值")
|
||||
update_fields.append("status = ?")
|
||||
update_values.append(kwargs['status'])
|
||||
updated_fields_log.append('status')
|
||||
|
||||
if 'profile_image' in kwargs:
|
||||
# 验证头像路径
|
||||
if kwargs['profile_image'] and len(kwargs['profile_image']) > 255:
|
||||
raise self.ValidationError("头像路径过长")
|
||||
update_fields.append("profile_image = ?")
|
||||
update_values.append(kwargs['profile_image'])
|
||||
updated_fields_log.append('profile_image')
|
||||
|
||||
if 'bio' in kwargs:
|
||||
# 验证个人简介
|
||||
if kwargs['bio'] and len(kwargs['bio']) > 500:
|
||||
raise self.ValidationError("个人简介不能超过500个字符")
|
||||
update_fields.append("bio = ?")
|
||||
update_values.append(kwargs['bio'])
|
||||
updated_fields_log.append('bio')
|
||||
|
||||
# 执行更新
|
||||
if update_fields:
|
||||
update_values.append(user_id)
|
||||
update_sql = f"UPDATE users SET {', '.join(update_fields)} WHERE user_id = ?"
|
||||
cursor.execute(update_sql, update_values)
|
||||
conn.commit()
|
||||
logger.info(f"更新用户信息成功,ID: {user_id}, 更新字段: {', '.join(updated_fields_log)}")
|
||||
return True
|
||||
else:
|
||||
# 没有要更新的字段
|
||||
logger.warning(f"没有更新字段提供,用户ID: {user_id}")
|
||||
return True
|
||||
except self.DatabaseError:
|
||||
raise
|
||||
except sqlite3.Error as e:
|
||||
conn.rollback()
|
||||
logger.error(f"更新用户信息错误: {e}, 用户ID: {user_id}")
|
||||
raise self.DatabaseError(f"更新用户信息失败: {str(e)}")
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
def delete_user(self, user_id):
|
||||
"""
|
||||
删除用户
|
||||
:param user_id: 用户ID
|
||||
:return: 删除成功返回True,否则返回False
|
||||
"""
|
||||
try:
|
||||
# 检查用户是否存在
|
||||
user = self.get_user_by_id(user_id)
|
||||
if not user:
|
||||
raise self.UserNotFoundError("用户不存在")
|
||||
|
||||
# 开始事务
|
||||
self.cursor.execute("DELETE FROM users WHERE user_id = ?", (user_id,))
|
||||
self.conn.commit()
|
||||
|
||||
if self.cursor.rowcount > 0:
|
||||
logger.info(f"删除用户成功,ID: {user_id}, 用户名: {user['username']}")
|
||||
return True
|
||||
return False
|
||||
except self.DatabaseError:
|
||||
raise
|
||||
except sqlite3.Error as e:
|
||||
self.conn.rollback()
|
||||
error_msg = f"删除用户失败: {str(e)}"
|
||||
logger.error(f"删除用户错误: {e}, 用户ID: {user_id}")
|
||||
raise self.DatabaseError(error_msg)
|
||||
|
||||
def list_users(self, limit=100, offset=0):
|
||||
"""
|
||||
列出用户(支持分页)
|
||||
:param limit: 每页数量
|
||||
:param offset: 偏移量
|
||||
:return: 用户列表
|
||||
"""
|
||||
conn = self._get_connection()
|
||||
try:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?",
|
||||
(limit, offset)
|
||||
)
|
||||
users = []
|
||||
for row in cursor.fetchall():
|
||||
users.append(dict(row))
|
||||
return users
|
||||
except sqlite3.Error as e:
|
||||
print(f"列出用户错误: {e}")
|
||||
return []
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def close(self):
|
||||
"""
|
||||
关闭数据库连接(保持兼容性)
|
||||
"""
|
||||
# 现在每个操作都使用独立连接,不需要关闭持久连接
|
||||
print("数据库连接管理已更新,每个操作使用独立连接")
|
||||
|
||||
# 示例用法
|
||||
if __name__ == "__main__":
|
||||
print("=== 用户数据库示例程序 ===")
|
||||
# 创建数据库实例
|
||||
db = UserDatabase()
|
||||
|
||||
try:
|
||||
print("\n1. 创建测试用户")
|
||||
# 尝试创建已存在的用户
|
||||
try:
|
||||
user_id = db.create_user(
|
||||
username="demo_user",
|
||||
email="demo@example.com",
|
||||
password="SecurePass123"
|
||||
)
|
||||
print(f"✅ 创建用户成功,ID: {user_id}")
|
||||
except db.UserExistsError as e:
|
||||
print(f"ℹ️ {e},继续使用现有用户")
|
||||
user = db.get_user_by_username("demo_user")
|
||||
if user:
|
||||
user_id = user['user_id']
|
||||
|
||||
print("\n2. 查询用户信息")
|
||||
user = db.get_user_by_id(user_id)
|
||||
if user:
|
||||
# 不打印密码哈希
|
||||
safe_user = {k: v for k, v in user.items() if k != 'password_hash'}
|
||||
print(f"✅ 查询用户成功: {safe_user}")
|
||||
|
||||
print("\n3. 验证密码")
|
||||
is_valid = db.verify_password("demo_user", "SecurePass123")
|
||||
print(f"✅ 密码验证: {'通过' if is_valid else '失败'}")
|
||||
|
||||
# 测试密码验证失败的情况
|
||||
is_valid_wrong = db.verify_password("demo_user", "WrongPassword")
|
||||
print(f"✅ 错误密码验证: {'通过' if is_valid_wrong else '失败'}")
|
||||
|
||||
print("\n4. 更新用户信息")
|
||||
success = db.update_user(user_id,
|
||||
bio="这是一个测试用户账号",
|
||||
status=1)
|
||||
if success:
|
||||
updated_user = db.get_user_by_id(user_id)
|
||||
safe_updated = {k: v for k, v in updated_user.items() if k != 'password_hash'}
|
||||
print(f"✅ 更新用户成功: {safe_updated}")
|
||||
|
||||
print("\n5. 列出用户")
|
||||
users = db.list_users(limit=10)
|
||||
safe_users = [{k: v for k, v in u.items() if k != 'password_hash'} for u in users]
|
||||
print(f"✅ 共找到 {len(safe_users)} 个用户")
|
||||
for u in safe_users:
|
||||
print(f" - {u['username']} ({u['email']})")
|
||||
|
||||
print("\n6. 测试数据验证")
|
||||
try:
|
||||
db.create_user(username="ab", email="invalid-email", password="123")
|
||||
except db.ValidationError as e:
|
||||
print(f"✅ 验证错误捕获成功: {e}")
|
||||
|
||||
print("\n7. 更新登录时间")
|
||||
if db.update_login_time(user_id):
|
||||
print("✅ 登录时间更新成功")
|
||||
|
||||
print("\n=== 测试完成 ===")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ 发生错误: {e}")
|
||||
finally:
|
||||
# 关闭数据库连接
|
||||
print("\n关闭数据库连接...")
|
||||
db.close()
|
||||
print("数据库连接已关闭")
|
||||
20
database/user_db.log
Normal file
20
database/user_db.log
Normal file
@@ -0,0 +1,20 @@
|
||||
2025-12-02 13:50:34,469 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB>ɹ<EFBFBD><C9B9><EFBFBD>ID: 6, <20>û<EFBFBD><C3BB><EFBFBD>: testuser123
|
||||
2025-12-02 13:50:40,670 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser123
|
||||
2025-12-02 13:52:24,597 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB>ɹ<EFBFBD><C9B9><EFBFBD>ID: 7, <20>û<EFBFBD><C3BB><EFBFBD>: testuser3
|
||||
2025-12-02 13:52:31,767 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser3
|
||||
2025-12-02 13:54:04,070 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB>ɹ<EFBFBD><C9B9><EFBFBD>ID: 8, <20>û<EFBFBD><C3BB><EFBFBD>: test
|
||||
2025-12-02 13:54:55,089 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser3
|
||||
2025-12-02 13:56:44,653 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser3
|
||||
2025-12-02 13:56:51,923 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser3
|
||||
2025-12-02 13:57:52,850 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser3
|
||||
2025-12-02 13:58:03,952 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser3
|
||||
2025-12-02 13:58:30,809 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: testuser3
|
||||
2025-12-02 13:59:00,866 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
2025-12-02 14:02:56,791 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>Ϣ<EFBFBD>ɹ<EFBFBD><C9B9><EFBFBD>ID: 7, <20><><EFBFBD><EFBFBD><EFBFBD>ֶ<EFBFBD>: email, bio
|
||||
2025-12-02 14:06:03,849 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
2025-12-02 14:12:21,407 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
2025-12-02 14:14:03,743 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
2025-12-02 14:16:30,941 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
2025-12-02 14:18:21,143 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
2025-12-02 14:23:19,324 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
2025-12-02 14:51:35,109 - UserDatabase - INFO - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>ɹ<EFBFBD>: test
|
||||
BIN
database/users.db
Normal file
BIN
database/users.db
Normal file
Binary file not shown.
67
database/view_all_users.py
Normal file
67
database/view_all_users.py
Normal file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
查看所有用户信息脚本
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from database.user_database import UserDatabase
|
||||
|
||||
def main():
|
||||
"""
|
||||
主函数:查看所有用户信息
|
||||
"""
|
||||
print("=== 查看所有用户信息 ===")
|
||||
|
||||
try:
|
||||
# 创建数据库实例 - 指定正确的数据库路径
|
||||
db_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'backend', 'users.db')
|
||||
print(f"使用数据库路径: {db_path}")
|
||||
db = UserDatabase(db_path=db_path)
|
||||
|
||||
# 获取所有用户
|
||||
print("\n正在查询用户信息...")
|
||||
users = db.list_users(limit=1000)
|
||||
|
||||
if not users:
|
||||
print("\n❌ 未找到任何用户")
|
||||
return
|
||||
|
||||
print(f"\n✅ 共找到 {len(users)} 个用户:")
|
||||
print("-" * 100)
|
||||
print(f"{'用户ID':<10} | {'用户名':<20} | {'邮箱':<30} | {'状态':<10} | {'创建时间':<20}")
|
||||
print("-" * 100)
|
||||
|
||||
# 显示用户信息(不包含密码哈希)
|
||||
for user in users:
|
||||
# 创建不包含密码哈希的安全用户信息
|
||||
safe_user = {k: v for k, v in user.items() if k != 'password_hash'}
|
||||
|
||||
# 格式化输出
|
||||
user_id = safe_user.get('user_id', '')
|
||||
username = safe_user.get('username', '')
|
||||
email = safe_user.get('email', '')
|
||||
status = '正常' if safe_user.get('status') == 1 else '禁用' if safe_user.get('status') == 0 else '异常'
|
||||
created_at = safe_user.get('created_at', '')
|
||||
|
||||
print(f"{user_id:<10} | {username:<20} | {email:<30} | {status:<10} | {created_at:<20}")
|
||||
|
||||
print("-" * 100)
|
||||
print("\n✅ 用户信息查看完成")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 发生错误: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
# 关闭数据库连接
|
||||
if 'db' in locals():
|
||||
print("\n关闭数据库连接...")
|
||||
db.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,222 +0,0 @@
|
||||
# 🚀 微信公众号文章爬虫系统 - Web界面
|
||||
|
||||
一个现代化的Web界面,用于管理微信公众号文章爬虫功能。
|
||||
|
||||
## 📋 功能特性
|
||||
|
||||
### 🍪 Cookie 配置
|
||||
- 便捷的Cookie输入和保存
|
||||
- Cookie示例和验证
|
||||
- 实时状态反馈
|
||||
|
||||
### 📄 下载单篇文章
|
||||
- 支持微信文章链接输入
|
||||
- 可选择保存图片和内容
|
||||
- 实时下载进度显示
|
||||
|
||||
### 📋 获取文章列表
|
||||
- Access Token URL输入
|
||||
- 自定义获取页数
|
||||
- 批量文章信息获取
|
||||
|
||||
### 📦 批量下载文章
|
||||
- 公众号名称或链接输入
|
||||
- 批量下载文章详情
|
||||
- 智能进度跟踪
|
||||
|
||||
### 📊 数据管理
|
||||
- 已下载数据概览
|
||||
- 文章统计信息
|
||||
- 快速文件夹访问
|
||||
|
||||
## 🛠 使用方法
|
||||
|
||||
### 方法1:快速启动(推荐)
|
||||
|
||||
1. **双击启动脚本**
|
||||
```
|
||||
start_web.bat
|
||||
```
|
||||
|
||||
2. **自动打开浏览器**
|
||||
- 系统会自动检测Python或使用PowerShell
|
||||
- 默认地址:`http://localhost:8000` 或 `http://localhost:8080`
|
||||
|
||||
### 方法2:手动启动
|
||||
|
||||
#### 使用Python(推荐)
|
||||
```bash
|
||||
cd frontend
|
||||
python -m http.server 8000
|
||||
```
|
||||
|
||||
#### 使用Node.js
|
||||
```bash
|
||||
cd frontend
|
||||
npx http-server -p 8000
|
||||
```
|
||||
|
||||
#### 使用其他Web服务器
|
||||
- 将frontend文件夹作为Web根目录即可
|
||||
|
||||
## 🎮 界面使用说明
|
||||
|
||||
### 主界面
|
||||
- **功能卡片**:点击不同卡片进入对应功能
|
||||
- **现代UI**:响应式设计,支持桌面和移动端
|
||||
- **状态指示**:实时显示操作状态和进度
|
||||
|
||||
### Cookie配置页面
|
||||
1. 点击"Cookie 配置"卡片
|
||||
2. 粘贴从Fiddler获取的Cookie内容
|
||||
3. 点击"保存Cookie"按钮
|
||||
4. 等待保存成功提示
|
||||
|
||||
### 下载单篇文章
|
||||
1. 进入"下载单篇文章"功能
|
||||
2. 输入微信文章完整链接
|
||||
3. 选择是否保存图片和内容
|
||||
4. 点击"开始下载"查看进度
|
||||
|
||||
### 获取文章列表
|
||||
1. 进入"获取文章列表"功能
|
||||
2. 粘贴包含认证参数的完整URL
|
||||
3. 设置获取页数(可选)
|
||||
4. 点击"开始获取"执行任务
|
||||
|
||||
### 批量下载
|
||||
1. 进入"批量下载文章"功能
|
||||
2. 输入公众号名称或任意文章链接
|
||||
3. 选择保存选项
|
||||
4. 点击"开始批量下载"
|
||||
|
||||
### 数据管理
|
||||
1. 进入"数据管理"功能
|
||||
2. 点击"刷新列表"查看已下载数据
|
||||
3. 可以查看文章详情或打开文件夹
|
||||
|
||||
## 🎨 界面特性
|
||||
|
||||
### 响应式设计
|
||||
- ✅ 桌面端优化体验
|
||||
- ✅ 平板和手机端兼容
|
||||
- ✅ 自适应布局
|
||||
|
||||
### 现代化UI
|
||||
- 🎨 渐变色彩搭配
|
||||
- 💫 平滑动画效果
|
||||
- 📱 卡片式设计语言
|
||||
- 🌟 悬停交互反馈
|
||||
|
||||
### 交互体验
|
||||
- ⌨️ 快捷键支持(ESC返回,Ctrl+Enter执行)
|
||||
- 🔄 实时进度条
|
||||
- 📊 状态指示器
|
||||
- 🔔 操作反馈提示
|
||||
|
||||
## 🔧 技术架构
|
||||
|
||||
### 前端技术栈
|
||||
- **HTML5**: 现代语义化标记
|
||||
- **CSS3**: Flexbox/Grid + 动画
|
||||
- **JavaScript**: ES6+ + jQuery
|
||||
- **响应式**: Mobile-First设计
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
frontend/
|
||||
├── index.html # 主页面
|
||||
├── css/
|
||||
│ └── style.css # 样式文件
|
||||
├── js/
|
||||
│ └── app.js # 应用逻辑
|
||||
├── start_web.bat # 启动脚本
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
### 与后端交互
|
||||
- 目前为演示版本,使用前端模拟
|
||||
- 预留了完整的API接口结构
|
||||
- 支持与命令行程序集成
|
||||
|
||||
## 🚀 部署选项
|
||||
|
||||
### 本地开发
|
||||
```bash
|
||||
# 克隆项目
|
||||
cd frontend
|
||||
|
||||
# 启动开发服务器
|
||||
python -m http.server 8000
|
||||
# 或
|
||||
npx http-server -p 8000
|
||||
```
|
||||
|
||||
### 生产环境
|
||||
- **Nginx**: 部署静态文件
|
||||
- **Apache**: 配置虚拟主机
|
||||
- **IIS**: Windows服务器部署
|
||||
- **Docker**: 容器化部署
|
||||
|
||||
## 📊 浏览器兼容性
|
||||
|
||||
| 浏览器 | 版本 | 支持状态 |
|
||||
|--------|------|---------|
|
||||
| Chrome | 60+ | ✅ 完全支持 |
|
||||
| Firefox | 55+ | ✅ 完全支持 |
|
||||
| Safari | 12+ | ✅ 完全支持 |
|
||||
| Edge | 79+ | ✅ 完全支持 |
|
||||
| IE | 11 | ⚠️ 基础支持 |
|
||||
|
||||
## 🐛 常见问题
|
||||
|
||||
### Q: 页面打不开或样式异常?
|
||||
A: 确保所有文件在同一目录下,使用HTTP服务器访问(不是file://协议)
|
||||
|
||||
### Q: 功能按钮点击无反应?
|
||||
A: 检查浏览器控制台是否有JavaScript错误,确保jQuery正常加载
|
||||
|
||||
### Q: 进度条不显示?
|
||||
A: 当前为演示版本,进度为模拟效果。实际部署需要连接后端API
|
||||
|
||||
### Q: 如何连接实际的后端?
|
||||
A: 修改`js/app.js`中的API调用部分,替换模拟逻辑为实际HTTP请求
|
||||
|
||||
## 🔮 后续计划
|
||||
|
||||
### v1.1 计划功能
|
||||
- [ ] 真实后端API集成
|
||||
- [ ] WebSocket实时通信
|
||||
- [ ] 文件上传拖拽功能
|
||||
- [ ] 任务队列管理
|
||||
|
||||
### v1.2 计划功能
|
||||
- [ ] 用户认证系统
|
||||
- [ ] 多公众号管理
|
||||
- [ ] 数据可视化图表
|
||||
- [ ] 导出功能增强
|
||||
|
||||
## 📄 开源协议
|
||||
|
||||
本项目仅供学习和研究使用,请遵守相关法律法规和服务条款。
|
||||
|
||||
## 🤝 贡献指南
|
||||
|
||||
1. Fork 本项目
|
||||
2. 创建功能分支
|
||||
3. 提交更改
|
||||
4. 发起 Pull Request
|
||||
|
||||
## 📞 支持联系
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
|
||||
- 📧 邮箱: your-email@example.com
|
||||
- 💬 Issues: 在GitHub提交Issue
|
||||
- 📱 QQ群: 123456789
|
||||
|
||||
---
|
||||
|
||||
**⚠️ 免责声明**: 本工具仅供学习交流使用,请遵守相关法律法规和平台服务条款。使用者需自行承担使用风险。
|
||||
|
||||
**🌟 如果这个项目对您有帮助,请给个Star支持一下!**
|
||||
@@ -393,9 +393,9 @@
|
||||
<a href="#" class="nav-link">监控中心</a>
|
||||
<a href="#" class="nav-link">数据分析</a>
|
||||
<a href="#" class="nav-link">帮助文档</a>
|
||||
<a href="user-center.html" class="nav-link" id="userMenu" style="display: none;">👤 用户中心</a>
|
||||
<a href="user-center.html" class="nav-link" id="userCenterLink">👤 用户中心</a>
|
||||
<a href="login.html" class="nav-link" id="loginLink">🔐 登录</a>
|
||||
<a href="#" class="nav-link" id="logoutLink" style="display: none;" onclick="logout()">🚪 退出</a>
|
||||
<div id="userMenu" class="user-info"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -736,11 +736,18 @@ ${article.content.replace(/<[^>]*>/g, '')}
|
||||
|
||||
// 登录状态管理
|
||||
function checkLoginStatus() {
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
||||
const username = localStorage.getItem('username');
|
||||
// 检查认证数据
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
|
||||
if (isLoggedIn && username) {
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
const username = auth.user_info && auth.user_info.username ? auth.user_info.username : '用户';
|
||||
showLoggedInState(username);
|
||||
} catch (e) {
|
||||
console.error('解析登录数据失败:', e);
|
||||
showLoggedOutState();
|
||||
}
|
||||
} else {
|
||||
showLoggedOutState();
|
||||
}
|
||||
@@ -748,38 +755,87 @@ ${article.content.replace(/<[^>]*>/g, '')}
|
||||
|
||||
// 显示已登录状态
|
||||
function showLoggedInState(username) {
|
||||
document.getElementById('loginLink').style.display = 'none';
|
||||
document.getElementById('userMenu').style.display = 'block';
|
||||
document.getElementById('logoutLink').style.display = 'block';
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
|
||||
// 为用户中心菜单添加点击事件
|
||||
document.getElementById('userMenu').onclick = function() {
|
||||
window.location.href = 'user-center.html';
|
||||
};
|
||||
if (loginLink) loginLink.style.display = 'none';
|
||||
if (userMenu) {
|
||||
userMenu.innerHTML = `
|
||||
<span style="margin-right: 10px;">👋 ${username}</span>
|
||||
<button onclick="logout()" style="
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'"
|
||||
onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||
退出登录
|
||||
</button>
|
||||
`;
|
||||
userMenu.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示未登录状态
|
||||
function showLoggedOutState() {
|
||||
document.getElementById('loginLink').style.display = 'block';
|
||||
document.getElementById('userMenu').style.display = 'none';
|
||||
document.getElementById('logoutLink').style.display = 'none';
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
|
||||
if (loginLink) loginLink.style.display = 'inline';
|
||||
if (userMenu) userMenu.innerHTML = '';
|
||||
}
|
||||
|
||||
// 登出功能
|
||||
function logout() {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
// 获取认证数据
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
let token = '';
|
||||
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
token = auth.token || '';
|
||||
} catch (e) {
|
||||
console.error('解析token失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用后端登出API
|
||||
fetch('http://localhost:8080/api/user/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
}
|
||||
}).then(response => {
|
||||
// 无论API调用成功与否,都清除本地数据
|
||||
localStorage.removeItem('authData');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('userSession');
|
||||
|
||||
showLoggedOutState();
|
||||
alert('已成功退出登录!');
|
||||
window.location.href = 'login.html';
|
||||
}).catch(error => {
|
||||
console.error('登出请求失败:', error);
|
||||
// 即使API调用失败,也清除本地数据
|
||||
localStorage.removeItem('authData');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('userSession');
|
||||
|
||||
// 如果在用户中心页面,跳转到首页
|
||||
if (window.location.pathname.includes('user-center.html')) {
|
||||
window.location.href = 'frontend.html';
|
||||
}
|
||||
alert('已成功退出登录!');
|
||||
window.location.href = 'login.html';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -150,6 +150,21 @@ body {
|
||||
box-shadow: 0 8px 25px rgba(149, 165, 166, 0.4);
|
||||
}
|
||||
|
||||
/* 按钮禁用状态 */
|
||||
.btn:disabled,
|
||||
.btn.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.btn:disabled:hover,
|
||||
.btn.disabled:hover {
|
||||
transform: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* 区域样式 */
|
||||
.section {
|
||||
animation: fadeIn 0.5s ease-in;
|
||||
@@ -480,3 +495,27 @@ body {
|
||||
/* 显示/隐藏工具类 */
|
||||
.hidden { display: none !important; }
|
||||
.visible { display: block !important; }
|
||||
|
||||
/* 帮助文本样式 */
|
||||
.help-text {
|
||||
background: #f8f9fa;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 25px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.help-text p {
|
||||
margin: 10px 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.help-text ul {
|
||||
margin: 10px 0 10px 20px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.help-text li {
|
||||
margin: 8px 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
@@ -1053,9 +1053,9 @@
|
||||
<!-- 头部 -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<a href="#" class="logo">🔍 易搜高</a>
|
||||
<a href="frontend.html" class="logo">🔍 易搜高</a>
|
||||
<nav class="nav-menu">
|
||||
<a href="#" class="nav-link active">首页</a>
|
||||
<a href="frontend.html" class="nav-link active">首页</a>
|
||||
<a href="#" class="nav-link">监控中心</a>
|
||||
<a href="#" class="nav-link">数据分析</a>
|
||||
<a href="#" class="nav-link">帮助文档</a>
|
||||
@@ -1073,6 +1073,7 @@
|
||||
<div class="nav-tabs">
|
||||
<button class="nav-tab active" data-tab="library">📚 自媒体库</button>
|
||||
<button class="nav-tab" data-tab="monitor">👁️ 监控账号</button>
|
||||
<button class="nav-tab" onclick="window.location.href='index.html'" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white; margin-top: 10px;">🚀 爬虫系统</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -310,9 +310,9 @@
|
||||
<a href="#" class="nav-link">监控中心</a>
|
||||
<a href="#" class="nav-link">数据分析</a>
|
||||
<a href="#" class="nav-link">帮助文档</a>
|
||||
<a href="user-center.html" class="nav-link" id="userMenu" style="display: none;">👤 用户中心</a>
|
||||
<a href="user-center.html" class="nav-link" id="userCenterLink">👤 用户中心</a>
|
||||
<a href="login.html" class="nav-link" id="loginLink">🔐 登录</a>
|
||||
<a href="#" class="nav-link" id="logoutLink" style="display: none;" onclick="logout()">🚪 退出</a>
|
||||
<div id="userMenu" class="user-info"></div>
|
||||
</nav>
|
||||
</div>
|
||||
<h1>历史文章</h1>
|
||||
@@ -629,11 +629,18 @@
|
||||
|
||||
// 登录状态管理
|
||||
function checkLoginStatus() {
|
||||
const isLoggedIn = localStorage.getItem('isLoggedIn') === 'true';
|
||||
const username = localStorage.getItem('username');
|
||||
// 检查认证数据
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
|
||||
if (isLoggedIn && username) {
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
const username = auth.user_info && auth.user_info.username ? auth.user_info.username : '用户';
|
||||
showLoggedInState(username);
|
||||
} catch (e) {
|
||||
console.error('解析登录数据失败:', e);
|
||||
showLoggedOutState();
|
||||
}
|
||||
} else {
|
||||
showLoggedOutState();
|
||||
}
|
||||
@@ -641,26 +648,59 @@
|
||||
|
||||
// 显示已登录状态
|
||||
function showLoggedInState(username) {
|
||||
document.getElementById('loginLink').style.display = 'none';
|
||||
document.getElementById('userMenu').style.display = 'block';
|
||||
document.getElementById('logoutLink').style.display = 'block';
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
|
||||
// 为用户中心菜单添加点击事件
|
||||
document.getElementById('userMenu').onclick = function() {
|
||||
window.location.href = 'user-center.html';
|
||||
};
|
||||
if (loginLink) loginLink.style.display = 'none';
|
||||
if (userMenu) {
|
||||
userMenu.innerHTML = `
|
||||
<div class="user-info">
|
||||
<span class="user-name">${username}</span>
|
||||
<button class="logout-btn" onclick="logout()" style="background: rgba(255,255,255,0.2); color: white; border: 1px solid rgba(255,255,255,0.3); padding: 6px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; transition: all 0.3s;">退出登录</button>
|
||||
</div>
|
||||
`;
|
||||
userMenu.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示未登录状态
|
||||
function showLoggedOutState() {
|
||||
document.getElementById('loginLink').style.display = 'block';
|
||||
document.getElementById('userMenu').style.display = 'none';
|
||||
document.getElementById('logoutLink').style.display = 'none';
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
|
||||
if (loginLink) loginLink.style.display = 'inline';
|
||||
if (userMenu) userMenu.innerHTML = '';
|
||||
}
|
||||
|
||||
// 登出功能
|
||||
function logout() {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
// 获取认证数据
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
let token = '';
|
||||
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
token = auth.token || '';
|
||||
} catch (e) {
|
||||
console.error('解析token失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用后端API登出
|
||||
fetch('http://localhost:8080/api/user/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify({ token: token })
|
||||
})
|
||||
.finally(() => {
|
||||
// 清除所有认证信息
|
||||
localStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('authData');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('token');
|
||||
@@ -668,11 +708,8 @@
|
||||
|
||||
showLoggedOutState();
|
||||
alert('已成功退出登录!');
|
||||
|
||||
// 如果在用户中心页面,跳转到首页
|
||||
if (window.location.pathname.includes('user-center.html')) {
|
||||
window.location.href = 'frontend.html';
|
||||
}
|
||||
window.location.href = 'login.html';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- 头部导航 -->
|
||||
<header class="top-navbar" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px 0; box-shadow: 0 2px 10px rgba(0,0,0,0.1); position: sticky; top: 0; z-index: 100;">
|
||||
<div class="navbar-content" style="max-width: 1200px; margin: 0 auto; padding: 0 20px; display: flex; align-items: center; justify-content: space-between;">
|
||||
<a href="frontend.html" class="logo" style="font-size: 24px; font-weight: bold; text-decoration: none; color: white;">🔍 易搜高</a>
|
||||
<nav class="nav-menu" style="display: flex; gap: 15px; align-items: center;">
|
||||
<a href="frontend.html" class="nav-link" style="color: white; text-decoration: none; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 6px; transition: all 0.3s; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'">首页</a>
|
||||
<a href="#" class="nav-link" style="color: white; text-decoration: none; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 6px; transition: all 0.3s; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'">监控中心</a>
|
||||
<a href="#" class="nav-link" style="color: white; text-decoration: none; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 6px; transition: all 0.3s; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'">数据分析</a>
|
||||
<a href="#" class="nav-link" style="color: white; text-decoration: none; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 6px; transition: all 0.3s; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'">帮助文档</a>
|
||||
<a href="user-center.html" class="nav-link" id="userCenterLink" style="color: white; text-decoration: none; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 6px; transition: all 0.3s; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'">👤 用户中心</a>
|
||||
<a href="login.html" class="nav-link" id="loginLink" style="color: white; text-decoration: none; font-size: 14px; font-weight: 500; padding: 8px 16px; border-radius: 6px; transition: all 0.3s; background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);" onmouseover="this.style.background='rgba(255, 255, 255, 0.2)'" onmouseout="this.style.background='rgba(255, 255, 255, 0.1)'">🔐 登录</a>
|
||||
<div id="userMenu" class="user-info" style="display: flex; align-items: center; gap: 10px;"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>🚀 微信公众号文章爬虫系统</h1>
|
||||
@@ -23,13 +39,6 @@
|
||||
<button class="btn btn-primary" onclick="showSection('homepage')">进入</button>
|
||||
</div>
|
||||
|
||||
<div class="card" id="card-single">
|
||||
<div class="card-icon">📄</div>
|
||||
<h3>下载单篇文章</h3>
|
||||
<p>根据链接下载单篇文章</p>
|
||||
<button class="btn btn-primary" onclick="showSection('single')">进入</button>
|
||||
</div>
|
||||
|
||||
<div class="card" id="card-list">
|
||||
<div class="card-icon">📋</div>
|
||||
<h3>获取文章列表</h3>
|
||||
@@ -44,11 +53,11 @@
|
||||
<button class="btn btn-primary" onclick="showSection('batch')">进入</button>
|
||||
</div>
|
||||
|
||||
<div class="card" id="card-data">
|
||||
<div class="card" id="card-detail">
|
||||
<div class="card-icon">📊</div>
|
||||
<h3>数据管理</h3>
|
||||
<p>查看已下载的文章数据</p>
|
||||
<button class="btn btn-primary" onclick="showSection('data')">进入</button>
|
||||
<h3>获取文章详情</h3>
|
||||
<p>获取文章阅读量、点赞数、评论等详细信息</p>
|
||||
<button class="btn btn-primary" onclick="showSection('detail')">进入</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -70,30 +79,6 @@
|
||||
<div class="result" id="homepage-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- 下载单篇文章区域 -->
|
||||
<div class="section" id="section-single" style="display:none;">
|
||||
<div class="section-header">
|
||||
<h2>📄 下载单篇文章</h2>
|
||||
<button class="btn btn-secondary" onclick="showSection('home')">返回</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>文章链接:</label>
|
||||
<input type="text" id="article-url" placeholder="请输入微信文章链接...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="save-image" checked> 保存图片
|
||||
</label>
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="save-content" checked> 保存内容
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-success" onclick="downloadSingleArticle()">开始下载</button>
|
||||
</div>
|
||||
<div class="result" id="single-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- 获取文章列表区域 -->
|
||||
<div class="section" id="section-list" style="display:none;">
|
||||
<div class="section-header">
|
||||
@@ -140,19 +125,42 @@
|
||||
<div class="result" id="batch-result"></div>
|
||||
</div>
|
||||
|
||||
<!-- 数据管理区域 -->
|
||||
<div class="section" id="section-data" style="display:none;">
|
||||
<!-- 获取文章详情区域 -->
|
||||
<div class="section" id="section-detail" style="display:none;">
|
||||
<div class="section-header">
|
||||
<h2>📊 数据管理</h2>
|
||||
<h2>📊 获取文章详情</h2>
|
||||
<button class="btn btn-secondary" onclick="showSection('home')">返回</button>
|
||||
</div>
|
||||
<div class="help-text">
|
||||
<p><strong>💡 功能说明:</strong></p>
|
||||
<ul>
|
||||
<li>📈 自动获取公众号所有文章列表</li>
|
||||
<li>📊 下载每篇文章的阅读量、点赞数、分享数等统计数据</li>
|
||||
<li>💬 获取文章评论信息(如果有)</li>
|
||||
<li>📁 保存为TXT格式,便于查看和分析</li>
|
||||
</ul>
|
||||
<p><strong>⚠️ 注意事项:</strong></p>
|
||||
<ul>
|
||||
<li>需要提供有效的Access Token URL(包含登录凭证)</li>
|
||||
<li>处理时间较长,请耐心等待</li>
|
||||
<li>数据将保存在 data/公众号名称/文章详细 目录下</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Access Token URL:</label>
|
||||
<textarea id="detail-access-token" placeholder="请粘贴从Fiddler获取的完整URL...\n\n步骤:\n1. 在浏览器中登录微信公众号平台\n2. 访问目标公众号主页\n3. 向下滚动加载文章列表\n4. 在Fiddler中找到profile_ext?action=getmsg请求\n5. 复制完整的URL(包含__biz、uin、key、pass_ticket等参数)" rows="6"></textarea>
|
||||
<small>示例:https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=...&uin=...&key=...&pass_ticket=...</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>获取页数:</label>
|
||||
<input type="number" id="detail-pages" value="0" min="0" max="999" placeholder="留空或0表示获取全部">
|
||||
<small>每页约10篇文章,留空或输入0表示获取全部文章</small>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button class="btn btn-info" onclick="loadDataList()">刷新列表</button>
|
||||
<button class="btn btn-warning" onclick="openDataFolder()">打开数据文件夹</button>
|
||||
</div>
|
||||
<div class="data-list" id="data-list">
|
||||
<p class="text-center">点击"刷新列表"加载数据...</p>
|
||||
<button class="btn btn-success" onclick="getArticleDetail()">开始获取</button>
|
||||
<button class="btn btn-info" onclick="loadDetailExample()">查看示例</button>
|
||||
</div>
|
||||
<div class="result" id="detail-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -163,5 +171,116 @@
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script>
|
||||
// 登录状态管理
|
||||
function checkLoginStatus() {
|
||||
// 检查认证数据
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
const username = auth.user_info && auth.user_info.username ? auth.user_info.username : '用户';
|
||||
showLoggedInState(username);
|
||||
} catch (e) {
|
||||
console.error('解析登录数据失败:', e);
|
||||
showLoggedOutState();
|
||||
}
|
||||
} else {
|
||||
showLoggedOutState();
|
||||
}
|
||||
}
|
||||
|
||||
// 显示已登录状态
|
||||
function showLoggedInState(username) {
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
|
||||
if (loginLink) loginLink.style.display = 'none';
|
||||
if (userMenu) {
|
||||
userMenu.innerHTML = `
|
||||
<span style="margin-right: 10px;">👋 ${username}</span>
|
||||
<button onclick="logout()" style="
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
" onmouseover="this.style.background='rgba(255, 255, 255, 0.3)'"
|
||||
onmouseout="this.style.background='rgba(255, 255, 255, 0.2)'">
|
||||
退出登录
|
||||
</button>
|
||||
`;
|
||||
userMenu.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示未登录状态
|
||||
function showLoggedOutState() {
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
|
||||
if (loginLink) loginLink.style.display = 'inline';
|
||||
if (userMenu) userMenu.innerHTML = '';
|
||||
}
|
||||
|
||||
// 登出功能
|
||||
function logout() {
|
||||
if (confirm('确定要退出登录吗?')) {
|
||||
// 获取认证数据
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
let token = '';
|
||||
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
token = auth.token || '';
|
||||
} catch (e) {
|
||||
console.error('解析token失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 调用后端登出API
|
||||
fetch('http://localhost:8080/api/user/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : ''
|
||||
}
|
||||
}).then(response => {
|
||||
// 无论API调用成功与否,都清除本地数据
|
||||
localStorage.removeItem('authData');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('userSession');
|
||||
|
||||
alert('已成功退出登录!');
|
||||
window.location.href = 'login.html';
|
||||
}).catch(error => {
|
||||
console.error('登出请求失败:', error);
|
||||
// 即使API调用失败,也清除本地数据
|
||||
localStorage.removeItem('authData');
|
||||
localStorage.removeItem('isLoggedIn');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('userSession');
|
||||
|
||||
alert('已成功退出登录!');
|
||||
window.location.href = 'login.html';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
checkLoginStatus();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -112,11 +112,6 @@ function openInNewTab(url) {
|
||||
}
|
||||
|
||||
// 下载单篇文章
|
||||
function downloadSingleArticle() {
|
||||
alert('此功能需要后端命令行支持。\n\n请使用命令行程序:\n1. 运行 wechat-crawler.exe\n2. 选择对应功能进行下载');
|
||||
showResult('single', 'info', '请使用命令行程序执行下载功能');
|
||||
}
|
||||
|
||||
// 获取文章列表
|
||||
function getArticleList() {
|
||||
const accessToken = $('#access-token').val().trim();
|
||||
@@ -197,7 +192,7 @@ function batchDownload() {
|
||||
isTaskRunning = true;
|
||||
showResult('batch', 'loading', '正在批量下载文章,请稍候...');
|
||||
|
||||
// 调用后端API(同步等待)
|
||||
// 调用后端 API(同步等待)
|
||||
$.ajax({
|
||||
url: `${API_BASE_URL}/article/batch`,
|
||||
method: 'POST',
|
||||
@@ -217,9 +212,6 @@ function batchDownload() {
|
||||
<p><strong>公众号:</strong>${data.account}</p>
|
||||
<p><strong>文章数量:</strong>${data.articleCount} 篇</p>
|
||||
<p><strong>保存路径:</strong>${data.path}</p>
|
||||
<div style="margin-top: 15px;">
|
||||
<button class="btn btn-info" onclick="loadDataList()">📊 查看数据列表</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
showResult('batch', 'success', resultHtml);
|
||||
@@ -241,92 +233,190 @@ function batchDownload() {
|
||||
});
|
||||
}
|
||||
|
||||
// 加载数据列表
|
||||
function loadDataList() {
|
||||
showResult('data', 'loading', '正在加载数据列表...');
|
||||
// 获取文章详情(功能4)
|
||||
function getArticleDetail() {
|
||||
const accessToken = $('#detail-access-token').val().trim();
|
||||
const pages = parseInt($('#detail-pages').val()) || 0;
|
||||
const submitBtn = $('#section-detail .btn-success');
|
||||
|
||||
// 调用后端API
|
||||
// 1. 验证输入
|
||||
if (!accessToken) {
|
||||
showResult('detail', 'error', '⚠️ 请输入Access Token URL');
|
||||
// 高亮输入框
|
||||
$('#detail-access-token').css('border-color', '#e74c3c').focus();
|
||||
setTimeout(() => {
|
||||
$('#detail-access-token').css('border-color', '');
|
||||
}, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 检查是否有任务正在执行
|
||||
if (isTaskRunning) {
|
||||
showResult('detail', 'error', '⚠️ 有任务正在执行,请稍候...');
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 设置任务状态并禁用按钮
|
||||
isTaskRunning = true;
|
||||
submitBtn.prop('disabled', true)
|
||||
.addClass('disabled')
|
||||
.html('⏳ 处理中...');
|
||||
|
||||
// 4. 构建提示信息
|
||||
const pagesInfo = pages > 0 ? `前${pages}页(约${pages * 10}篇文章)` : '全部文章';
|
||||
|
||||
// 5. 显示加载状态
|
||||
showResult('detail', 'loading', `
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 1.2em; margin-bottom: 10px;">⏳ 正在获取文章详情,请耐心等待...</div>
|
||||
<div style="background: #e3f2fd; padding: 12px; border-radius: 5px; margin: 15px 0; border-left: 4px solid #2196f3;">
|
||||
<p style="margin: 5px 0; color: #1565c0;"><strong>📌 本次获取范围:</strong>${pagesInfo}</p>
|
||||
</div>
|
||||
<div style="background: #fff3cd; padding: 12px; border-radius: 5px; margin: 15px 0;">
|
||||
<p style="margin: 5px 0; color: #856404;"><strong>📝 执行步骤:</strong></p>
|
||||
<ol style="text-align: left; margin: 10px 0; padding-left: 30px; color: #856404;">
|
||||
<li>正在获取公众号文章列表...</li>
|
||||
<li>正在下载每篇文章的详细数据...</li>
|
||||
<li>正在保存为TXT文件...</li>
|
||||
</ol>
|
||||
<p style="margin: 10px 0 0 0; color: #d9534f;"><strong>⚠️ 此过程可能需要几分钟,系统会自动延时避免被封禁</strong></p>
|
||||
<p style="margin: 5px 0 0 0; color: #856404; font-size: 0.9em;">💡 提示:请不要关闭页面或刷新浏览器</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
console.log('🚀 开始获取文章详情...');
|
||||
console.log('📄 获取页数:', pages === 0 ? '全部' : pages);
|
||||
|
||||
// 6. 调用后端 API
|
||||
$.ajax({
|
||||
url: `${API_BASE_URL}/data/list`,
|
||||
method: 'GET',
|
||||
url: `${API_BASE_URL}/article/detail`,
|
||||
method: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({
|
||||
access_token: accessToken,
|
||||
pages: pages
|
||||
}),
|
||||
beforeSend: function() {
|
||||
console.log('📡 发送请求到后端 API...');
|
||||
},
|
||||
success: function(response) {
|
||||
console.log('✅ 收到服务器响应:', response);
|
||||
|
||||
// 恢复任务状态和按钮
|
||||
isTaskRunning = false;
|
||||
submitBtn.prop('disabled', false)
|
||||
.removeClass('disabled')
|
||||
.html('开始获取');
|
||||
|
||||
if (response.success && response.data) {
|
||||
displayDataList(response.data);
|
||||
hideResult('data');
|
||||
const data = response.data;
|
||||
const resultHtml = `
|
||||
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-top: 10px; border: 2px solid #28a745;">
|
||||
<h4 style="color: #28a745; margin-bottom: 15px; font-size: 1.3em;">✅ 获取成功!</h4>
|
||||
<div style="background: white; padding: 15px; border-radius: 5px; margin: 10px 0;">
|
||||
<p style="margin: 8px 0;"><strong>📱 公众号:</strong><span style="color: #667eea;">${data.account}</span></p>
|
||||
<p style="margin: 8px 0;"><strong>📊 文章数量:</strong><span style="color: #667eea;">${data.articleCount} 篇</span></p>
|
||||
<p style="margin: 8px 0;"><strong>📁 保存路径:</strong><span style="color: #667eea; font-size: 0.9em;">${data.path}</span></p>
|
||||
</div>
|
||||
<div style="background: #e7f3ff; padding: 15px; border-radius: 5px; margin: 15px 0; border-left: 4px solid #3498db;">
|
||||
<p style="margin: 5px 0; color: #0066cc; font-weight: bold;">📊 数据包含:</p>
|
||||
<ul style="margin: 10px 0; padding-left: 25px; color: #0066cc;">
|
||||
<li style="margin: 5px 0;">📄 文章文本内容</li>
|
||||
<li style="margin: 5px 0;">👁️ 阅读量、点赞数、转发数、在看数</li>
|
||||
<li style="margin: 5px 0;">💬 评论内容及评论点赞数</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p style="margin-top: 15px; text-align: center; color: #7f8c8d; font-size: 0.9em;">💡 数据已保存在 data/${data.account}/文章详细 目录下</p>
|
||||
</div>
|
||||
`;
|
||||
showResult('detail', 'success', resultHtml);
|
||||
} else {
|
||||
showResult('data', 'error', '加载失败');
|
||||
showResult('detail', 'error', `
|
||||
<div style="padding: 10px;">
|
||||
<h4 style="color: #e74c3c; margin-bottom: 10px;">❌ 获取失败</h4>
|
||||
<p style="margin: 10px 0;">${response.message || '未知错误,请稍后重试'}</p>
|
||||
<div style="background: #fff3cd; padding: 12px; border-radius: 5px; margin-top: 15px;">
|
||||
<p style="margin: 5px 0; color: #856404;"><strong>💡 可能的原因:</strong></p>
|
||||
<ul style="margin: 10px 0; padding-left: 25px; color: #856404;">
|
||||
<li>Access Token 已过期</li>
|
||||
<li>公众号权限不足</li>
|
||||
<li>网络连接问题</li>
|
||||
</ul>
|
||||
<p style="margin: 10px 0 0 0; color: #856404;">请重新从 Fiddler 获取 Access Token URL 并重试</p>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
let errorMsg = '请求失败:' + error;
|
||||
console.error('❌ 请求失败:', { xhr, status, error });
|
||||
|
||||
// 恢复任务状态和按钮
|
||||
isTaskRunning = false;
|
||||
submitBtn.prop('disabled', false)
|
||||
.removeClass('disabled')
|
||||
.html('开始获取');
|
||||
|
||||
let errorMsg = '请求失败';
|
||||
let errorDetail = '';
|
||||
|
||||
if (xhr.status === 0) {
|
||||
errorMsg = '无法连接到后端服务器,请确保 API 服务器已启动';
|
||||
}
|
||||
showResult('data', 'error', errorMsg);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示数据列表
|
||||
function displayDataList(dataList) {
|
||||
let html = '';
|
||||
|
||||
if (!dataList || dataList.length === 0) {
|
||||
html = '<p class="text-center" style="padding: 20px; color: #666;">暂无数据,请先使用其他功能爬取文章</p>';
|
||||
} else {
|
||||
dataList.forEach(item => {
|
||||
const safeItemName = (item.name || '').replace(/'/g, "\\'");
|
||||
const safeItemPath = (item.path || '').replace(/'/g, "\\'").replace(/\\/g, '\\\\');
|
||||
html += `
|
||||
<div class="data-item">
|
||||
<h4><span class="status-indicator status-success"></span>${item.name || '未知'}</h4>
|
||||
<div class="data-item-info">
|
||||
<div class="data-item-stats">
|
||||
<div class="stat-item">
|
||||
<span>📊</span>
|
||||
<span>${item.articleCount || 0} 篇文章</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>📅</span>
|
||||
<span>${item.lastUpdate || '未知'}</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span>📁</span>
|
||||
<span>${item.path || '未知'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-item-actions">
|
||||
<button class="btn btn-info" onclick="viewArticles('${safeItemName}')">查看文章</button>
|
||||
<button class="btn btn-warning" onclick="alert('文件夹路径:${safeItemPath}')">查看路径</button>
|
||||
</div>
|
||||
</div>
|
||||
errorMsg = '无法连接到后端服务器';
|
||||
errorDetail = `
|
||||
<div style="background: #f8d7da; padding: 12px; border-radius: 5px; margin-top: 15px; border-left: 4px solid #e74c3c;">
|
||||
<p style="margin: 5px 0; color: #721c24;"><strong>🔧 解决方法:</strong></p>
|
||||
<ol style="margin: 10px 0; padding-left: 25px; color: #721c24;">
|
||||
<li>确认后端 API 服务器已启动</li>
|
||||
<li>检查服务器地址:http://localhost:8080</li>
|
||||
<li>查看服务器控制台是否有错误信息</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else if (xhr.status === 404) {
|
||||
errorMsg = '接口不存在';
|
||||
errorDetail = `<p style="margin-top: 10px; color: #721c24;">请检查API路径配置是否正确</p>`;
|
||||
} else if (xhr.status === 500) {
|
||||
errorMsg = '服务器内部错误';
|
||||
errorDetail = `<p style="margin-top: 10px; color: #721c24;">请查看服务器日志获取详细错误信息</p>`;
|
||||
} else if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
errorMsg = xhr.responseJSON.message;
|
||||
} else if (status === 'timeout') {
|
||||
errorMsg = '请求超时';
|
||||
errorDetail = `
|
||||
<p style="margin-top: 10px; color: #721c24;">
|
||||
文章数量较多,处理时间超过10分钟。<br>
|
||||
建议:减少文章数量或稍后查看服务器是否已生成文件。
|
||||
</p>
|
||||
`;
|
||||
} else {
|
||||
errorMsg += ': ' + (error || '未知错误');
|
||||
}
|
||||
|
||||
$('#data-list').html(html);
|
||||
showResult('detail', 'error', `
|
||||
<div style="padding: 10px;">
|
||||
<h4 style="color: #e74c3c; margin-bottom: 10px;">❌ ${errorMsg}</h4>
|
||||
${errorDetail}
|
||||
<p style="margin-top: 15px; color: #7f8c8d; font-size: 0.9em;">
|
||||
错误状态码: ${xhr.status || 'N/A'}<br>
|
||||
错误类型: ${status || 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
complete: function() {
|
||||
console.log('🏁 请求完成');
|
||||
},
|
||||
timeout: 600000 // 10分钟超时(因为功能4需要较长时间)
|
||||
});
|
||||
}
|
||||
|
||||
// 查看文章列表
|
||||
function viewArticles(accountName) {
|
||||
alert(`查看 ${accountName} 的文章列表
|
||||
// 加载示例 Access Token
|
||||
function loadDetailExample() {
|
||||
const exampleToken = 'https://mp.weixin.qq.com/mp/profile_ext?action=getmsg&__biz=MzI1NjEwMTM4OA==&uin=MTIzNDU2Nzg5&key=abcdef123456&pass_ticket=xyz789&...';
|
||||
|
||||
这里将展示该公众号的所有文章,包括:
|
||||
- 文章标题
|
||||
- 发布时间
|
||||
- 文件大小
|
||||
- 下载状态等`);
|
||||
}
|
||||
|
||||
// 打开文件夹
|
||||
function openFolder(path) {
|
||||
alert(`打开文件夹: ${path}\n\n在实际环境中,这里会调用系统命令打开文件资源管理器。`);
|
||||
}
|
||||
|
||||
// 打开数据文件夹
|
||||
function openDataFolder() {
|
||||
alert('打开数据文件夹\n\n在实际环境中,这里会打开data目录。');
|
||||
$('#detail-access-token').val(exampleToken);
|
||||
showResult('detail', 'info', '已加载 Access Token 示例。<br><br><strong>注意:</strong>请从 Fiddler 中获取实际的 Access Token URL,示例仅供参考。<br><br><strong>获取步骤:</strong><br>1. 在微信客户端打开公众号主页<br>2. 在 Fiddler 中查找 URL 为 /mp/profile_ext?action=getmsg 的请求<br>3. 按 Ctrl+U 复制完整 URL<br>4. 粘贴到此处');
|
||||
}
|
||||
|
||||
// 任务管理函数
|
||||
@@ -470,6 +560,9 @@ $(document).keydown(function(e) {
|
||||
case 'batch':
|
||||
batchDownload();
|
||||
break;
|
||||
case 'detail':
|
||||
getArticleDetail();
|
||||
break;
|
||||
case 'data':
|
||||
loadDataList();
|
||||
break;
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
};
|
||||
|
||||
// 调用后端登录API
|
||||
fetch('http://localhost:8000/api/user/login', {
|
||||
fetch('http://localhost:8080/api/user/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -576,17 +576,18 @@
|
||||
body: JSON.stringify(loginData)
|
||||
})
|
||||
.then(response => {
|
||||
console.log('✅ 收到响应,状态码:', response.status);
|
||||
if (!response.ok) {
|
||||
// 处理HTTP错误状态码
|
||||
if (response.status === 500) {
|
||||
throw new Error('服务器内部错误');
|
||||
} else if (response.status === 401) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.msg || '用户名或密码错误');
|
||||
throw new Error(data.message || '用户名或密码错误');
|
||||
});
|
||||
} else if (response.status === 400) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.msg || '请求参数错误');
|
||||
throw new Error(data.message || '请求参数错误');
|
||||
});
|
||||
} else {
|
||||
throw new Error('网络响应异常');
|
||||
@@ -595,13 +596,14 @@
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('✅ 收到数据:', data);
|
||||
// 检查响应状态
|
||||
if (data.code === 200) {
|
||||
if (data.success || data.code === 200) {
|
||||
// 登录成功,存储认证信息
|
||||
const authData = {
|
||||
token: data.token,
|
||||
user_id: data.user_id,
|
||||
user_info: data.user_info,
|
||||
token: data.data.token,
|
||||
user_id: data.data.user_id,
|
||||
user_info: data.data.user_info,
|
||||
loginTime: new Date().toISOString(),
|
||||
remember: remember
|
||||
};
|
||||
@@ -635,19 +637,19 @@
|
||||
// 根据错误码显示不同的错误信息
|
||||
switch(data.code) {
|
||||
case 401:
|
||||
errorMessage = data.msg || '用户名或密码错误';
|
||||
errorMessage = data.message || '用户名或密码错误';
|
||||
break;
|
||||
case 400:
|
||||
errorMessage = data.msg || '请求参数错误,请检查输入';
|
||||
errorMessage = data.message || '请求参数错误,请检查输入';
|
||||
break;
|
||||
case 404:
|
||||
errorMessage = data.msg || '账号不存在';
|
||||
errorMessage = data.message || '账号不存在';
|
||||
break;
|
||||
case 500:
|
||||
errorMessage = data.msg || '服务器内部错误,请稍后再试';
|
||||
errorMessage = data.message || '服务器内部错误,请稍后再试';
|
||||
break;
|
||||
default:
|
||||
errorMessage = data.msg || '登录失败,请检查用户名和密码';
|
||||
errorMessage = data.message || '登录失败,请检查用户名和密码';
|
||||
}
|
||||
|
||||
showError(errorMessage);
|
||||
@@ -657,12 +659,17 @@
|
||||
})
|
||||
.catch(error => {
|
||||
// 捕获网络或其他错误
|
||||
console.error('登录请求失败:', error);
|
||||
console.error('❌ 登录请求失败:', error);
|
||||
console.error('错误类型:', error.name);
|
||||
console.error('错误消息:', error.message);
|
||||
|
||||
// 根据错误信息显示具体的错误提示
|
||||
let errorMessage = '登录失败,请稍后再试';
|
||||
|
||||
if (error.message.includes('账号不存在')) {
|
||||
// 检查是否是网络错误
|
||||
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
|
||||
errorMessage = '无法连接到服务器,请确保:\n1. API服务器正在运行 (http://localhost:8080)\n2. 检查网络连接';
|
||||
} else if (error.message.includes('账号不存在')) {
|
||||
errorMessage = '账号不存在,请先注册';
|
||||
} else if (error.message.includes('密码错误')) {
|
||||
errorMessage = '密码错误,请重新输入';
|
||||
@@ -701,7 +708,10 @@
|
||||
window.addEventListener('load', function() {
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
// 检查user_info是否存在且有username
|
||||
if (auth.user_info && auth.user_info.username) {
|
||||
showSuccess(`欢迎回来,${auth.user_info.username}!`);
|
||||
|
||||
// 如果已经登录,可以选择直接跳转
|
||||
@@ -711,6 +721,13 @@
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('解析登录数据失败:', e);
|
||||
// 清除无效的登录数据
|
||||
localStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('authData');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Enter键提交表单
|
||||
|
||||
@@ -639,7 +639,7 @@
|
||||
};
|
||||
|
||||
// 调用后端注册API
|
||||
fetch('http://localhost:8000/api/user/register', {
|
||||
fetch('http://localhost:8080/api/user/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -654,7 +654,7 @@
|
||||
})
|
||||
.then(data => {
|
||||
// 检查响应状态
|
||||
if (data.code === 200) {
|
||||
if (data.success || data.code === 200) {
|
||||
// 注册成功
|
||||
showSuccess('注册成功!正在跳转到登录页面...');
|
||||
|
||||
@@ -664,7 +664,7 @@
|
||||
}, 1500);
|
||||
} else {
|
||||
// 注册失败,显示错误信息
|
||||
showError(data.msg || '注册失败,请稍后重试');
|
||||
showError(data.message || '注册失败,请稍后重试');
|
||||
registerBtn.textContent = '注册账号';
|
||||
registerBtn.disabled = false;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,9 @@ echo.
|
||||
echo 💡 提示: 按 Ctrl+C 停止服务器
|
||||
echo ===============================================
|
||||
echo.
|
||||
echo 🌐 正在打开浏览器...
|
||||
start http://localhost:8000/frontend.html
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
python -m http.server 8000
|
||||
@@ -48,7 +51,7 @@ $listener.Prefixes.Add('http://localhost:8080/')
|
||||
$listener.Start()
|
||||
Write-Host '✅ Web服务器已启动: http://localhost:8080'
|
||||
Write-Host '🌐 正在打开浏览器...'
|
||||
Start-Process 'http://localhost:8080'
|
||||
Start-Process 'http://localhost:8080/frontend.html'
|
||||
|
||||
while ($listener.IsListening) {
|
||||
$context = $listener.GetContext()
|
||||
@@ -56,7 +59,7 @@ while ($listener.IsListening) {
|
||||
$response = $context.Response
|
||||
|
||||
$path = $request.Url.LocalPath
|
||||
if ($path -eq '/') { $path = '/index.html' }
|
||||
if ($path -eq '/') { $path = '/frontend.html' }
|
||||
|
||||
$filePath = Join-Path (Get-Location) $path.TrimStart('/')
|
||||
|
||||
|
||||
@@ -397,14 +397,15 @@
|
||||
<!-- 头部导航 -->
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<a href="frontend.html" class="logo">易搜高</a>
|
||||
<a href="frontend.html" class="logo">🔍 易搜高</a>
|
||||
<nav class="nav-menu">
|
||||
<a href="frontend.html">首页监控</a>
|
||||
<a href="history-articles.html">历史文章</a>
|
||||
<a href="user-center.html" class="active">用户中心</a>
|
||||
<!-- 用户信息和登出按钮会通过JavaScript动态添加 -->
|
||||
<div id="userMenu"></div>
|
||||
<a href="login.html" id="loginLink" class="login-link">登录/注册</a>
|
||||
<a href="frontend.html" class="nav-link">首页</a>
|
||||
<a href="#" class="nav-link">监控中心</a>
|
||||
<a href="#" class="nav-link">数据分析</a>
|
||||
<a href="#" class="nav-link">帮助文档</a>
|
||||
<a href="user-center.html" class="nav-link active" id="userCenterLink">👤 用户中心</a>
|
||||
<a href="login.html" class="nav-link" id="loginLink">🔐 登录</a>
|
||||
<div id="userMenu" class="user-info"></div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
@@ -441,7 +442,6 @@
|
||||
</h2>
|
||||
|
||||
<div class="info-card">
|
||||
<!-- 修复个人信息区域中重复的ID -->
|
||||
<div class="info-row">
|
||||
<span class="info-label">用户名</span>
|
||||
<span class="info-value" id="info-username">--</span>
|
||||
@@ -465,6 +465,23 @@
|
||||
<span class="info-value" id="info-status">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑个人信息表单 -->
|
||||
<h3 class="section-title" style="font-size: 18px; margin-top: 30px;">
|
||||
<span class="section-icon">✏️</span>
|
||||
编辑信息
|
||||
</h3>
|
||||
<form id="edit-info-form" onsubmit="return saveUserInfo(event)">
|
||||
<div class="form-group">
|
||||
<label for="edit-email" class="form-label">邮箱</label>
|
||||
<input type="email" id="edit-email" class="form-input" placeholder="请输入邮箱">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-bio" class="form-label">个人简介</label>
|
||||
<textarea id="edit-bio" class="form-input" rows="4" placeholder="介绍一下自己..."></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">保存修改</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- 密码修改部分 -->
|
||||
@@ -742,6 +759,7 @@
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// 添加缺失的函数定义,确保功能完整性
|
||||
function showMessage(message, type = 'info') {
|
||||
const messageElement = document.getElementById('message');
|
||||
@@ -759,12 +777,17 @@
|
||||
|
||||
function checkLoginStatus() {
|
||||
// 检查本地存储中的认证信息
|
||||
const authToken = localStorage.getItem('authToken') || localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
const username = localStorage.getItem('username') || localStorage.getItem('nickname');
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
const username = auth.user_info && auth.user_info.username ? auth.user_info.username : '用户';
|
||||
|
||||
if (authToken && username) {
|
||||
// 用户已登录,更新UI
|
||||
document.getElementById('loginLink').style.display = 'none';
|
||||
const loginLink = document.getElementById('loginLink');
|
||||
if (loginLink) loginLink.style.display = 'none';
|
||||
|
||||
const userMenu = document.getElementById('userMenu');
|
||||
if (userMenu) {
|
||||
userMenu.innerHTML = `
|
||||
@@ -776,27 +799,42 @@
|
||||
}
|
||||
|
||||
// 加载用户信息
|
||||
loadUserData();
|
||||
loadUserData(auth);
|
||||
} catch (e) {
|
||||
console.error('解析登录数据失败:', e);
|
||||
window.location.href = 'login.html';
|
||||
}
|
||||
} else {
|
||||
// 用户未登录,跳转到登录页面
|
||||
window.location.href = 'login.html';
|
||||
window.location.href = 'login.html?redirect=' + encodeURIComponent(window.location.href);
|
||||
}
|
||||
}
|
||||
|
||||
function logout() {
|
||||
// 清除本地存储的认证信息
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('userSettings');
|
||||
localStorage.removeItem('lastLogin');
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
let token = '';
|
||||
|
||||
// 尝试调用登出API(使用fetch而不是apiCall,避免依赖本地函数)
|
||||
fetch('http://localhost:8000/api/user/logout', {
|
||||
if (authData) {
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
token = auth.token;
|
||||
} catch (e) {
|
||||
console.error('解析token失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 清除本地存储的认证信息
|
||||
localStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('authData');
|
||||
|
||||
// 调用登出API
|
||||
fetch('http://localhost:8080/api/user/logout', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
credentials: 'include'
|
||||
body: JSON.stringify({ token: token })
|
||||
}).finally(() => {
|
||||
// 无论API调用成功与否,都跳转到登录页面
|
||||
window.location.href = 'login.html';
|
||||
@@ -804,65 +842,54 @@
|
||||
}
|
||||
|
||||
function clearAuthData() {
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('userSettings');
|
||||
localStorage.removeItem('authData');
|
||||
sessionStorage.removeItem('authData');
|
||||
}
|
||||
|
||||
function loadUserData() {
|
||||
// 显示加载状态
|
||||
showLoading(true);
|
||||
function loadUserData(authData) {
|
||||
console.log('🔍 加载用户数据:', authData);
|
||||
|
||||
// 获取认证token
|
||||
const authToken = localStorage.getItem('authToken') || localStorage.getItem('token') || sessionStorage.getItem('token');
|
||||
|
||||
// 调用后端API获取用户信息
|
||||
fetch('http://localhost:8000/api/user/info', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
},
|
||||
credentials: 'include'
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// 认证失败,清除认证信息并重定向到登录页面
|
||||
clearAuthData();
|
||||
if (!authData || !authData.user_info) {
|
||||
console.error('❌ 无效的认证数据');
|
||||
window.location.href = 'login.html';
|
||||
throw new Error('认证失败,请重新登录');
|
||||
return;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
||||
// 从authData中提取用户信息
|
||||
const userInfo = authData.user_info;
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '--';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data && data.data) {
|
||||
// 使用后端返回的用户数据
|
||||
const userData = data.data;
|
||||
};
|
||||
|
||||
// 准备显示的用户数据
|
||||
const userData = {
|
||||
username: userInfo.username || '--',
|
||||
email: userInfo.email || '--',
|
||||
joinDate: formatDate(userInfo.created_at),
|
||||
lastLogin: formatDate(userInfo.last_login_at),
|
||||
status: userInfo.status === 1 ? 'active' : 'inactive',
|
||||
bio: userInfo.bio || ''
|
||||
};
|
||||
|
||||
console.log('✅ 格式化后的用户数据:', userData);
|
||||
|
||||
// 更新用户信息显示
|
||||
updateUserInfoDisplay(userData);
|
||||
}
|
||||
hideLoading();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载用户数据失败:', error);
|
||||
hideLoading();
|
||||
// 如果API调用失败,使用模拟数据
|
||||
const username = localStorage.getItem('username') || '测试用户';
|
||||
const userData = {
|
||||
username: username,
|
||||
email: 'test@example.com',
|
||||
nickname: username,
|
||||
role: '普通用户',
|
||||
joinDate: new Date().toLocaleDateString('zh-CN'),
|
||||
lastLogin: new Date().toLocaleString('zh-CN')
|
||||
};
|
||||
updateUserInfoDisplay(userData);
|
||||
});
|
||||
}
|
||||
|
||||
// 增强版API调用包装函数
|
||||
function apiCall(endpoint, method = 'GET', data = null, requiresAuth = true) {
|
||||
@@ -1195,6 +1222,77 @@
|
||||
|
||||
// 保存用户名用于账号注销确认
|
||||
document.getElementById('username-display').textContent = userData.username || '';
|
||||
|
||||
// 填充编辑表单
|
||||
const emailInput = document.getElementById('edit-email');
|
||||
const bioInput = document.getElementById('edit-bio');
|
||||
if (emailInput) emailInput.value = userData.email || '';
|
||||
if (bioInput) bioInput.value = userData.bio || '';
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
function saveUserInfo(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const email = document.getElementById('edit-email').value;
|
||||
const bio = document.getElementById('edit-bio').value;
|
||||
|
||||
// 验证邮箱
|
||||
if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
showMessage('请输入有效的邮箱地址', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 获取认证信息
|
||||
const authData = localStorage.getItem('authData') || sessionStorage.getItem('authData');
|
||||
if (!authData) {
|
||||
showMessage('未登录,请先登录', 'error');
|
||||
setTimeout(() => {
|
||||
window.location.href = 'login.html';
|
||||
}, 1500);
|
||||
return false;
|
||||
}
|
||||
|
||||
let token, userId;
|
||||
try {
|
||||
const auth = JSON.parse(authData);
|
||||
token = auth.token;
|
||||
userId = auth.user_id;
|
||||
} catch (e) {
|
||||
showMessage('认证信息错误', 'error');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 调用API更新用户信息
|
||||
fetch('http://localhost:8080/api/user/update', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
email: email,
|
||||
bio: bio
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('✅ 更新响应:', data);
|
||||
if (data.success) {
|
||||
showMessage('信息保存成功!', 'success');
|
||||
// 更新显示的邮箱
|
||||
document.getElementById('info-email').textContent = email;
|
||||
} else {
|
||||
showMessage(data.message || '保存失败', 'error');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('❌ 保存错误:', error);
|
||||
showMessage('网络错误,请稍后再试', 'error');
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
@@ -1624,8 +1722,9 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
// 添加消息提示的CSS样式
|
||||
<style>
|
||||
/* 消息提示样式 */
|
||||
.message-toast {
|
||||
position: fixed;
|
||||
@@ -1750,4 +1849,7 @@
|
||||
|
||||
.password-match-message.mismatch {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
}</style>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,178 +0,0 @@
|
||||
# 🔧 前端功能问题说明和解决方案
|
||||
|
||||
## ❌ 当前问题
|
||||
|
||||
前端的所有功能(除了"提取公众号主页")都**无法正常工作**,原因如下:
|
||||
|
||||
### 问题1:前端是纯模拟,未调用真实后端
|
||||
当前前端代码中的所有下载功能都是**模拟执行**:
|
||||
```javascript
|
||||
// 这只是模拟,没有真正下载
|
||||
const progressInterval = setInterval(() => {
|
||||
progress += Math.random() * 20;
|
||||
if (progress >= 100) {
|
||||
endTask('single', 'success', '文章下载完成!'); // 假的成功提示
|
||||
}
|
||||
}, 800);
|
||||
```
|
||||
|
||||
### 问题2:浏览器无法直接执行本地程序
|
||||
Web前端在浏览器中运行,出于安全限制,**无法直接调用本地的exe程序**。
|
||||
|
||||
## ✅ 解决方案
|
||||
|
||||
需要搭建一个**HTTP API服务器**作为桥梁,连接前端和后端程序。
|
||||
|
||||
### 方案架构
|
||||
```
|
||||
前端网页 (浏览器)
|
||||
↓ HTTP请求
|
||||
API服务器 (Go/Node.js)
|
||||
↓ 执行命令
|
||||
后端爬虫程序 (wechat-crawler.exe)
|
||||
```
|
||||
|
||||
## 🚀 实施步骤
|
||||
|
||||
### 步骤1:已创建API服务器代码
|
||||
|
||||
文件:`backend/api/server.go`
|
||||
|
||||
主要功能:
|
||||
- ✅ 提取公众号主页 (`/api/homepage/extract`)
|
||||
- ⏳ 下载单篇文章 (`/api/article/download`)
|
||||
- ⏳ 获取文章列表 (`/api/article/list`)
|
||||
- ⏳ 批量下载 (`/api/article/batch`)
|
||||
- ✅ 获取数据列表 (`/api/data/list`)
|
||||
|
||||
### 步骤2:编译API服务器
|
||||
|
||||
```bash
|
||||
cd d:\workspace\Access_wechat_article\backend\api
|
||||
go build -o api_server.exe server.go
|
||||
```
|
||||
|
||||
### 步骤3:启动API服务器
|
||||
|
||||
```bash
|
||||
cd d:\workspace\Access_wechat_article\backend
|
||||
.\api\api_server.exe
|
||||
```
|
||||
|
||||
服务器将运行在 `http://localhost:8080`
|
||||
|
||||
### 步骤4:修复前端代码
|
||||
|
||||
前端`js/app.js`文件被意外破坏,需要修复第68行的代码错误。
|
||||
|
||||
**问题代码**(第68行):
|
||||
```javascript
|
||||
<button class="btn btn-info" onclick="copyToClipboard('${homepageUrl.replace(/'/g, "\\'")}')"入下载功能中。`
|
||||
```
|
||||
|
||||
**应该是**:
|
||||
```javascript
|
||||
<button class="btn btn-info" onclick="copyToClipboard('${homepageUrl.replace(/'/g, "\\'")}')">📋 复制链接</button>
|
||||
<button class="btn btn-warning" onclick="openInNewTab('${homepageUrl}')">🔗 打开主页</button>
|
||||
```
|
||||
|
||||
## 📋 当前可用功能
|
||||
|
||||
### ✅ 已实现功能
|
||||
1. **提取公众号主页** - 通过API服务器调用后端程序
|
||||
|
||||
### ⏳ 需要完善的功能
|
||||
2. **下载单篇文章** - 需要后端添加对应的命令行接口
|
||||
3. **获取文章列表** - 需要后端添加对应的命令行接口
|
||||
4. **批量下载** - 可使用现有的功能5
|
||||
5. **数据管理** - 已有API,前端需要调用
|
||||
|
||||
## 🔨 完整解决方案
|
||||
|
||||
由于问题比较复杂,建议采用以下简化方案:
|
||||
|
||||
### 方案A:命令行方式(推荐)
|
||||
**优点**:
|
||||
- 简单直接,无需额外开发
|
||||
- 稳定可靠
|
||||
- 功能完整
|
||||
|
||||
**使用方法**:
|
||||
```bash
|
||||
# 直接运行后端程序
|
||||
cd backend
|
||||
.\wechat-crawler.exe
|
||||
|
||||
# 按菜单选择功能
|
||||
数字键1:提取公众号主页
|
||||
数字键3:获取文章列表
|
||||
数字键5:批量下载文章
|
||||
```
|
||||
|
||||
### 方案B:Web界面(需要修复)
|
||||
**需要完成的工作**:
|
||||
1. ✅ API服务器已创建
|
||||
2. ❌ 前端JS代码需要修复
|
||||
3. ❌ 后端需要添加更多命令行接口
|
||||
4. ❌ 前端需要修改为调用真实API
|
||||
|
||||
**工作量**:约2-3小时开发时间
|
||||
|
||||
## 💡 临时解决方案
|
||||
|
||||
在API服务器和前端代码完全修复之前,建议:
|
||||
|
||||
### 1. 使用命令行程序
|
||||
```bash
|
||||
cd d:\workspace\Access_wechat_article\backend
|
||||
.\wechat-crawler.exe
|
||||
```
|
||||
|
||||
### 2. 只使用"提取公众号主页"功能
|
||||
这个功能已经可以正常工作(通过API服务器)
|
||||
|
||||
### 3. 其他功能直接在命令行执行
|
||||
- 功能3:获取文章列表
|
||||
- 功能5:批量下载文章
|
||||
|
||||
## 📊 功能对比
|
||||
|
||||
| 功能 | 命令行 | Web界面 | 状态 |
|
||||
|------|--------|---------|------|
|
||||
| 提取公众号主页 | ✅ | ✅ | 可用 |
|
||||
| 获取文章列表 | ✅ | ❌ | 仅命令行 |
|
||||
| 批量下载文章 | ✅ | ❌ | 仅命令行 |
|
||||
| 数据查看 | ✅ | ⏳ | 需修复 |
|
||||
|
||||
## 🎯 下一步建议
|
||||
|
||||
### 选项1:继续使用命令行(推荐)
|
||||
- 功能完整且稳定
|
||||
- 无需额外开发
|
||||
- 立即可用
|
||||
|
||||
### 选项2:完善Web界面
|
||||
需要完成:
|
||||
1. 修复前端JS代码错误
|
||||
2. 实现完整的API调用逻辑
|
||||
3. 测试所有功能
|
||||
|
||||
**预计时间**:2-3小时
|
||||
|
||||
## 🔍 错误定位
|
||||
|
||||
当前前端代码的主要问题在:
|
||||
- 文件:`frontend/js/app.js`
|
||||
- 行号:第68行
|
||||
- 问题:字符串拼接错误,导致语法错误
|
||||
|
||||
## 📞 技术支持
|
||||
|
||||
如需完善Web界面,建议:
|
||||
1. 先修复`app.js`第68行的语法错误
|
||||
2. 测试API服务器是否正常运行
|
||||
3. 逐个功能进行调试和完善
|
||||
|
||||
---
|
||||
|
||||
**当前状态**:建议优先使用命令行程序,功能完整且稳定。Web界面可作为未来优化项目。
|
||||
Reference in New Issue
Block a user