diff --git a/NEWS_FEATURE_v2.2.md b/NEWS_FEATURE_v2.2.md new file mode 100644 index 0000000..c16bbe0 --- /dev/null +++ b/NEWS_FEATURE_v2.2.md @@ -0,0 +1,408 @@ +# ZJPB v2.2.0 - 新闻搜索功能 + +**版本**: v2.2.0 +**发布日期**: 2025-01-30 +**主要功能**: 集成博查Web Search API,自动获取网站相关新闻 + +--- + +## 📋 功能概述 + +v2.2.0版本引入了全新的新闻搜索和展示功能,通过博查AI搜索引擎API,自动为每个网站获取最新的相关新闻,并在网站详情页进行展示。 + +### ✨ 核心特性 + +1. **智能新闻搜索** 🔍 + - 基于网站名称自动搜索相关新闻 + - 支持自定义搜索时间范围(一天、一周、一月、一年) + - 自动排除网站自身的内容,确保新闻来源多样性 + +2. **新闻管理系统** 📰 + - 后台新闻列表管理 + - 支持手动编辑和删除新闻 + - 新闻来源信息展示(网站名称、图标) + +3. **前台新闻展示** 🎨 + - 网站详情页展示最多5条相关新闻 + - 显示新闻标题、摘要、来源、发布时间 + - 点击新闻直接跳转到原文链接 + +4. **定期任务支持** ⏰ + - 提供cron任务脚本,支持定期批量获取新闻 + - 可配置获取数量、时间范围等参数 + +--- + +## 🚀 快速开始 + +### 1. 配置博查API + +在`.env`文件中添加博查API配置: + +```env +# 博查 Web Search API配置 +BOCHA_API_KEY=sk-your-api-key-here +BOCHA_BASE_URL=https://api.bocha.cn +``` + +获取API Key:访问 [博查AI开放平台](https://open.bocha.cn) 注册并获取API密钥。 + +### 2. 数据库迁移 + +运行迁移脚本,为News表添加新字段: + +```bash +python migrate_news_fields.py +``` + +迁移会添加以下字段: +- `source_name`: 新闻来源网站名称 +- `source_icon`: 新闻来源网站图标URL + +### 3. 测试新闻获取 + +#### 方法1:使用Python测试脚本 + +```bash +# 测试NewsSearcher类 +python utils/news_searcher.py +``` + +#### 方法2:使用后台API + +登录后台后,使用API接口: + +**单个网站获取新闻**: +```bash +curl -X POST http://localhost:5000/api/fetch-site-news \ + -H "Content-Type: application/json" \ + -d '{ + "site_id": 1, + "count": 10, + "freshness": "oneMonth" + }' +``` + +**批量获取新闻**: +```bash +curl -X POST http://localhost:5000/api/fetch-all-news \ + -H "Content-Type: application/json" \ + -d '{ + "count": 5, + "freshness": "oneMonth", + "limit": 10 + }' +``` + +### 4. 查看新闻 + +1. 访问任意网站详情页(例如:`http://localhost:5000/site/12345678`) +2. 滚动到页面下方,查看"相关新闻"部分 +3. 点击新闻标题或"阅读全文"链接跳转到原文 + +--- + +## 📖 使用指南 + +### 后台管理 + +#### 新闻管理界面 + +1. 登录后台:`http://localhost:5000/admin` +2. 点击左侧菜单"新闻管理" +3. 可以查看、编辑、删除已获取的新闻 + +**新闻列表字段**: +- ID +- 关联网站 +- 新闻标题 +- 来源网站 +- 新闻类型 +- 发布时间 +- 是否启用 + +#### 手动获取新闻 + +虽然提供了API接口,但目前没有直接的后台UI按钮。可以通过以下方式触发: + +1. 使用API接口(参见快速开始) +2. 使用定期任务脚本(参见下文) + +### 定期任务设置 + +使用`fetch_news_cron.py`脚本定期自动获取新闻。 + +#### 手动执行 + +```bash +# 默认参数:处理10个网站,每个网站获取5条新闻,时间范围一个月 +python fetch_news_cron.py + +# 自定义参数 +python fetch_news_cron.py --limit 20 --count 10 --freshness oneWeek +``` + +**参数说明**: +- `--limit`: 处理的网站数量限制(默认:10) +- `--count`: 每个网站获取的新闻数量(默认:5) +- `--freshness`: 新闻时间范围(可选:noLimit, oneDay, oneWeek, oneMonth, oneYear) + +#### 配置Crontab + +在Linux服务器上配置定期任务: + +```bash +# 编辑crontab +crontab -e + +# 添加以下行(每天早上8点执行) +0 8 * * * cd /opt/1panel/apps/zjpb && /opt/1panel/apps/zjpb/venv/bin/python fetch_news_cron.py --limit 10 >> logs/news_fetch.log 2>&1 + +# 或每6小时执行一次 +0 */6 * * * cd /opt/1panel/apps/zjpb && /opt/1panel/apps/zjpb/venv/bin/python fetch_news_cron.py --limit 20 >> logs/news_fetch.log 2>&1 +``` + +**注意**: +- 确保创建`logs`目录:`mkdir -p logs` +- 修改路径为实际的项目路径 +- 根据API配额合理设置执行频率 + +--- + +## 🗂️ 文件结构 + +### 新增文件 + +``` +zjpb/ +├── utils/ +│ └── news_searcher.py # 博查API封装类 +├── migrate_news_fields.py # 数据库迁移脚本 +├── fetch_news_cron.py # 定期任务脚本 +└── NEWS_FEATURE_v2.2.md # 本文档 +``` + +### 修改文件 + +``` +zjpb/ +├── config.py # 添加博查API配置 +├── models.py # News模型添加source_name/source_icon字段 +├── app.py # 添加新闻获取API路由,更新NewsAdmin +└── templates/ + └── detail_new.html # 优化新闻展示UI +``` + +--- + +## 🔧 技术实现 + +### API集成 + +使用博查Web Search API进行新闻搜索: + +- **接口地址**: `https://api.bocha.cn/v1/web-search` +- **认证方式**: Bearer Token +- **请求方法**: POST +- **返回格式**: JSON + +### 数据模型 + +News模型字段: + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | Integer | 主键 | +| site_id | Integer | 关联网站ID(外键) | +| title | String(200) | 新闻标题 | +| content | Text | 新闻内容/摘要 | +| news_type | String(50) | 新闻类型 | +| url | String(500) | 新闻链接 | +| source_name | String(100) | ⭐ 来源网站名称(新增) | +| source_icon | String(500) | ⭐ 来源网站图标(新增) | +| published_at | DateTime | 发布时间 | +| is_active | Boolean | 是否启用 | +| created_at | DateTime | 创建时间 | +| updated_at | DateTime | 更新时间 | + +### API路由 + +**1. 单个网站获取新闻** + +- **路径**: `/api/fetch-site-news` +- **方法**: POST +- **权限**: 需要登录 +- **参数**: + ```json + { + "site_id": 1, + "count": 10, + "freshness": "oneMonth" + } + ``` + +**2. 批量获取新闻** + +- **路径**: `/api/fetch-all-news` +- **方法**: POST +- **权限**: 需要登录 +- **参数**: + ```json + { + "count": 5, + "freshness": "oneMonth", + "limit": 10 + } + ``` + +--- + +## 📊 配置选项 + +在`config.py`中的新闻相关配置: + +```python +# 博查 Web Search API配置 +BOCHA_API_KEY = os.environ.get('BOCHA_API_KEY') +BOCHA_BASE_URL = os.environ.get('BOCHA_BASE_URL') or 'https://api.bocha.cn' +BOCHA_SEARCH_ENDPOINT = '/v1/web-search' + +# 新闻搜索配置 +NEWS_SEARCH_COUNT = 10 # 每次搜索返回的新闻数量 +NEWS_SEARCH_FRESHNESS = 'oneMonth' # 默认搜索一个月内的新闻 +NEWS_SEARCH_SUMMARY = True # 是否显示摘要 +``` + +--- + +## ⚠️ 注意事项 + +### API配额限制 + +- 博查API根据充值金额有请求频率限制 +- 建议合理设置定期任务频率,避免过度消耗配额 +- 详情参见:[博查API定价](https://open.bocha.cn) + +### 去重机制 + +系统会根据新闻URL自动去重,同一条新闻不会重复保存。 + +### 搜索策略 + +- 搜索关键词:`{网站名称} 最新 新闻` +- 自动排除:网站自身域名的内容 +- 时间优先:优先显示最新发布的新闻 + +### 性能优化 + +- 批量获取时建议限制数量(`--limit 10-20`) +- 避免短时间内频繁调用API +- 数据库查询已优化,使用索引和去重 + +--- + +## 🐛 故障排查 + +### 1. 新闻获取失败 + +**可能原因**: +- 博查API Key未配置或无效 +- API配额不足 +- 网络连接问题 + +**解决方法**: +```bash +# 检查环境变量 +python -c "import os; from dotenv import load_dotenv; load_dotenv(); print(os.getenv('BOCHA_API_KEY'))" + +# 测试API连接 +python utils/news_searcher.py +``` + +### 2. 数据库字段不存在 + +**错误信息**:`Unknown column 'source_name' in 'field list'` + +**解决方法**: +```bash +# 运行数据库迁移 +python migrate_news_fields.py +``` + +### 3. 详情页不显示新闻 + +**可能原因**: +- 数据库中没有该网站的新闻记录 +- 新闻被设置为不启用(is_active=False) + +**解决方法**: +```bash +# 为该网站获取新闻 +curl -X POST http://localhost:5000/api/fetch-site-news \ + -H "Content-Type: application/json" \ + -d '{"site_id": YOUR_SITE_ID}' +``` + +### 4. 定期任务不执行 + +**检查清单**: +- [ ] Crontab配置是否正确 +- [ ] Python路径是否正确 +- [ ] 日志文件是否有写入权限 +- [ ] 查看cron日志:`grep CRON /var/log/syslog` + +--- + +## 📈 未来改进 + +### 计划功能 + +- [ ] 后台UI按钮直接触发新闻获取 +- [ ] 新闻分类和标签支持 +- [ ] 新闻摘要AI优化 +- [ ] 新闻热度排序 +- [ ] 用户收藏新闻功能 +- [ ] 新闻RSS订阅 + +### 性能优化 + +- [ ] 使用异步任务队列(Celery) +- [ ] 新闻缓存机制 +- [ ] 图片CDN加速 + +--- + +## 📞 技术支持 + +- **项目名称**: ZJPB - 焦提示词 | AI工具导航 +- **版本**: v2.2.0 +- **发布日期**: 2025-01-30 +- **博查API文档**: https://bocha-ai.feishu.cn/wiki/RXEOw02rFiwzGSkd9mUcqoeAnNK + +--- + +## 📝 更新日志 + +### v2.2.0 (2025-01-30) + +**新增**: +- ✨ 集成博查Web Search API +- ✨ 新闻自动搜索和存储功能 +- ✨ News模型添加source_name和source_icon字段 +- ✨ 网站详情页新闻展示优化 +- ✨ 后台新闻管理界面增强 +- ✨ 定期任务脚本(fetch_news_cron.py) +- ✨ API路由:/api/fetch-site-news 和 /api/fetch-all-news + +**修改**: +- 🔧 config.py添加博查API配置 +- 🔧 NewsAdmin添加source_name字段显示 +- 🔧 detail_new.html优化新闻展示UI + +**文档**: +- 📖 NEWS_FEATURE_v2.2.md 功能文档 +- 📖 migrate_news_fields.py 迁移脚本文档 + +--- + +**祝您使用愉快!** 🎉 diff --git a/app.py b/app.py index 7a2e392..b0f27d4 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,7 @@ from config import config from models import db, Site, Tag, Admin as AdminModel, News, site_tags, PromptTemplate from utils.website_fetcher import WebsiteFetcher from utils.tag_generator import TagGenerator +from utils.news_searcher import NewsSearcher def create_app(config_name='default'): """应用工厂函数""" @@ -442,6 +443,205 @@ def create_app(config_name='default'): 'message': f'生成失败: {str(e)}' }), 500 + # ========== 新闻获取路由 ========== + @app.route('/api/fetch-site-news', methods=['POST']) + @login_required + def fetch_site_news(): + """为指定网站获取最新新闻""" + try: + data = request.get_json() + site_id = data.get('site_id') + count = data.get('count', app.config.get('NEWS_SEARCH_COUNT', 10)) + freshness = data.get('freshness', app.config.get('NEWS_SEARCH_FRESHNESS', 'oneMonth')) + + if not site_id: + return jsonify({ + 'success': False, + 'message': '请提供网站ID' + }), 400 + + # 获取网站信息 + site = Site.query.get(site_id) + if not site: + return jsonify({ + 'success': False, + 'message': '网站不存在' + }), 404 + + # 检查博查API配置 + api_key = app.config.get('BOCHA_API_KEY') + if not api_key: + return jsonify({ + 'success': False, + 'message': '博查API未配置,请在.env文件中设置BOCHA_API_KEY' + }), 500 + + # 创建新闻搜索器 + searcher = NewsSearcher(api_key) + + # 搜索新闻 + news_items = searcher.search_site_news( + site_name=site.name, + site_url=site.url, + count=count, + freshness=freshness + ) + + if not news_items: + return jsonify({ + 'success': False, + 'message': '未找到相关新闻' + }), 404 + + # 保存新闻到数据库 + saved_count = 0 + for item in news_items: + # 检查新闻是否已存在(根据URL判断) + existing_news = News.query.filter_by( + site_id=site_id, + url=item['url'] + ).first() + + if not existing_news: + # 创建新闻记录 + news = News( + site_id=site_id, + title=item['title'], + content=item.get('summary') or item.get('snippet', ''), + url=item['url'], + source_name=item.get('site_name', ''), + source_icon=item.get('site_icon', ''), + published_at=item.get('published_at'), + news_type='Search Result', + is_active=True + ) + db.session.add(news) + saved_count += 1 + + # 提交事务 + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'成功获取并保存 {saved_count} 条新闻', + 'total_found': len(news_items), + 'saved': saved_count, + 'news_items': searcher.format_news_for_display(news_items) + }) + + except Exception as e: + db.session.rollback() + return jsonify({ + 'success': False, + 'message': f'获取失败: {str(e)}' + }), 500 + + @app.route('/api/fetch-all-news', methods=['POST']) + @login_required + def fetch_all_news(): + """批量为所有网站获取新闻""" + try: + data = request.get_json() + count_per_site = data.get('count', 5) # 每个网站获取的新闻数量 + freshness = data.get('freshness', app.config.get('NEWS_SEARCH_FRESHNESS', 'oneMonth')) + limit = data.get('limit', 10) # 限制处理的网站数量 + + # 检查博查API配置 + api_key = app.config.get('BOCHA_API_KEY') + if not api_key: + return jsonify({ + 'success': False, + 'message': '博查API未配置,请在.env文件中设置BOCHA_API_KEY' + }), 500 + + # 获取启用的网站(按更新时间排序,优先处理旧的) + sites = Site.query.filter_by(is_active=True).order_by(Site.updated_at).limit(limit).all() + + if not sites: + return jsonify({ + 'success': False, + 'message': '没有可用的网站' + }), 404 + + # 创建新闻搜索器 + searcher = NewsSearcher(api_key) + + # 统计信息 + total_saved = 0 + total_found = 0 + processed_sites = [] + + # 为每个网站获取新闻 + for site in sites: + try: + # 搜索新闻 + news_items = searcher.search_site_news( + site_name=site.name, + site_url=site.url, + count=count_per_site, + freshness=freshness + ) + + site_saved = 0 + for item in news_items: + # 检查是否已存在 + existing_news = News.query.filter_by( + site_id=site.id, + url=item['url'] + ).first() + + if not existing_news: + news = News( + site_id=site.id, + title=item['title'], + content=item.get('summary') or item.get('snippet', ''), + url=item['url'], + source_name=item.get('site_name', ''), + source_icon=item.get('site_icon', ''), + published_at=item.get('published_at'), + news_type='Search Result', + is_active=True + ) + db.session.add(news) + site_saved += 1 + + total_found += len(news_items) + total_saved += site_saved + + processed_sites.append({ + 'id': site.id, + 'name': site.name, + 'found': len(news_items), + 'saved': site_saved + }) + + except Exception as e: + # 单个网站失败不影响其他网站 + processed_sites.append({ + 'id': site.id, + 'name': site.name, + 'error': str(e) + }) + continue + + # 提交事务 + db.session.commit() + + return jsonify({ + 'success': True, + 'message': f'批量获取完成,共处理 {len(processed_sites)} 个网站', + 'total_found': total_found, + 'total_saved': total_saved, + 'processed_sites': processed_sites + }) + + except Exception as e: + db.session.rollback() + return jsonify({ + 'success': False, + 'message': f'批量获取失败: {str(e)}' + }), 500 + # ========== 批量导入路由 ========== @app.route('/admin/batch-import', methods=['GET', 'POST']) @login_required @@ -908,9 +1108,9 @@ def create_app(config_name='default'): # 显示操作列 column_display_actions = True - column_list = ['id', 'site', 'title', 'news_type', 'published_at', 'is_active'] - column_searchable_list = ['title', 'content'] - column_filters = ['site', 'news_type', 'is_active', 'published_at'] + column_list = ['id', 'site', 'title', 'source_name', 'news_type', 'published_at', 'is_active'] + column_searchable_list = ['title', 'content', 'source_name'] + column_filters = ['site', 'news_type', 'source_name', 'is_active', 'published_at'] column_labels = { 'id': 'ID', 'site': '关联网站', @@ -918,16 +1118,19 @@ def create_app(config_name='default'): 'content': '新闻内容', 'news_type': '新闻类型', 'url': '新闻链接', + 'source_name': '来源网站', + 'source_icon': '来源图标', 'published_at': '发布时间', 'is_active': '是否启用', 'created_at': '创建时间', 'updated_at': '更新时间' } - form_columns = ['site', 'title', 'content', 'news_type', 'url', 'published_at', 'is_active'] + form_columns = ['site', 'title', 'content', 'news_type', 'url', 'source_name', 'source_icon', 'published_at', 'is_active'] # 可选的新闻类型 form_choices = { 'news_type': [ + ('Search Result', 'Search Result'), ('Product Update', 'Product Update'), ('Industry News', 'Industry News'), ('Company News', 'Company News'), @@ -935,6 +1138,9 @@ def create_app(config_name='default'): ] } + # 默认排序 + column_default_sort = ('published_at', True) # 按发布时间倒序排列 + # Prompt模板管理视图 class PromptAdmin(SecureModelView): can_edit = True diff --git a/config.py b/config.py index 9cde07b..7a44182 100644 --- a/config.py +++ b/config.py @@ -46,6 +46,16 @@ class Config: DEEPSEEK_API_KEY = os.environ.get('DEEPSEEK_API_KEY') DEEPSEEK_BASE_URL = os.environ.get('DEEPSEEK_BASE_URL') or 'https://api.deepseek.com' + # 博查 Web Search API配置 + BOCHA_API_KEY = os.environ.get('BOCHA_API_KEY') + BOCHA_BASE_URL = os.environ.get('BOCHA_BASE_URL') or 'https://api.bocha.cn' + BOCHA_SEARCH_ENDPOINT = '/v1/web-search' + + # 新闻搜索配置 + NEWS_SEARCH_COUNT = 10 # 每次搜索返回的新闻数量 + NEWS_SEARCH_FRESHNESS = 'oneMonth' # 默认搜索一个月内的新闻 + NEWS_SEARCH_SUMMARY = True # 是否显示摘要 + class DevelopmentConfig(Config): """开发环境配置""" DEBUG = True diff --git a/fetch_news_cron.py b/fetch_news_cron.py new file mode 100644 index 0000000..9e6b092 --- /dev/null +++ b/fetch_news_cron.py @@ -0,0 +1,167 @@ +""" +定期新闻获取任务脚本 +用途:定期为网站批量获取最新新闻 +使用:python fetch_news_cron.py [options] + +可以通过crontab定时执行: +# 每天早上8点执行,获取10个网站的新闻 +0 8 * * * cd /path/to/zjpb && /path/to/venv/bin/python fetch_news_cron.py --limit 10 >> logs/news_fetch.log 2>&1 +""" +import os +import sys +import argparse +from datetime import datetime +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +# 添加项目根目录到Python路径 +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app import create_app +from models import db, Site, News +from utils.news_searcher import NewsSearcher + + +def fetch_news_for_sites(limit=10, count_per_site=5, freshness='oneMonth'): + """ + 批量为网站获取新闻 + + Args: + limit: 处理的网站数量限制 + count_per_site: 每个网站获取的新闻数量 + freshness: 新闻时间范围 + """ + # 创建Flask应用上下文 + app = create_app(os.getenv('FLASK_ENV', 'production')) + + with app.app_context(): + # 检查博查API配置 + api_key = app.config.get('BOCHA_API_KEY') + if not api_key: + print(f"[{datetime.now()}] 错误:未配置BOCHA_API_KEY") + return False + + # 获取启用的网站(按更新时间排序,优先处理旧的) + sites = Site.query.filter_by(is_active=True).order_by(Site.updated_at).limit(limit).all() + + if not sites: + print(f"[{datetime.now()}] 没有可处理的网站") + return False + + print(f"[{datetime.now()}] 开始批量获取新闻,共 {len(sites)} 个网站") + print(f"配置:每个网站 {count_per_site} 条新闻,时间范围:{freshness}") + print("-" * 60) + + # 创建新闻搜索器 + searcher = NewsSearcher(api_key) + + # 统计信息 + total_saved = 0 + total_found = 0 + success_count = 0 + error_count = 0 + + # 为每个网站获取新闻 + for i, site in enumerate(sites, 1): + print(f"[{i}/{len(sites)}] 处理网站: {site.name}") + + try: + # 搜索新闻 + news_items = searcher.search_site_news( + site_name=site.name, + site_url=site.url, + count=count_per_site, + freshness=freshness + ) + + if not news_items: + print(f" └─ 未找到新闻") + continue + + site_saved = 0 + for item in news_items: + # 检查是否已存在 + existing_news = News.query.filter_by( + site_id=site.id, + url=item['url'] + ).first() + + if not existing_news: + news = News( + site_id=site.id, + title=item['title'], + content=item.get('summary') or item.get('snippet', ''), + url=item['url'], + source_name=item.get('site_name', ''), + source_icon=item.get('site_icon', ''), + published_at=item.get('published_at'), + news_type='Search Result', + is_active=True + ) + db.session.add(news) + site_saved += 1 + + # 提交该网站的新闻 + db.session.commit() + + total_found += len(news_items) + total_saved += site_saved + success_count += 1 + + print(f" └─ 找到 {len(news_items)} 条,保存 {site_saved} 条新闻") + + except Exception as e: + error_count += 1 + print(f" └─ 错误: {str(e)}") + db.session.rollback() + continue + + print("-" * 60) + print(f"[{datetime.now()}] 批量获取完成") + print(f"成功: {success_count} 个网站, 失败: {error_count} 个网站") + print(f"共找到 {total_found} 条新闻,保存 {total_saved} 条新新闻") + print("=" * 60) + + return True + + +def main(): + """主函数""" + parser = argparse.ArgumentParser(description='定期新闻获取任务') + parser.add_argument('--limit', type=int, default=10, help='处理的网站数量限制(默认:10)') + parser.add_argument('--count', type=int, default=5, help='每个网站获取的新闻数量(默认:5)') + parser.add_argument('--freshness', type=str, default='oneMonth', + choices=['noLimit', 'oneDay', 'oneWeek', 'oneMonth', 'oneYear'], + help='新闻时间范围(默认:oneMonth)') + + args = parser.parse_args() + + print("=" * 60) + print(f"定期新闻获取任务 - 开始时间: {datetime.now()}") + print("=" * 60) + + try: + success = fetch_news_for_sites( + limit=args.limit, + count_per_site=args.count, + freshness=args.freshness + ) + + if success: + print(f"\n任务执行成功!") + sys.exit(0) + else: + print(f"\n任务执行失败!") + sys.exit(1) + + except Exception as e: + print(f"\n[{datetime.now()}] 严重错误: {str(e)}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == '__main__': + main() diff --git a/migrate_news_fields.py b/migrate_news_fields.py new file mode 100644 index 0000000..5020516 --- /dev/null +++ b/migrate_news_fields.py @@ -0,0 +1,99 @@ +""" +数据库迁移脚本 - 为News表添加source_name和source_icon字段 +版本:v2.2.0 +日期:2025-01-30 +""" +import pymysql +import os +from dotenv import load_dotenv + +# 加载环境变量 +load_dotenv() + +def migrate(): + """执行数据库迁移""" + # 数据库配置 + db_config = { + 'host': os.environ.get('DB_HOST', 'localhost'), + 'port': int(os.environ.get('DB_PORT', 3306)), + 'user': os.environ.get('DB_USER', 'root'), + 'password': os.environ.get('DB_PASSWORD', ''), + 'database': os.environ.get('DB_NAME', 'ai_nav'), + 'charset': 'utf8mb4' + } + + try: + # 连接数据库 + connection = pymysql.connect(**db_config) + cursor = connection.cursor() + + print("=" * 60) + print("开始执行数据库迁移 v2.2.0") + print("=" * 60) + + # 检查字段是否已存在 + cursor.execute(""" + SELECT COLUMN_NAME + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = %s + AND TABLE_NAME = 'news' + AND COLUMN_NAME IN ('source_name', 'source_icon') + """, (db_config['database'],)) + + existing_columns = [row[0] for row in cursor.fetchall()] + + # 添加 source_name 字段 + if 'source_name' not in existing_columns: + print("\n1. 添加 source_name 字段...") + cursor.execute(""" + ALTER TABLE news + ADD COLUMN source_name VARCHAR(100) + COMMENT '新闻来源网站名称' + AFTER url + """) + print(">>> source_name 字段添加成功") + else: + print("\n1. source_name 字段已存在,跳过") + + # 添加 source_icon 字段 + if 'source_icon' not in existing_columns: + print("\n2. 添加 source_icon 字段...") + cursor.execute(""" + ALTER TABLE news + ADD COLUMN source_icon VARCHAR(500) + COMMENT '新闻来源网站图标URL' + AFTER source_name + """) + print(">>> source_icon 字段添加成功") + else: + print("\n2. source_icon 字段已存在,跳过") + + # 提交事务 + connection.commit() + + print("\n" + "=" * 60) + print(">>> 数据库迁移完成!") + print("=" * 60) + + # 显示表结构 + print("\n当前 news 表结构:") + cursor.execute("DESCRIBE news") + for row in cursor.fetchall(): + print(f" - {row[0]}: {row[1]} {row[2]}") + + except Exception as e: + print(f"\n>>> 迁移失败:{str(e)}") + if 'connection' in locals(): + connection.rollback() + raise + + finally: + if 'cursor' in locals(): + cursor.close() + if 'connection' in locals(): + connection.close() + print("\n数据库连接已关闭") + + +if __name__ == '__main__': + migrate() diff --git a/models.py b/models.py index fae1c97..6de887f 100644 --- a/models.py +++ b/models.py @@ -90,6 +90,8 @@ class News(db.Model): content = db.Column(db.Text, comment='新闻内容') news_type = db.Column(db.String(50), default='Industry News', comment='新闻类型') url = db.Column(db.String(500), comment='新闻链接') + source_name = db.Column(db.String(100), comment='新闻来源网站名称') + source_icon = db.Column(db.String(500), comment='新闻来源网站图标URL') published_at = db.Column(db.DateTime, default=datetime.now, comment='发布时间') is_active = db.Column(db.Boolean, default=True, comment='是否启用') created_at = db.Column(db.DateTime, default=datetime.now, comment='创建时间') @@ -110,6 +112,8 @@ class News(db.Model): 'content': self.content, 'news_type': self.news_type, 'url': self.url, + 'source_name': self.source_name, + 'source_icon': self.source_icon, 'published_at': self.published_at.strftime('%Y-%m-%d') if self.published_at else None, 'created_at': self.created_at.strftime('%Y-%m-%d %H:%M:%S') if self.created_at else None } diff --git a/templates/detail_new.html b/templates/detail_new.html index c84192f..18b43a1 100644 --- a/templates/detail_new.html +++ b/templates/detail_new.html @@ -628,10 +628,43 @@ {% for news in news_list %}
{{ news.content[:200] }}...
-{{ news.content[:200] }}{% if news.content|length > 200 %}...{% endif %}
+ {% endif %} +