From b7b09251f8cf30b41e1797127e5302cd25d95cf8 Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Sun, 12 Jul 2026 01:07:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E5=8F=82=E6=95=B0?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E8=87=AA=E5=8A=A8=E5=8C=96=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 功能: - 文章内容库管理 - 待处理产品列表管理 - 自动处理流程 - 智能搜索和数据提取 - ParamHub API集成 - 定时任务调度 部署端口: 16043 --- README.md | 270 ++++++++++++ __pycache__/config.cpython-310.pyc | Bin 0 -> 753 bytes app.pid | 1 + app.py | 80 ++++ config.py | 28 ++ data/param_auto.db | Bin 0 -> 49152 bytes logs/app.log | 34 ++ models/__pycache__/database.cpython-310.pyc | Bin 0 -> 12929 bytes models/database.py | 313 +++++++++++++ requirements.txt | 8 + routes/__pycache__/articles.cpython-310.pyc | Bin 0 -> 3273 bytes routes/__pycache__/products.cpython-310.pyc | Bin 0 -> 4778 bytes routes/__pycache__/system.cpython-310.pyc | Bin 0 -> 2194 bytes routes/articles.py | 135 ++++++ routes/products.py | 216 +++++++++ routes/system.py | 68 +++ .../paramhub_client.cpython-310.pyc | Bin 0 -> 4043 bytes .../process_service.cpython-310.pyc | Bin 0 -> 8602 bytes .../search_service.cpython-310.pyc | Bin 0 -> 2890 bytes services/paramhub_client.py | 132 ++++++ services/process_service.py | 412 ++++++++++++++++++ services/search_service.py | 99 +++++ start.sh | 35 ++ stop.sh | 38 ++ utils/__pycache__/scheduler.cpython-310.pyc | Bin 0 -> 4243 bytes utils/scheduler.py | 127 ++++++ 26 files changed, 1996 insertions(+) create mode 100644 README.md create mode 100644 __pycache__/config.cpython-310.pyc create mode 100644 app.pid create mode 100644 app.py create mode 100644 config.py create mode 100644 data/param_auto.db create mode 100644 logs/app.log create mode 100644 models/__pycache__/database.cpython-310.pyc create mode 100644 models/database.py create mode 100644 requirements.txt create mode 100644 routes/__pycache__/articles.cpython-310.pyc create mode 100644 routes/__pycache__/products.cpython-310.pyc create mode 100644 routes/__pycache__/system.cpython-310.pyc create mode 100644 routes/articles.py create mode 100644 routes/products.py create mode 100644 routes/system.py create mode 100644 services/__pycache__/paramhub_client.cpython-310.pyc create mode 100644 services/__pycache__/process_service.cpython-310.pyc create mode 100644 services/__pycache__/search_service.cpython-310.pyc create mode 100644 services/paramhub_client.py create mode 100644 services/process_service.py create mode 100644 services/search_service.py create mode 100755 start.sh create mode 100644 stop.sh create mode 100644 utils/__pycache__/scheduler.cpython-310.pyc create mode 100644 utils/scheduler.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..2af7550 --- /dev/null +++ b/README.md @@ -0,0 +1,270 @@ +# Param Auto Manager - 参数数据自动化管理系统 + +自动管理参数数据网站的系统,支持从内容库和互联网搜索数据,自动处理产品并提交到后台管理待审核区。 + +## 功能特性 + +- 📚 **内容库管理**:存储和管理搜索获取的相关文章内容 +- 🔄 **自动处理流程**:自动从待处理列表中提取产品并处理 +- 🔍 **智能搜索**:从内容库和互联网搜索相关数据 +- 📝 **数据提取**:根据产品类别提取和填充字段 +- ✅ **审核提交**:自动提交到ParamHub后台管理待审核区 +- 🕐 **定时任务**:支持定时自动处理产品 + +## 系统架构 + +``` +param-auto-manager/ +├── app.py # 主应用入口 +├── config.py # 配置文件 +├── requirements.txt # Python依赖 +├── models/ +│ └── database.py # 数据库模型 +├── routes/ +│ ├── articles.py # 文章内容库API +│ ├── products.py # 产品处理API +│ └── system.py # 系统管理API +├── services/ +│ ├── search_service.py # 搜索服务 +│ ├── process_service.py # 数据处理服务 +│ └── paramhub_client.py # ParamHub API客户端 +├── utils/ +│ └── scheduler.py # 定时任务调度器 +├── data/ # 数据库文件目录 +└── logs/ # 日志文件目录 +``` + +## 安装依赖 + +```bash +cd works/param-auto-manager +pip install -r requirements.txt +``` + +## 启动服务 + +```bash +# 方式1: 使用启动脚本 +chmod +x start.sh +./start.sh + +# 方式2: 直接运行 +python3 app.py +``` + +服务将在端口 **16043** 启动。 + +## API 文档 + +### 文章内容库 API (`/api/articles`) + +#### 获取文章列表 +``` +GET /api/articles?limit=100&offset=0 +``` + +#### 搜索文章 +``` +GET /api/articles/search?q=关键词&category=分类 +``` + +#### 获取文章详情 +``` +GET /api/articles/{article_id} +``` + +#### 创建文章 +``` +POST /api/articles +Content-Type: application/json + +{ + "product_names": ["产品A", "产品B"], + "category": "AI模型", + "keywords": ["关键词1", "关键词2"], + "summary": "文章摘要", + "content": "文章内容", + "source": "来源", + "url": "原文链接" +} +``` + +#### 从URL抓取文章 +``` +POST /api/articles/fetch +Content-Type: application/json + +{ + "url": "https://example.com/article", + "product_names": ["产品A"], + "category": "分类" +} +``` + +#### 删除文章 +``` +DELETE /api/articles/{article_id} +``` + +### 产品处理 API (`/api/products`) + +#### 获取待处理产品列表 +``` +GET /api/products/pending?limit=20&order_by=priority +``` + +#### 添加待处理产品 +``` +POST /api/products/pending +Content-Type: application/json + +{ + "product_name": "产品名称", + "category": "分类", + "subcategory": "子分类", + "priority": 10, + "source": "manual" +} +``` + +#### 批量添加待处理产品 +``` +POST /api/products/pending +Content-Type: application/json + +[ + {"product_name": "产品1", "category": "分类"}, + {"product_name": "产品2", "category": "分类"} +] +``` + +#### 移除待处理产品 +``` +DELETE /api/products/pending/{product_name} +``` + +#### 获取处理中的产品 +``` +GET /api/products/processing +``` + +#### 获取处理历史 +``` +GET /api/products/history?limit=100 +``` + +#### 获取指定产品的处理历史 +``` +GET /api/products/history/{product_name} +``` + +#### 处理单个产品 +``` +POST /api/products/process +Content-Type: application/json + +{ + "product_name": "产品名称", + "category": "分类", + "subcategory": "子分类" +} +``` + +#### 批量处理产品 +``` +POST /api/products/process/batch +Content-Type: application/json + +{ + "limit": 5 +} +``` + +### 系统管理 API (`/api/system`) + +#### 获取系统配置 +``` +GET /api/system/config +``` + +#### 更新系统配置 +``` +PUT /api/system/config +Content-Type: application/json + +{ + "auto_process_enabled": "true", + "process_interval": "300", + "batch_size": "5" +} +``` + +#### 获取系统统计 +``` +GET /api/system/stats +``` + +#### 健康检查 +``` +GET /api/system/health +``` + +## 处理流程 + +1. **添加待处理产品**:手动添加或系统自动发现新产品 +2. **自动/手动触发处理**: + - 从内容库搜索相关文章 + - 从互联网搜索最新数据 + - 提取产品具体内容 + - 根据类别字段填充数据 + - 提交到ParamHub待审核区 +3. **发现新产品**:处理过程中自动发现并添加相关产品 + +## 数据库表结构 + +### articles (文章内容库) +- id, product_names, category, keywords, summary, content, source, url, fetch_date + +### pending_products (待处理产品列表) +- id, product_name, category, subcategory, priority, source + +### processing_products (处理中产品) +- id, product_name, category, subcategory, status, started_at + +### process_history (处理历史) +- id, product_name, category, subcategory, status, review_id, details + +## 配置说明 + +编辑 `config.py` 文件: + +```python +PORT = 16043 # 服务端口 +PARAMHUB_BASE_URL = 'http://localhost:16041' # ParamHub API地址 +PARAMHUB_PASSWORD = 'admin123' # ParamHub管理员密码 +SEARCH_MAX_RESULTS = 10 # 搜索最大结果数 +PROCESS_INTERVAL = 300 # 自动处理间隔(秒) +BATCH_SIZE = 5 # 批量处理数量 +``` + +## 注意事项 + +1. 确保 ParamHub 服务(端口16041)正常运行 +2. 首次运行会自动创建数据库和表结构 +3. 定时任务默认每5分钟执行一次自动处理 +4. 可通过系统配置API调整自动处理参数 + +## 日志 + +日志文件位于 `logs/app.log`,包含: +- 系统启动信息 +- 处理过程记录 +- 错误和异常信息 + +## 版本历史 + +- v1.0.0 (2026-07-12): 初始版本 + - 内容库管理 + - 产品处理流程 + - 定时任务调度 + - ParamHub API集成 \ No newline at end of file diff --git a/__pycache__/config.cpython-310.pyc b/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b9866f6484e9a0863dbc168b3450a3d7073d589b GIT binary patch literal 753 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 literal 0 HcmV?d00001 diff --git a/logs/app.log b/logs/app.log new file mode 100644 index 0000000..5304c3d --- /dev/null +++ b/logs/app.log @@ -0,0 +1,34 @@ +2026-07-12 01:06:36,858 - param_auto_manager - INFO - 初始化数据库... +2026-07-12 01:06:36,859 - apscheduler.scheduler - INFO - Adding job tentatively -- it will be properly scheduled when the scheduler starts +2026-07-12 01:06:36,859 - scheduler - INFO - 添加定时任务: auto_process, 间隔: 300秒 +2026-07-12 01:06:36,860 - apscheduler.scheduler - INFO - Added job "auto_process_task" to job store "default" +2026-07-12 01:06:36,860 - apscheduler.scheduler - INFO - Scheduler started +2026-07-12 01:06:36,860 - scheduler - INFO - 定时任务调度器已启动 +2026-07-12 01:06:36,861 - param_auto_manager - INFO - 定时任务调度器已启动 +2026-07-12 01:06:36,861 - param_auto_manager - INFO - 启动服务,端口: 16043 + * Serving Flask app 'app' + * Debug mode: on +2026-07-12 01:06:36,878 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:16043 + * Running on http://192.168.0.101:16043 +2026-07-12 01:06:36,878 - werkzeug - INFO - Press CTRL+C to quit +2026-07-12 01:06:36,881 - werkzeug - INFO - * Restarting with stat +2026-07-12 01:06:37,478 - param_auto_manager - INFO - 初始化数据库... +2026-07-12 01:06:37,479 - apscheduler.scheduler - INFO - Adding job tentatively -- it will be properly scheduled when the scheduler starts +2026-07-12 01:06:37,479 - scheduler - INFO - 添加定时任务: auto_process, 间隔: 300秒 +2026-07-12 01:06:37,480 - apscheduler.scheduler - INFO - Added job "auto_process_task" to job store "default" +2026-07-12 01:06:37,480 - apscheduler.scheduler - INFO - Scheduler started +2026-07-12 01:06:37,481 - scheduler - INFO - 定时任务调度器已启动 +2026-07-12 01:06:37,481 - param_auto_manager - INFO - 定时任务调度器已启动 +2026-07-12 01:06:37,481 - param_auto_manager - INFO - 启动服务,端口: 16043 +2026-07-12 01:06:37,493 - werkzeug - WARNING - * Debugger is active! +2026-07-12 01:06:37,657 - werkzeug - INFO - * Debugger PIN: 854-773-964 +2026-07-12 01:06:40,247 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:40] "GET / HTTP/1.1" 200 - +2026-07-12 01:06:46,294 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:46] "GET /api/system/stats HTTP/1.1" 200 - +2026-07-12 01:06:46,302 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:46] "GET /api/products/pending HTTP/1.1" 200 - +2026-07-12 01:06:46,321 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:46] "GET /api/articles HTTP/1.1" 200 - +2026-07-12 01:06:56,298 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:56] "POST /api/articles HTTP/1.1" 200 - +2026-07-12 01:06:56,319 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:56] "POST /api/products/pending HTTP/1.1" 200 - +2026-07-12 01:06:59,746 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:59] "GET /api/articles/search?q=GPT-4o HTTP/1.1" 200 - +2026-07-12 01:06:59,757 - werkzeug - INFO - 127.0.0.1 - - [12/Jul/2026 01:06:59] "GET /api/products/pending HTTP/1.1" 200 - diff --git a/models/__pycache__/database.cpython-310.pyc b/models/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..766fd05b9f8d80b2f46a6c76af78a0b083be0c43 GIT binary patch literal 12929 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 E2c(-hA^-pY literal 0 HcmV?d00001 diff --git a/models/database.py b/models/database.py new file mode 100644 index 0000000..dc99791 --- /dev/null +++ b/models/database.py @@ -0,0 +1,313 @@ +""" +数据库模型和操作 +""" +import sqlite3 +import json +from datetime import datetime +from contextlib import contextmanager +from config import Config + +class Database: + def __init__(self, db_path=None): + self.db_path = db_path or Config.DATABASE + self.init_db() + + @contextmanager + def get_connection(self): + """获取数据库连接""" + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + try: + yield conn + finally: + conn.close() + + def init_db(self): + """初始化数据库""" + with self.get_connection() as conn: + cursor = conn.cursor() + + # 内容库表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS articles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_names TEXT NOT NULL, + category TEXT, + keywords TEXT, + summary TEXT, + content TEXT, + source TEXT, + url TEXT, + fetch_date DATETIME DEFAULT CURRENT_TIMESTAMP, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 待处理产品列表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS pending_products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_name TEXT NOT NULL UNIQUE, + category TEXT, + subcategory TEXT, + priority INTEGER DEFAULT 0, + source TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 处理中产品列表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS processing_products ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_name TEXT NOT NULL UNIQUE, + category TEXT, + subcategory TEXT, + status TEXT DEFAULT 'processing', + started_at DATETIME DEFAULT CURRENT_TIMESTAMP, + error_message TEXT + ) + ''') + + # 处理历史记录 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS process_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_name TEXT NOT NULL, + category TEXT, + subcategory TEXT, + status TEXT, + review_id TEXT, + submitted_at DATETIME DEFAULT CURRENT_TIMESTAMP, + details TEXT + ) + ''') + + # 任务配置表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS task_configs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + config TEXT, + enabled INTEGER DEFAULT 1, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 系统配置表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS system_config ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + +# ========== 内容库操作 ========== + def add_article(self, product_names, category, keywords, summary, content, source, url=None): + """添加文章到内容库""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO articles (product_names, category, keywords, summary, content, source, url) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (json.dumps(product_names, ensure_ascii=False), category, + json.dumps(keywords, ensure_ascii=False), summary, content, source, url)) + conn.commit() + return cursor.lastrowid + + def search_articles(self, keyword, category=None): + """搜索文章""" + with self.get_connection() as conn: + cursor = conn.cursor() + if category: + cursor.execute(''' + SELECT * FROM articles + WHERE (product_names LIKE ? OR keywords LIKE ? OR summary LIKE ? OR content LIKE ?) + AND category = ? + ORDER BY fetch_date DESC + ''', (f'%{keyword}%', f'%{keyword}%', f'%{keyword}%', f'%{keyword}%', category)) + else: + cursor.execute(''' + SELECT * FROM articles + WHERE product_names LIKE ? OR keywords LIKE ? OR summary LIKE ? OR content LIKE ? + ORDER BY fetch_date DESC + ''', (f'%{keyword}%', f'%{keyword}%', f'%{keyword}%', f'%{keyword}%')) + return [dict(row) for row in cursor.fetchall()] + + def get_article_by_id(self, article_id): + """获取文章详情""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM articles WHERE id = ?', (article_id,)) + row = cursor.fetchone() + return dict(row) if row else None + + def get_all_articles(self, limit=100, offset=0): + """获取所有文章""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM articles ORDER BY fetch_date DESC LIMIT ? OFFSET ?', (limit, offset)) + return [dict(row) for row in cursor.fetchall()] + + def delete_article(self, article_id): + """删除文章""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM articles WHERE id = ?', (article_id,)) + conn.commit() + return cursor.rowcount > 0 + +# ========== 待处理产品操作 ========== + def add_pending_product(self, product_name, category=None, subcategory=None, priority=0, source='manual'): + """添加待处理产品""" + with self.get_connection() as conn: + cursor = conn.cursor() + try: + cursor.execute(''' + INSERT INTO pending_products (product_name, category, subcategory, priority, source) + VALUES (?, ?, ?, ?, ?) + ''', (product_name, category, subcategory, priority, source)) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + # 产品已存在,更新优先级 + cursor.execute(''' + UPDATE pending_products + SET priority = MAX(priority, ?), updated_at = CURRENT_TIMESTAMP + WHERE product_name = ? + ''', (priority, product_name)) + conn.commit() + return None + + def get_pending_products(self, limit=10, order_by='priority'): + """获取待处理产品列表""" + with self.get_connection() as conn: + cursor = conn.cursor() + if order_by == 'priority': + cursor.execute('SELECT * FROM pending_products ORDER BY priority DESC, created_at ASC LIMIT ?', (limit,)) + else: + cursor.execute('SELECT * FROM pending_products ORDER BY created_at ASC LIMIT ?', (limit,)) + return [dict(row) for row in cursor.fetchall()] + + def get_pending_count(self): + """获取待处理产品数量""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT COUNT(*) FROM pending_products') + return cursor.fetchone()[0] + + def remove_pending_product(self, product_name): + """从待处理列表移除产品""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM pending_products WHERE product_name = ?', (product_name,)) + conn.commit() + return cursor.rowcount > 0 + +# ========== 处理中产品操作 ========== + def start_processing(self, product_name, category, subcategory): + """开始处理产品""" + with self.get_connection() as conn: + cursor = conn.cursor() + try: + cursor.execute(''' + INSERT INTO processing_products (product_name, category, subcategory, status) + VALUES (?, ?, ?, 'processing') + ''', (product_name, category, subcategory)) + conn.commit() + return cursor.lastrowid + except sqlite3.IntegrityError: + return None + + def finish_processing(self, product_name, status='completed', error_message=None): + """完成处理""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM processing_products WHERE product_name = ?', (product_name,)) + conn.commit() + return cursor.rowcount > 0 + + def get_processing_products(self): + """获取处理中的产品""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM processing_products') + return [dict(row) for row in cursor.fetchall()] + +# ========== 处理历史操作 ========== + def add_process_history(self, product_name, category, subcategory, status, review_id=None, details=None): + """添加处理历史""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO process_history (product_name, category, subcategory, status, review_id, details) + VALUES (?, ?, ?, ?, ?, ?) + ''', (product_name, category, subcategory, status, review_id, + json.dumps(details, ensure_ascii=False) if details else None)) + conn.commit() + return cursor.lastrowid + + def get_process_history(self, limit=100): + """获取处理历史""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM process_history ORDER BY submitted_at DESC LIMIT ?', (limit,)) + return [dict(row) for row in cursor.fetchall()] + + def get_history_by_product(self, product_name): + """获取指定产品的处理历史""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM process_history WHERE product_name = ? ORDER BY submitted_at DESC', (product_name,)) + return [dict(row) for row in cursor.fetchall()] + +# ========== 任务配置操作 ========== + def save_task_config(self, name, config): + """保存任务配置""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT OR REPLACE INTO task_configs (name, config, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ''', (name, json.dumps(config, ensure_ascii=False))) + conn.commit() + + def get_task_config(self, name): + """获取任务配置""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT * FROM task_configs WHERE name = ?', (name,)) + row = cursor.fetchone() + if row: + result = dict(row) + result['config'] = json.loads(result['config']) + return result + return None + +# ========== 系统配置操作 ========== + def get_system_config(self, key, default=None): + """获取系统配置""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT value FROM system_config WHERE key = ?', (key,)) + row = cursor.fetchone() + return row['value'] if row else default + + def set_system_config(self, key, value): + """设置系统配置""" + with self.get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT OR REPLACE INTO system_config (key, value, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ''', (key, value)) + conn.commit() + +# 全局数据库实例 +db = Database() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7805951 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +flask==3.0.0 +flask-cors==4.0.0 +sqlite3 +requests==2.31.0 +beautifulsoup4==4.12.2 +lxml==4.9.3 +apscheduler==3.10.4 +python-dotenv==1.0.0 \ No newline at end of file diff --git a/routes/__pycache__/articles.cpython-310.pyc b/routes/__pycache__/articles.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f3e27c8f4dc3236fa8a28e1f76eab14661a5307a GIT binary patch literal 3273 zcmbtW-ESOM6`y-&c4l_${jifXjg!#uDHe#Ak0PNcDo`3BqzbCWyex{A$=tj4IJ2J3 zy|W3n)=zGV8*ppXCJ|MY?y7U zVG-1sQK4-&Y~8mar|mXeVsOd|yvS_kY?uv?dy5myWyN{Iyba1G*yKyb8fi>weQBgG zvx?T2w7xphPqAsOFX!v*%h%b@4rqNPw|^qnA7oEzeO2oZv7!A8JFN9n?8s@OcC>pK zKFk|j{qgqOTj|S}(p!H{@4mi$tG~VZ^09Be@Eva#hK-u3iqA$}-Vxzaq8!1`ce#Yl zqKucq)^d#~%Fco;OYVzcp((kz5C&YizDU9#;&P=__d8*I_zI?8S47Q@;H~gS%24LF zPtAeSY4cLAV=7zaC)L5*LYe-@(7J5dHFnfcp z(jI-zICEsxOdu3%WXtK9TNFdlr+v~hnS0WBXv7D1m z52{i}#H<@6%_YCh<@@vpXOw%6FJFuWlXEvs<;ZRja47}4K^20yyOh*CVd@opF)x)l z&l6=ra0CKYRpGViM^SU=aOE|dVY?HHq}hZM#y*n*{i@7}FV(EQ6J=XC`MjHkcnYlE zZ`2p!Hm}DWz7$0M#ro{12WLDdJAS~AFRVPBEHs0daZ%UWddC-j`?%jt;^S?9$)AS- z5qA?VM>v@6EUQu!N;qr|6l9}(0>rdAP0G|YXUGvkDJ7KH#QnGUI1H^g^$cep4U?aO z8Nvj@3^3`_3?lGm_Q@*g8S)?i1>h1A2&vtor;Wtg(trWm{|_!t!O931_Yt^Y& zx8Hp;gNq{PMGS|Ibq5fn#3)GX!Xpc9`&C4xl{}4Kf zu?6wbQ6jRE==kX0N86M)$Gj6Q<{1inww{r;e)df z>^U35QlrY-bIFH!Sxt;B^c9#QG1Q)kzMi2oIVao?#OFb+tQWp_dhRhgAeppd*P(-s z?HjAcJd~?9kpu*C;ywg>&$vLuQE(H`qerh9zogpXR*O=D7^jW%UScj!q~+ zcKXg6gIj+*aZG#~1~P(ZC?i3{aTK#C>L8Q}1?Z;pzm3Ef!E#OmW|-sksn<8t%fG6b zS@b`Lt3lgz%z{GbKqld{oi#|eJRi%G{H@-7xcNX&#UsbTIhJv$ZE94QA zs-XV`uKx%HRXGqGvc_;F9puk~ool+(B`yJc{KqmOV-gSYCjMMv|BHH07_sdH2@>zYlz_3tXA;i0^&Bb}}rgU8O1cNXj|^bI(AN zWe`ZB+z}sC3j)Vh?0SnQ!9oE2(SvTl46R*p1v*0t0r(*=SkTQ8C!BL== zcor@qwze!R6OS*Ab1~%()noQ9%K-J$k4@5+)JP5G_$p|0w6jgKAcg;gTv>CnB7|9MtO#iiOldba@$#25T09MAz;|3vpIO<0 z%Wjl>DDX?!{5*Fq!W@Y~cTG=MA&*+{t7_aM^7Xx84EI%Ad>zccTbou+v&rHBuS2cK zm*)glqU_xIVpM6vO(v4FNX@f8Txd1!4(2yoIs5QUSLly(oB9R^YtpkFRnV6mePhuj zQ)g;TJPkR0kTG6s#2$k~DEB1n-HrG&SSaNuKv>3pD6^ArAu3akl&7H`fL4Ka2-;+s F{1T%M*XicqE$p$e_6R5G8dE;bt)6-vynb0 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 M<#F}ndMc;>AIT2kdjJ3c literal 0 HcmV?d00001 diff --git a/routes/__pycache__/system.cpython-310.pyc b/routes/__pycache__/system.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3ee58e76ce7c796f54eaf5c419d8bbb71071db5e GIT binary patch literal 2194 zcmb7F&5ImG6t9o&nV#O6$!4?p5TXQBaKPOOet?G}WZL(|(;yVJYf)03*| z#9d~=km$+PU11zFq*h&rBlI(6Xx(L zb6I7bc0BGa)mW8zi;+RdfY+{1UI1c!M=9K4I|#yZ0yAL5tTdKDCPqtHYkMp;Gez5)E=Jvhe_O-80ymR(L{}G_5MRo1nB8`$}x?qQK(Y9#2ABr%YF(J*QVJ}>S1Ciy5%aZ=t z{)+a^8wzhV@FXW;@*N-hxbm1(2qxJ3R~!LQ)3cwg{UO2Vf~P7bV~m#haBM!dzWZhC#*gFqn9C1<*&;Wa_kSub zw1_Z|G}R#`1l6hv^AhcuNKDOXH-7MU zW3`d@;R-J)Itq|9Y4zwNnZksW1f2kAwbGUeJPy+$r1iYNh6k9TXF!jZi68@f zom?Q$Pz2f=V$Y?n$$TDcDb6g3cZnvmCpGTw=hZ)z#8O3B+CTj=SpUoTJC0VLwJl+2 zoIRCfQJBcnuo&I^e=od<#$PC&2M^|<3t?=U+T}uIqIlMeaJ27%;w8Xl4Bmn%-hmRE zYq=l-yvO3+qR|MJM@s5Lr7Y|Ln}SEY2hCkJexux%zMCpjVG@Kw#ZkiLsG?n4KTyw4 z)M4Z<%5u-y0)9p7l23k984aq<2s$fQ!v{OvRy=Q1v~$M*t`i>;IE*|!$fr> z*zEZXHMW_I1}OVqX&PpOT48r{BfOiJ?`-~df9v)aTQ~2_>1ttV#cE8UE;|pe?F_%( z`u_IjufKvA8&kGCglU!*wiJy8VkB5tHK-T1f~d=*W%E#JI*3ps1E7?T#apobsMXZf zAYfS(1lnClLb)tVd0UyXw)KG&WIB', methods=['GET']) +def get_article(article_id): + """获取文章详情""" + article = db.get_article_by_id(article_id) + + if not article: + return jsonify({'error': '文章不存在'}), 404 + + article['product_names'] = __import__('json').loads(article.get('product_names', '[]')) + article['keywords'] = __import__('json').loads(article.get('keywords', '[]')) + + return jsonify({ + 'success': True, + 'article': article + }) + +@bp.route('', methods=['POST']) +def create_article(): + """创建文章(手动添加)""" + data = request.get_json() + + required_fields = ['product_names', 'summary', 'content', 'source'] + for field in required_fields: + if field not in data: + return jsonify({'error': f'缺少必填字段: {field}'}), 400 + + article_id = search_service.save_to_articles( + product_names=data['product_names'], + category=data.get('category'), + keywords=data.get('keywords', []), + summary=data['summary'], + content=data['content'], + source=data['source'], + url=data.get('url') + ) + + return jsonify({ + 'success': True, + 'article_id': article_id, + 'message': '文章创建成功' + }) + +@bp.route('/', methods=['DELETE']) +def delete_article(article_id): + """删除文章""" + success = db.delete_article(article_id) + + if success: + return jsonify({ + 'success': True, + 'message': '文章已删除' + }) + else: + return jsonify({'error': '文章不存在或删除失败'}), 404 + +@bp.route('/fetch', methods=['POST']) +def fetch_article(): + """从URL抓取文章""" + data = request.get_json() + url = data.get('url') + + if not url: + return jsonify({'error': '请提供URL'}), 400 + + result = search_service.fetch_url_content(url) + + if result: + # 自动保存到内容库 + article_id = search_service.save_to_articles( + product_names=data.get('product_names', [result['title']]), + category=data.get('category'), + keywords=data.get('keywords', []), + summary=result.get('description', ''), + content=result['content'], + source=url, + url=url + ) + + return jsonify({ + 'success': True, + 'article_id': article_id, + 'data': result + }) + else: + return jsonify({'error': '抓取失败'}), 500 \ No newline at end of file diff --git a/routes/products.py b/routes/products.py new file mode 100644 index 0000000..293b133 --- /dev/null +++ b/routes/products.py @@ -0,0 +1,216 @@ +""" +产品处理 API +""" +from flask import Blueprint, request, jsonify +from models.database import db +from services.process_service import process_service + +bp = Blueprint('products', __name__, url_prefix='/api/products') + +@bp.route('/pending', methods=['GET']) +def list_pending(): + """获取待处理产品列表""" + limit = request.args.get('limit', 20, type=int) + order_by = request.args.get('order_by', 'priority') + + products = db.get_pending_products(limit=limit, order_by=order_by) + count = db.get_pending_count() + + return jsonify({ + 'success': True, + 'products': products, + 'count': count + }) + +@bp.route('/pending', methods=['POST']) +def add_pending(): + """添加待处理产品""" + data = request.get_json() + + if isinstance(data, dict): + products = [data] + elif isinstance(data, list): + products = data + else: + return jsonify({'error': '无效的数据格式'}), 400 + + added_count = 0 + for item in products: + if 'product_name' not in item: + continue + + result = db.add_pending_product( + product_name=item['product_name'], + category=item.get('category'), + subcategory=item.get('subcategory'), + priority=item.get('priority', 0), + source=item.get('source', 'manual') + ) + if result: + added_count += 1 + + return jsonify({ + 'success': True, + 'added_count': added_count, + 'message': f'成功添加 {added_count} 个产品到待处理列表' + }) + +@bp.route('/pending/', methods=['DELETE']) +def remove_pending(product_name): + """从待处理列表移除产品""" + success = db.remove_pending_product(product_name) + + if success: + return jsonify({ + 'success': True, + 'message': '产品已从待处理列表移除' + }) + else: + return jsonify({'error': '产品不存在'}), 404 + +@bp.route('/processing', methods=['GET']) +def list_processing(): + """获取正在处理的产品列表""" + products = db.get_processing_products() + + return jsonify({ + 'success': True, + 'products': products, + 'count': len(products) + }) + +@bp.route('/history', methods=['GET']) +def list_history(): + """获取处理历史""" + limit = request.args.get('limit', 100, type=int) + history = db.get_process_history(limit=limit) + + # 解析JSON字段 + for item in history: + if item.get('details'): + item['details'] = __import__('json').loads(item['details']) + + return jsonify({ + 'success': True, + 'history': history, + 'count': len(history) + }) + +@bp.route('/history/', methods=['GET']) +def get_product_history(product_name): + """获取指定产品的处理历史""" + history = db.get_history_by_product(product_name) + + for item in history: + if item.get('details'): + item['details'] = __import__('json').loads(item['details']) + + return jsonify({ + 'success': True, + 'product_name': product_name, + 'history': history, + 'count': len(history) + }) + +@bp.route('/process', methods=['POST']) +def process_single(): + """处理单个产品""" + data = request.get_json() + + if 'product_name' not in data: + return jsonify({'error': '请提供产品名称'}), 400 + + product_info = { + 'product_name': data['product_name'], + 'category': data.get('category'), + 'subcategory': data.get('subcategory') + } + + # 检查是否正在处理 + processing = db.get_processing_products() + if any(p['product_name'] == product_info['product_name'] for p in processing): + return jsonify({'error': '该产品正在处理中'}), 400 + + # 添加到处理中列表 + db.start_processing( + product_name=product_info['product_name'], + category=product_info['category'], + subcategory=product_info['subcategory'] + ) + + try: + # 执行处理 + result = process_service.process_product(product_info) + + # 从待处理列表移除 + db.remove_pending_product(product_info['product_name']) + + # 如果发现新产品,已在process_service中添加到待处理列表 + + return jsonify({ + 'success': result['success'], + 'message': result['message'], + 'review_id': result.get('review_id'), + 'new_products': result.get('new_products', []) + }) + finally: + # 完成处理,从处理中列表移除 + db.finish_processing(product_info['product_name']) + +@bp.route('/process/batch', methods=['POST']) +def process_batch(): + """批量处理产品""" + data = request.get_json() + limit = data.get('limit', 5) + + # 获取待处理产品 + products = db.get_pending_products(limit=limit) + + if not products: + return jsonify({ + 'success': True, + 'message': '没有待处理的产品', + 'processed': 0 + }) + + results = [] + for product in products: + # 检查是否正在处理 + processing = db.get_processing_products() + if any(p['product_name'] == product['product_name'] for p in processing): + results.append({ + 'product_name': product['product_name'], + 'success': False, + 'message': '正在处理中' + }) + continue + + # 添加到处理中列表 + db.start_processing( + product_name=product['product_name'], + category=product.get('category'), + subcategory=product.get('subcategory') + ) + + try: + # 执行处理 + result = process_service.process_product(product) + + # 从待处理列表移除 + db.remove_pending_product(product['product_name']) + + results.append({ + 'product_name': product['product_name'], + 'success': result['success'], + 'message': result['message'], + 'review_id': result.get('review_id') + }) + finally: + # 完成处理 + db.finish_processing(product['product_name']) + + return jsonify({ + 'success': True, + 'processed': len(results), + 'results': results + }) \ No newline at end of file diff --git a/routes/system.py b/routes/system.py new file mode 100644 index 0000000..8c08fa2 --- /dev/null +++ b/routes/system.py @@ -0,0 +1,68 @@ +""" +系统管理 API +""" +from flask import Blueprint, request, jsonify +from models.database import db + +bp = Blueprint('system', __name__, url_prefix='/api/system') + +@bp.route('/config', methods=['GET']) +def get_config(): + """获取系统配置""" + configs = { + 'auto_process_enabled': db.get_system_config('auto_process_enabled', 'true'), + 'process_interval': db.get_system_config('process_interval', '300'), + 'batch_size': db.get_system_config('batch_size', '5') + } + + return jsonify({ + 'success': True, + 'config': configs + }) + +@bp.route('/config', methods=['PUT']) +def update_config(): + """更新系统配置""" + data = request.get_json() + + for key, value in data.items(): + db.set_system_config(key, str(value)) + + return jsonify({ + 'success': True, + 'message': '配置已更新' + }) + +@bp.route('/stats', methods=['GET']) +def get_stats(): + """获取系统统计信息""" + pending_count = db.get_pending_count() + processing_count = len(db.get_processing_products()) + + # 获取最近处理历史 + recent_history = db.get_process_history(limit=10) + success_count = len([h for h in recent_history if h['status'] == 'submitted']) + failed_count = len([h for h in recent_history if h['status'] in ['failed', 'error']]) + + # 获取内容库统计 + articles = db.get_all_articles(limit=1000) + + return jsonify({ + 'success': True, + 'stats': { + 'pending_products': pending_count, + 'processing_products': processing_count, + 'recent_success': success_count, + 'recent_failed': failed_count, + 'total_articles': len(articles) + } + }) + +@bp.route('/health', methods=['GET']) +def health_check(): + """健康检查""" + return jsonify({ + 'success': True, + 'status': 'healthy', + 'message': '系统运行正常' + }) \ No newline at end of file diff --git a/services/__pycache__/paramhub_client.cpython-310.pyc b/services/__pycache__/paramhub_client.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b40381d15fa8c4c8065e7d996c5abd3703c78b70 GIT binary patch literal 4043 zcmbVP-H#L36`#5D;j!0icD)Osv;?c_2UY`a8d9~0G~GaGklM{^w+Lk`b!2>Jy<=>T zb7zLFEiYOGNtd*+L@hy0LTg8f%Bso+q*hc5>2v!p%xkgDKIOd+wCCIz4`Z`iRUK*W z_dWOC-#Pbp&b3BI69Rnq&d-+K7#DQHjcs!}3&xT%j?lL9WsX znxG?>#rYVWq@y$iC5`T)W6&3;<1`IaSH?}Nk< zN@7#c3FH#wG8G_Is4yq2i_=+^;b3okW@>ioC$F7;MSo>#?p6Kt?2kR|Y~D6>mz5)( zGgEVOXI`H@;c2zJZ7*4jdNJFuZPTg(9I|bc7uE$f0uujK$SqnGBWKl&YN4E8${h|% zFzuXO%NLA8i;W}BqF%75!E(GcS3}$nJ*v|?yQZE^;{lm$nte_s@qi6}J2oH$jTM3Y<+KM-y~Dhg+W zHv}46CKm`*=7Q3axFlpXcLJ8~{qBC}^Ba$@-RrEq7a-bDb7iY&Rv8{{9pEk5Ru%Ae z3xI}w(oKcaIv?MC^yg2GW~Q?vp2mzfUBh;4FE;0EhQ(o^C)X_7VF(eI$97?(R5Tnf z{^~CZM$G{odP-@UTZ{qRa>_3hr>`;FbXe9g=i@{Uoo zm}%HA=gZ}no_8n1(f+s)`*n>2&Ato+7>@G6Mx5DhO!i>%3?>NHFqSD$h5lqUyty_? zAw&g0CjnXnbj$~I{}7;qa|?K-`=1Pj(8xCNuOU$)-5+WUN?F*XEp&c%aAXZXWIl-XCH zKP&sTITGMi*G)sz7TtXjOS@8d_t}`n{$E zzb{2=!ezK6fh6gSfUbc8v#V-ni4fy3tv^)A`$5osD+4_1Dg?H^VVuVTu*)qhSptgj#m$HREU| zu%x^9JHP)`=3oW9M>$ulxw!)Tv|i0u%tCfxKFvG%qnU@Bf9(9`qQ8lO?ohl*`@&D1 z_~OB8@3*%fZeHuWbGy58(O>h62Uo(GVPV#ATvi?0^1*+hKS1CpP5@o@WI z+;-1aOvf?kNl)PianT9*y7lkpbT%I0K<;bMxB0$i#9zX9_naR&D{`?$YPeF2d6cmExbU_S>5Ng|hO5?ph1P4Fjy zQ&pl#V+1)iEv89I+>0Fh??z^b1fCMhz6K(9=Grp!{-0d?V!*Y1PZr_Yh$jP<9b#Jq zG~mwgiml#!)V|%A8;JiIjNFD>1J|{0 z2-e8Nog;!OV!01!?|+I2S`e&JDGbY$`gZ|OKP*$JJQx}9uuS1$8KTKcoWfLh8#IAE zwtl(RNJr7k6)YEGCWU7V!|$hX^`{Nux@{0J_5=;MCm^;sh$Wh6B(6HJBbNT`osF}B zfM%Wsv=j-kAmSB+co%|!l1$}uq;`Opz(0jy*RjA@#~|!d0o;hO1~D%N1{#1H!w`Xk z8+S1<=&ZfFwQ_N5<^A3tK0&u|{aR=3PH&^#yZ&|nan*9n1rt~Sf>U6;fH*lzF{C;6 zO-Oi1HXy4X4xa>K!Qj`A$v72pQu-FQ4aQ_aNWqR^*N&mYxD7m{59|uY^JkVHr>PQ# z!0O{Y(eU>vCK(pOFqX)QoKP@?*LCn>hOT=FT?cRAma&}F^*7yo+3#T(bobg)L6eeViX@i*1E?KbQ~&?~ literal 0 HcmV?d00001 diff --git a/services/__pycache__/process_service.cpython-310.pyc b/services/__pycache__/process_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b3bdc4902c37c71ccfa87b033b8ea37d16cf0057 GIT binary patch literal 8602 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 PCAI(3iu4!sXOH|JHa>aY literal 0 HcmV?d00001 diff --git a/services/__pycache__/search_service.cpython-310.pyc b/services/__pycache__/search_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dd90e38396e345f24d9a71f099e775b01b557b43 GIT binary patch literal 2890 zcmZ`*|8E>e6`$EJw|ndJ`QoHWs;cD>sNzIz2gONIYa!e;DiMu~e2!dBz-qlSw$I(W zz0A(V_Gyg@4q!?RrAZ4(UGO20S^;U13Wz3E{*U>UobCKVeh2a8z1_QuZD39}`{vD? zFYmLT_uhtsgARfA=PT1og+W68ii7=4hryTOAAJKlMk<6+rUkS@;i(5&O|R%Q)fp87 zbVgv-%!--l%pg;0CQW;=cnUu24GjN2n^TyWh zN59(H+U&g6E{+uM-Tras``_!_`b+2IYn?Y=zxVN*d*8Xf`^itcW^aACvw6$84-3gu ziK@ZpxYv}``DRcKn+;Id%#&PJYaHavxv)N8T>wdAb3QKF-};U|4_Z#ZK~mAs*{h_Y zvms_M^C|@wGt2^w2^yPaL6czxHo$TqS)dzad5~`tFAUwUz-tOjb{iYs{33}giE8W9iX-zNgzFZui2*kV}YV_}2W{lHru8|_QW zp?D=~cs?IlY>mrB*AE#NV~KXG;R&xc0@#KlHLvb1z(kaK6@h?Gy4~?;V^!I%Tdh~6 z>*mlqyuVFzv~~Od9HTv>DIc-#HMB->5@3cwh_!|hlO?jIEs!-mMr3JIz=TumHsHu1 zUOc$7aqpA6yVu`KX)^h>3p*Qc?{55V@5XN*-1%XD;ojXF_kZyE&du-c{OnJu{qC(- zJDYFq-n`o7*L&@qAN{g#U!r0F4lWr29aGjTd==nfiBefzz!R9$E{|42U3{u_$wi&Z z0w|*lbdG5lhSqxgAS#EfeI!Y*;o%sr2{wNNp*4#PtSkPAQosUnlI?n)7Xk9V>UGWx7mCk2H}G zJFi{q{N!!O0r%T~OmjoaxdiMRnOxv?*_yZ*wyHtkjhz`iRs7s+wa&ujs5muK{KBcx zQ_mJbad!OK;>y|aQgO1;2>2|Y`)XB=ojLvV=;^a1O<9ZF1L8*_3uh@KSg8fdlvNpU zMN}s8MYSQ78Oc>pP-g~KWFwC8Awz+Bm{&%POHUbfug28?8_%{jk;u zBd%;%?Bcw@uvB_ITvpj?6v7@gPpa(s6`waGIHSyl08%R*d@cX`b4zYX53|>R^oh*Ukqr5W0<7D2Xx*bV;_8*lJ^$ zq6r80m!&jSlr&+2Z9m*SA*+7CqmrH?gJG|1A71D}C{_{0;p@r$zogLHbH>N} z^_m4j|Gv{OSb!hqg)JQ-o@^7$3vCT!9%5XdAh9kU13mZznE>J(w8jLHT5PUU=wb9I zS*IrmNG4bt;B}^X7B=oD!rj|AIxp$U?pWtj7kR zo{6otm2d~Se(&zPo!|dF1#kEEwVhvVbv8ciL)hcdfXA5IMXY3}1!u4!Le})9i-{#l zk+8c(2vSPatkpc=RuK+?OcgeT&(ny+%D=B96(a8ncV*byKl&ETM@YTnTI-2JE*~-# zNVk$UHB-uBoyVKFu5w(r23HGE&bjX8rWd3$VhE4MHz`hIheQ_>*gc2c!;2*n7EFP) z5i4DUBO5*)9K#-To^*`-iDa&nQ /dev/null 2>&1; then + echo "服务已在运行中 (PID: $PID)" + exit 1 + else + rm -f $PID_FILE + fi +fi + +# 设置Python路径 +export PYTHONPATH="$HOME/.local/lib/python3.12/site-packages:$PYTHONPATH" + +# 启动服务 +echo "启动参数数据自动化管理系统..." +nohup python3 app.py > $LOG_DIR/app.log 2>&1 & +echo $! > $PID_FILE + +echo "服务已启动 (PID: $(cat $PID_FILE))" +echo "日志文件: $LOG_DIR/app.log" \ No newline at end of file diff --git a/stop.sh b/stop.sh new file mode 100644 index 0000000..96ea199 --- /dev/null +++ b/stop.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# 停止脚本 + +APP_DIR="/home/openclaw/.openclaw/workspace-hz4th_coder/works/param-auto-manager" +PID_FILE="$APP_DIR/app.pid" + +if [ ! -f "$PID_FILE" ]; then + echo "PID文件不存在,服务可能未运行" + exit 1 +fi + +PID=$(cat $PID_FILE) + +if ps -p $PID > /dev/null 2>&1; then + echo "停止服务 (PID: $PID)..." + kill $PID + + # 等待进程结束 + for i in {1..10}; do + if ! ps -p $PID > /dev/null 2>&1; then + break + fi + sleep 1 + done + + # 如果进程还在运行,强制杀掉 + if ps -p $PID > /dev/null 2>&1; then + echo "强制停止服务..." + kill -9 $PID + fi + + echo "服务已停止" +else + echo "服务未运行" +fi + +rm -f $PID_FILE \ No newline at end of file diff --git a/utils/__pycache__/scheduler.cpython-310.pyc b/utils/__pycache__/scheduler.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..900ac8d868960c8967616748d51756e70dadcad6 GIT binary patch literal 4243 zcmb6cU2GJ`d1v<5=kxi_HU_GcUTA2!ramWX`bQC>B~Z~KC&ehJS4FG!&DuVD@AjD8 z#rSlNK&A!KkfIhPf`H^}X&_O6HjP3AY5Lx$KF@18+dNc#=v!Noe&6if`D{?D?rLUd zXTJI7`~SZ2`}z_JJiT*=XOxVh{1tz?9}Rx?LJQu2jwuC&5k@^yrUgoHta@r$D`;}8 zd3sqd=yI%kM%gTwa%^~3xu?)0$EKGk_ZE8P*z%Hvq??@SgO*}FEO8!iy2+(}*4rS3 z^!F8(WPPU<*0)Ft88@@26b4v|^`BM>gDlN5@Eu~-xRM))HUn{q=B>-iZ+>|H-o56l zjn&^RHt)XOymTY+0G5;-5rf}zinCMPk1A}uIPJ2?b2&`+AFPBfKkj%(cxh@%PFc(e z-LO=4VVSZC1gdhs=mvou0BEU*3&uXbGFh4$t39`4rxO$oY)tJr< z`0C7L7JLm9CwxuT%aZW5SRV)~CHjsy!EA^BA_0u;2gi8SK4`%JbfL1S)D@dB0zG94 zAfRIhawdW1z4Hb@qC!l16q0I$%KCE8HXD1lWaRF^bH8wCEhHs4L?3r)9HIQT zK>G)Z*`NtxkQC7#)&|#eDbf9J7Wx0d;;AdkH-Ft>@xXs)Q39`L(F&%ckoj{JK8TE< zk{-t)*h}bFzksWopsNjRWX!J?QMc_!hk*(0htPsO(4p#PNS#cQkk-K+&Xc;zC{wSf zb?q8?9h4{4sMXcC6sFfH^u|?PD%Au_wjvbq@*M~kOWhg-*ER#U=vP>9B&P~9jKhMU zB_`6`t$JX(wtJ!!gkbg&a`z~XUD*iL_SNizH$QlA`6B$H(XCwnB}yvx8;3D_#-HH0 zuqG&2PQirGCZkGGq)V;P1Yt)KBn;SM10lxjoWrMr4Mroi|FR<2kAu0rROjh)H0}mB~Sp0G^9%^_zrnXxhnnUsKT^y zMN>j;{#l|dKv=I00gTQl!Z0;7>U0sIHQaqv`7yvR&{rt#ibr!~PRZ#~Nh{ayJ-BqO zTS-#xk4}T2k=b}5&I8SBzhC{}w`&TycxmDC z!;)VSy34uGg&Kq$A|GmpqtOcDZje6T4%5#<3lNCXBpC*m-ASyI^t0NQ^`c%o^bz0f zL|~M5EaC=6ULr-Y`ud&bFWy)_cXIj4xtPHmjTy0~+z`149_MWQ%Yf|LNE@P0(2c>e zy>~3vgW(l3mTilKZI^w_lQ2%&_OZy3xy-gX6bf;O;+S~(lh`5q@|-TY$e>hyW#<@4v1+G6cT zfX`|u3u@SNMwuisomzGEd1a9-5{Ppw?M@%N8kTfOs#)jyqCzVhDkdyVCD*H;^_jfupB z6BeiKpj2}O`8q^=-7A$#VT1{H`TYj?R`Zje#uNc9F;Nj_>E_2Lo7aCHuf#Dvd;g<% zV}Ny@jBbY6>ub=0EOe*9YG3YMfDp-)tqN!d`dRuClpMz(cVT5ElE55`ilKDBG3iAx zOY_eNQib};Y%Ry`g-#`Uc2Bt#_e7QN`3atb2hx+2d8Mt6k8XA(D6Aq>#Zx|?7rj9=(f)y$hGTSQ}-bY;T^q`{3dw2-I1a=vHPN=3>>gqj_$KV?7`; z7`TG@Aau)6NMbz>s`Td|WUs|?6%wKtLO>`+86<5qB9wC=RL`x5&2lNO@7J$7m3d)8 zqQwGNWG12L45r%_h*N1MfQw`FTDvi{4im$zn7@Gt{zZW1PhqzMx||`*mTmJ9 z#A9`cO~456Ab7ID-5~Npj`=;d2Q)M;jUw}i^7q18a0oi8B_Ss$#tf}G#BZiz+{4HKi5@HI)mxve{P9+|^1I`JNVg== zDRD%yn^(w$h(xAmNM!sw_=9G`G4o0Ba_7a{cMpja`iR}R zWyA>$8HCtiwF|1Qye0)b9JparwYy2c1bhOv1!&d7AXlunC`mlzFCwx{!nAGX7a^ZM z=~O$%_hU-5_=YAXFi4& m7uN2$g4lz`5nzd+DM>X++OI`46{Tc+n$S|zc-o@LZ~X_X@!F37 literal 0 HcmV?d00001 diff --git a/utils/scheduler.py b/utils/scheduler.py new file mode 100644 index 0000000..2abf89a --- /dev/null +++ b/utils/scheduler.py @@ -0,0 +1,127 @@ +""" +定时任务调度器 +""" +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.interval import IntervalTrigger +from datetime import datetime +from models.database import db +from services.process_service import process_service +from config import Config +import logging + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger('scheduler') + +class TaskScheduler: + def __init__(self): + self.scheduler = BackgroundScheduler() + self.running = False + + def start(self): + """启动调度器""" + if not self.running: + self.scheduler.start() + self.running = True + logger.info("定时任务调度器已启动") + + def stop(self): + """停止调度器""" + if self.running: + self.scheduler.shutdown() + self.running = False + logger.info("定时任务调度器已停止") + + def add_interval_job(self, func, interval_seconds, job_id, **kwargs): + """添加定时任务""" + self.scheduler.add_job( + func, + trigger=IntervalTrigger(seconds=interval_seconds), + id=job_id, + replace_existing=True, + **kwargs + ) + logger.info(f"添加定时任务: {job_id}, 间隔: {interval_seconds}秒") + + def remove_job(self, job_id): + """移除定时任务""" + try: + self.scheduler.remove_job(job_id) + logger.info(f"移除定时任务: {job_id}") + except Exception as e: + logger.error(f"移除任务失败: {job_id}, 错误: {str(e)}") + + def get_jobs(self): + """获取所有任务""" + return self.scheduler.get_jobs() + +# 创建全局调度器实例 +task_scheduler = TaskScheduler() + +def auto_process_task(): + """ + 自动处理产品的定时任务 + """ + try: + # 检查是否启用自动处理 + enabled = db.get_system_config('auto_process_enabled', 'true') + if enabled.lower() != 'true': + logger.info("自动处理已禁用,跳过本次执行") + return + + # 获取批量处理数量 + batch_size = int(db.get_system_config('batch_size', '5')) + + # 获取待处理产品 + products = db.get_pending_products(limit=batch_size) + + if not products: + logger.info("没有待处理的产品") + return + + logger.info(f"开始处理 {len(products)} 个产品") + + for product in products: + try: + # 检查是否正在处理 + processing = db.get_processing_products() + if any(p['product_name'] == product['product_name'] for p in processing): + logger.warning(f"产品 {product['product_name']} 正在处理中,跳过") + continue + + # 添加到处理中列表 + db.start_processing( + product_name=product['product_name'], + category=product.get('category'), + subcategory=product.get('subcategory') + ) + + # 执行处理 + result = process_service.process_product(product) + + # 从待处理列表移除 + db.remove_pending_product(product['product_name']) + + logger.info(f"产品 {product['product_name']} 处理完成: {result['message']}") + + except Exception as e: + logger.error(f"处理产品 {product['product_name']} 时出错: {str(e)}") + db.finish_processing(product['product_name']) + finally: + # 确保从处理中列表移除 + db.finish_processing(product['product_name']) + + except Exception as e: + logger.error(f"自动处理任务执行失败: {str(e)}") + +def setup_auto_process_job(): + """设置自动处理定时任务""" + interval = int(db.get_system_config('process_interval', str(Config.PROCESS_INTERVAL))) + task_scheduler.add_interval_job( + auto_process_task, + interval_seconds=interval, + job_id='auto_process' + ) \ No newline at end of file