release: v2.2.0 - 博查新闻搜索功能 (生产环境部署版)

核心功能:
  - 集成博查Web Search API自动获取网站相关新闻
  - 智能新闻更新机制(每日首次访问触发)
  - 精确新闻搜索(使用引号强制匹配网站名称)
  - News模型扩展(source_name, source_icon字段)
  - 网站详情页新闻展示模块
  - 新闻来源网站信息展示
  - 自动去重防止重复新闻

  技术实现:
  - NewsSearcher工具类封装博查API
  - 数据库迁移脚本migrate_news_fields.py
  - 测试脚本test_news_feature.py
  - 定期任务脚本fetch_news_cron.py
  - API路由:/api/fetch-site-news, /api/fetch-all-news

  配置优化:
  - 修复manage.sh路径和启动命令
  - 博查API配置(BOCHA_API_KEY, BOCHA_BASE_URL)
  - 新闻搜索参数配置

  界面优化:
  - 详情页新闻模块(左侧主栏)
  - 相似推荐模块(右侧边栏)
  - 首页标签图标修复
  - 后台添加修改密码功能
  - 登录页面优化

  部署信息:
  - 部署日期: 2025-12-30
  - 部署方式: 手动上传文件
  - 数据库: 已迁移(添加source_name和source_icon字段)
This commit is contained in:
ZJPB Admin
2025-12-30 23:44:27 +08:00
parent 9f5d006090
commit b00e52e1e0
12 changed files with 1255 additions and 107 deletions

278
app.py
View File

@@ -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'):
"""应用工厂函数"""
@@ -115,6 +116,70 @@ def create_app(config_name='default'):
site.view_count += 1
db.session.commit()
# 智能新闻更新:检查今天是否已更新过新闻
from datetime import date
today = date.today()
# 检查该网站最新一条新闻的创建时间
latest_news = News.query.filter_by(
site_id=site.id
).order_by(News.created_at.desc()).first()
# 判断是否需要更新新闻
need_update = False
if not latest_news:
# 没有任何新闻,需要获取
need_update = True
elif latest_news.created_at.date() < today:
# 最新新闻不是今天创建的,需要更新
need_update = True
# 如果需要更新,自动获取最新新闻
if need_update:
api_key = app.config.get('BOCHA_API_KEY')
if api_key:
try:
# 创建新闻搜索器
searcher = NewsSearcher(api_key)
# 获取新闻限制3条一周内的
news_items = searcher.search_site_news(
site_name=site.name,
site_url=site.url,
count=3,
freshness='oneWeek'
)
# 保存新闻到数据库
if news_items:
for item in news_items:
# 检查是否已存在根据URL去重
existing = News.query.filter_by(
site_id=site.id,
url=item['url']
).first()
if not existing:
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)
db.session.commit()
except Exception as e:
# 获取新闻失败,不影响页面显示
print(f"自动获取新闻失败:{str(e)}")
db.session.rollback()
# 获取该网站的相关新闻最多显示5条
news_list = News.query.filter_by(
site_id=site.id,
@@ -442,6 +507,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 +1172,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 +1182,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 +1202,9 @@ def create_app(config_name='default'):
]
}
# 默认排序
column_default_sort = ('published_at', True) # 按发布时间倒序排列
# Prompt模板管理视图
class PromptAdmin(SecureModelView):
can_edit = True