From 87a2514bb395dbd6a420b70f7c75c2758cd090c1 Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Tue, 29 Jul 2025 19:50:47 +0800 Subject: [PATCH 1/2] fix the document problem --- api/__pycache__/main.cpython-312.pyc | Bin 3233 -> 3233 bytes .../content_integration.cpython-312.pyc | Bin 7828 -> 7634 bytes api/routers/content_integration.py | 70 ++++++----- ...ontent_integration_service.cpython-312.pyc | Bin 12250 -> 12278 bytes .../database_service.cpython-312.pyc | Bin 45319 -> 50123 bytes api/services/content_integration_service.py | 3 +- api/services/database_service.py | 109 +++++++++++++++++- config/database.json | 7 +- 8 files changed, 144 insertions(+), 45 deletions(-) diff --git a/api/__pycache__/main.cpython-312.pyc b/api/__pycache__/main.cpython-312.pyc index 834e7c3d16f9e450ab31dff484181a08d93275a3..a0d2a6442db42dafba7069b486f0f438db13d803 100644 GIT binary patch delta 20 acmZ1|xloe(G%qg~0}ym=?%2pZjRycYrv;J# delta 20 acmZ1|xloe(G%qg~0}yOqU$K#U8V>+DdIhTh diff --git a/api/routers/__pycache__/content_integration.cpython-312.pyc b/api/routers/__pycache__/content_integration.cpython-312.pyc index a923656e2337409e7a51b96ae8acec90c3c255f2..d3bd16ee5e21f992c6815264dc62c8bb05e82712 100644 GIT binary patch delta 3001 zcmbVOYj6|S6~1?0(ptzemY=fT1{+~yLu}q)UJe9o13utnSd6{J@4zr!z|MOdkAb@0Ge3sh+G15=VhYE~F$Pt(RwUQ%>(egMcTgYAJ zI2EJB`5ehn9L+JDhSPHFJ4~F8(SC#6OdCnUpMwDkv()WzN;0b9{CU`C)5WOEb5#FQ z5NB8*6+T;2gmL79Su7LR#29}Go*FclPzLycAO`)GbSTDel}}KnYzQQK+J}U{6i3*q zM;ooGZF)QfP7~8WEZ~gzPh|p8p^j73EHq7F3<^5QEjLs7Y|$ye6~}XDVHMGAPGhPevM%6Ywc0rMVzzdMKX4b zkS}ReAb%ZZT=mQc!SR(8comcaX`Z@f9uPLgwa*p5Jbk>`G40GkOD=T>XW(+=0c~+` zu2iAgVp=sm+C^Q?RgdE^gYwf@Ci5emcc0bb%2 zMvNlJuBZv6HF>Oe;;n&)e>gqyLI1;{Ym=y||CrWKnRBZKxITVQFr8Zw+ zdsL4$O`Ln9$$MOg)(Gjc!6q@*XipC1;}sd_scrN3b<3= zWO&J3s<3LhqN4GHLlP>9LW`)(l00Cr8AvZLDQwUeWW~bwcQusMTJBe2>7BC zaD^3JhHSVM^^L+lTc;Q@R5?`2OqLKa6nHTjkVO3d+5YU2|CY19XB7s-!h&9L5Z%p> z!M6uNstn}j9o&{8wn4f9eyPhH*O|w3=47FDyl}z&!Uer#iYS{~@TJu;u>a)#-Yv;O zQ@{7bfuz%Q-hI|RNDelSIhPN?QRk|D%@hNy_JQ3ecaK}#qZaoC&qslaficVKp$(&! zp4#5ce=c#P7*Me!S+z9z(u$-vlw7zt>8koluPHb6>ry$u={|2eYa2BF)^f#iof&g% z>1Q4mJI0G^MvH5PUcOm&zqmQ^+TIkERcw7wSu?$-I)jrV~wf^IT^H*_-Cz3QuPmfKE7l^@XO2w_^4}0vhXX8g08# zT~j}gx>=4PZZ2fEF4NqsuHCvsb888W^jphFgmh~qy^UmV)ncH}8MfX=e{R^It2fcZ zrXCv2hHVtW8=l8D7&XK0l7?)}h?YTkBiS^vj2JQYNCDeeNRHU58*|Cqxir#m7m|%8 zaNEQ*vE*$>&GrK3PCi8XodULrV(-{Wwy$IFtfH}H9gQqsP+0$h#c17{st&#_Aaz_d z7TkjM`KSonU5v02jlxnasR6Kq)6v2e(LEOF{~AoU4) zflw!>N{Z;6NbQ&P=69JgY^;=jWA3ZRMA63?j!NjJ!2A^|5=IwA`lDQk*J18yZUP1n zFskq>7U>kUYl2}e8sKY%R=Lm8wO|i~4KOjkHU%=;rafP0v?nAWHp3K=HmBCh%dIUG GhWRHV0sxx; delta 3021 zcmah~T~Hg>72d1e)emS9mJsL%OF}}nWMeF2FkoUF>|kRAn2gglz@&`KE<&>YW_MLc zE0v6$xK4*oX*UhQZU)@AV|z?nJTQ$r%}YGvnKpne89Zoq`jCe};)hOX%`}3KcpL?xPmmDG@FL-g+$!|rwWcZjmMk|>gC%iZle3{)q3{n< zhs%OmA&HyhNg{y<^$5}sw?NJbUtK$62Jv5 zy)aAa2$;=-2B0bnJ%kQ_CyxzJX>O@tX5^(Wm2yh^BE@TT^v7?c653j^>wDjTs4;a%7uaA3SiYF0VMb|%SZj%bzyAceh3 zqg9?DA&F;BJYOco(F$6Le7aP;L&6eI&m@YP%9C1q9?(RR{4qHQ2C*E2T5J@I+}*;% zxt+&pTLOm{VjGVxc&1Rsk<`}ATXNQI&)Hsy|{Ai_7}6i6?!P6aFg;1|Dw(f ziwZ8@*T-<&E24@Igc*+ag=3;7#zqksi1>REDpG++EYA0G@o?D3zAjR9v@afJB7AQk z(jOI-LjzpS(gq?t<7a(5L-+PYp&j8xier3i-vAo-2mEdoYlDnfv>U8_7)f-z*m5MC zNbEuay^A8nuxym=g{(NApNS)vm5Xn!{Qgg?znWbcy|j91bmgZLUw$-_t8R7dkB`S@ zzx?R38;D98Qb5dqNIAQlVJF?|Y;m?b&%_Cs!}Iy|U$QP}#Px|ST<)NtG&#)qyEsjNmM4aPUk;wKE4>1weApA^IF^rxF7Tsgkj0F0VVRs^7 zLBfiJ5(zgHBJO8+5syR%MI}tupWQV%GZf%>PL#(Zp+MvuY<&T8&EuJHthYZ9VmJ|p z#lfAI0Ja)A07#!@3|u_Ki%OQ^V$lf4WS8O4ABu3Y@YL{8Xs}2iM%+65bNK@M`VDMt z7p5(GqjgQz0mwG+X})$jzw}{#X~t;2)S4-=ESI>JN?fD(noPZ;_%mbKvavdCtiIDc zZ-21+ue+C9yVI@Rsh;mI_xRI2{>9dTR3MmcJ@?2MO2uCP%R`jKiJU)0lP~eL7*Wuj}#*1(#aZi!n;~bMM>Ul&SH~4^k(-mp=MxsrfsxNn8r<$t&7I}kkj?UR}ZHuk1aZmKj-vJz;h(q3~#rk z%%17uJDT)?mXzmI%GkQ3_I|1<5#G`6r@m^Ip$4Xnq5rJs2@YKs@$dBr5QcRQkQP4B zO%E@@%|DNK+QG%0?G@OyCg3fVee7&fdo|?U14=l!TYu~boVj-hwAB&!YF{$L!6#Z0 zMtowZZfhXs)F^sRLwb#>xgr!XSE_2OA?KV;`E4F@-b2E{d<_A^<{MP)Zp8v&ZZB6X zSW$|FSIbERxlxXV3RTBGe4*xGhYP>&BB6bMAKu}??tA2&R{VZ#_2~-vgS`OS4=Plh zCe?!)bEjUFDkLG2(vuKLnX ContentIntegr try: # 创建临时文件处理base64文档 if request.documents: - temp_files = [] for doc in request.documents: try: + # 从base64内容中提取实际内容(跳过data:image/jpeg;base64,这样的前缀) + content = doc.content + if ',' in content: + content = content.split(',', 1)[1] + # 创建临时文件 - with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(doc.filename)[1]) as temp_file: + suffix = os.path.splitext(doc.filename)[1] + if not suffix: + # 根据MIME类型推断后缀 + mime_to_ext = { + 'text/plain': '.txt', + 'application/pdf': '.pdf', + 'application/msword': '.doc', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', + 'image/jpeg': '.jpg', + 'image/png': '.png' + } + suffix = mime_to_ext.get(doc.mime_type, '.bin') + + with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as temp_file: # 解码base64内容并写入临时文件 - content = base64.b64decode(doc.content) - temp_file.write(content) - temp_files.append(temp_file.name) + try: + decoded_content = base64.b64decode(content) + temp_file.write(decoded_content) + temp_files.append(temp_file.name) + logger.info(f"成功保存临时文件: {temp_file.name}") + except Exception as e: + logger.error(f"Base64解码失败: {e}") + raise HTTPException( + status_code=400, + detail=f"文档 {doc.filename} 的Base64内容无效: {str(e)}" + ) except Exception as e: logger.error(f"处理文档 {doc.filename} 失败: {e}") raise HTTPException( @@ -70,8 +95,8 @@ async def integrate_content(request: ContentIntegrationRequest) -> ContentIntegr # 调用服务层处理 result = await integration_service.integrate_content( document_paths=temp_files, - keywords=request.keywords, - cookies=request.cookies, + keywords=request.keywords or [], + cookies=request.cookies or "", sort_type=request.sort_type, note_type=request.note_type, note_time=request.note_time, @@ -80,36 +105,7 @@ async def integrate_content(request: ContentIntegrationRequest) -> ContentIntegr query_num=request.query_num ) - # 转换为响应模型 - if result["success"]: - response = ContentIntegrationResponse( - success=True, - timestamp=result["timestamp"], - processing_time=result["processing_time"], - input_summary=result["input_summary"], - document_info=result["document_info"], - xhs_info=result["xhs_info"], - integrated_content=result["integrated_content"], - search_config=result["search_config"], - error_message=None # 成功时无错误信息 - ) - logger.info(f"内容整合成功,处理时间:{result['processing_time']}") - else: - from datetime import datetime - response = ContentIntegrationResponse( - success=False, - timestamp=result.get("timestamp", datetime.now().strftime("%Y%m%d_%H%M%S")), - processing_time=result.get("processing_time", "0秒"), - input_summary=result.get("input_summary"), - document_info=result.get("document_info"), - xhs_info=result.get("xhs_info"), - integrated_content=result.get("integrated_content"), - search_config=result.get("search_config"), - error_message=result.get("error_message") - ) - logger.error(f"内容整合失败:{result['error_message']}") - - return response + return result except Exception as e: logger.error(f"内容整合接口异常:{e}", exc_info=True) diff --git a/api/services/__pycache__/content_integration_service.cpython-312.pyc b/api/services/__pycache__/content_integration_service.cpython-312.pyc index 8af7c34d6e78dd8cfffc11a3abfc000f61b86f85..4766af2c1daf611ef76c02ef3fce522fc8859b89 100644 GIT binary patch delta 684 zcmXYsO=uHA6vyZ7?Du9jO|scdV(KS_)Ko?E(4x{W!5plz#*;`B=pHIoCDBVUu?I!k zlhFY+cnJuCw2Hb2q80I2+Tw;35f9?QgBNkB;zbJ1Y<&ao{r+#}J^u5#^rxgR>$-|) z6kg6w{&=px6`@6z#dFyjSJkda?1n{+n}UV-s`m#(QGr02aNE z_IDndR(K}8t)7o_xs2Eu~NG-y;h#^-Kn+mg#e);ZkVH`3?r;0 zN4eh!DMvYPBkS-pV$p#2N+IT$Xek2@;+M9J8dR+G9&~VIwR=RT;=}HYBV(UGMA;t% zI?_f!xs3&uWxW+sPzIc@$ zTZDCQE`OMtInN2(zoxSHi%lgEmP1)ZS@`Rxsq&5zEz` G)cHT-i?Wvh delta 676 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 W$9tJu`1<=Inx9H1-lPzUqx*mVK_d15 delta 6670 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 diff --git a/api/services/content_integration_service.py b/api/services/content_integration_service.py index 043eaae..672145f 100644 --- a/api/services/content_integration_service.py +++ b/api/services/content_integration_service.py @@ -78,7 +78,8 @@ class ContentIntegrationService: 整合结果字典 """ start_time = time.time() - logger.info(f"开始整合任务:文档数量 {len(document_paths)}, 关键词数量 {len(keywords)}") + + logger.info(f"开始整合任务:文档数量 {len(document_paths)}, 关键词数量 {len(keywords) if keywords else 0}") try: # 确保输出目录存在 diff --git a/api/services/database_service.py b/api/services/database_service.py index 8d3a855..641fc3b 100644 --- a/api/services/database_service.py +++ b/api/services/database_service.py @@ -12,12 +12,46 @@ import traceback from typing import Dict, Any, Optional, List, Tuple import mysql.connector from mysql.connector import pooling +from functools import wraps from core.config import ConfigManager logger = logging.getLogger(__name__) +def database_retry(max_retries: int = 3, delay: float = 1.0): + """数据库查询重试装饰器""" + def decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + if not self.db_pool: + logger.error("数据库连接池未初始化,尝试重新初始化...") + self.db_pool = self._init_db_pool() + if not self.db_pool: + logger.error("数据库连接池重新初始化失败,返回兜底数据") + return self._get_fallback_data(func.__name__) + + last_exception = None + current_delay = delay + + for attempt in range(max_retries): + try: + return func(self, *args, **kwargs) + except Exception as e: + last_exception = e + logger.warning(f"数据库查询 {func.__name__} 第 {attempt + 1} 次失败: {e}") + if attempt < max_retries - 1: + time.sleep(current_delay) + current_delay *= 2 + + # 所有重试都失败,返回兜底数据 + logger.error(f"数据库查询 {func.__name__} 在 {max_retries} 次重试后仍然失败: {last_exception}") + return self._get_fallback_data(func.__name__) + + return wrapper + return decorator + + class DatabaseService: """数据库服务类""" @@ -29,7 +63,46 @@ class DatabaseService: config_manager: 配置管理器 """ self.config_manager = config_manager + + # 从配置获取数据库相关设置 + db_config = config_manager.get_raw_config('database') + self.pool_size = db_config.get('pool_size', 10) + self.max_retry_attempts = db_config.get('max_retry_attempts', 3) + self.query_timeout = db_config.get('query_timeout', 30) + self.soft_delete_field = db_config.get('soft_delete_field', 'isDelete') + self.active_record_value = db_config.get('active_record_value', 0) + self.db_pool = self._init_db_pool() + + # 兜底数据缓存 + self._fallback_cache = { + 'styles': [], + 'audiences': [], + 'scenic_spots': [], + 'products': [], + 'materials': [] + } + + def _get_fallback_data(self, func_name: str) -> Any: + """获取兜底数据""" + fallback_mapping = { + 'get_all_styles': self._fallback_cache['styles'], + 'get_all_audiences': self._fallback_cache['audiences'], + 'get_scenic_spot_by_id': None, + 'get_product_by_id': None, + 'get_style_by_id': None, + 'get_audience_by_id': None, + 'get_scenic_spots_by_ids': [], + 'get_products_by_ids': [], + 'get_styles_by_ids': [], + 'get_audiences_by_ids': [], + 'get_content_by_id': None, + 'get_content_by_topic_index': None, + } + + result = fallback_mapping.get(func_name, None) + logger.info(f"使用兜底数据 for {func_name}: {type(result)}") + return result def _init_db_pool(self): """初始化数据库连接池""" @@ -57,7 +130,7 @@ class DatabaseService: # 创建连接池 pool = pooling.MySQLConnectionPool( pool_name=f"database_service_pool_{int(time.time())}", - pool_size=10, + pool_size=self.pool_size, **attempt["config"] ) @@ -90,18 +163,16 @@ class DatabaseService: return processed_config + @database_retry(max_retries=3, delay=1.0) def get_scenic_spot_by_id(self, spot_id: int) -> Optional[Dict[str, Any]]: """根据ID获取单个景区信息""" - if not self.db_pool: - logger.error("数据库连接池未初始化") - return None try: with self.db_pool.get_connection() as conn: with conn.cursor(dictionary=True) as cursor: cursor.execute( - "SELECT * FROM scenicSpot WHERE id = %s AND isDelete = 0", - (spot_id,) + f"SELECT * FROM scenicSpot WHERE id = %s AND {self.soft_delete_field} = %s", + (spot_id, self.active_record_value) ) result = cursor.fetchone() if result: @@ -114,6 +185,7 @@ class DatabaseService: logger.error(f"查询景区信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_product_by_id(self, product_id: int) -> Optional[Dict[str, Any]]: """根据ID获取单个产品信息""" if not self.db_pool: @@ -133,6 +205,7 @@ class DatabaseService: logger.error(f"查询产品信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_style_by_id(self, style_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取风格信息 @@ -166,6 +239,7 @@ class DatabaseService: logger.error(f"查询风格信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_audience_by_id(self, audience_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取受众信息 @@ -199,6 +273,7 @@ class DatabaseService: logger.error(f"查询受众信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_scenic_spots_by_ids(self, spot_ids: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取景区信息 @@ -227,6 +302,7 @@ class DatabaseService: logger.error(f"批量查询景区信息失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def get_products_by_ids(self, productIds: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取产品信息 @@ -255,6 +331,7 @@ class DatabaseService: logger.error(f"批量查询产品信息失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def get_styles_by_ids(self, styleIds: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取风格信息 @@ -283,6 +360,7 @@ class DatabaseService: logger.error(f"批量查询风格信息失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def get_audiences_by_ids(self, audienceIds: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取受众信息 @@ -311,6 +389,7 @@ class DatabaseService: logger.error(f"批量查询受众信息失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def list_all_scenic_spots(self, user_id: Optional[int] = None, is_public: Optional[bool] = None) -> List[Dict[str, Any]]: """ 获取所有景区列表 @@ -356,6 +435,7 @@ class DatabaseService: logger.error(f"获取景区列表失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def list_all_products(self, user_id: Optional[int] = None, is_public: Optional[bool] = None) -> List[Dict[str, Any]]: """ 获取所有产品列表 @@ -403,6 +483,7 @@ class DatabaseService: logger.error(f"获取产品列表失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def list_all_styles(self) -> List[Dict[str, Any]]: """ 获取所有风格列表 @@ -425,6 +506,7 @@ class DatabaseService: logger.error(f"获取风格列表失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def list_all_audiences(self) -> List[Dict[str, Any]]: """ 获取所有受众列表 @@ -458,6 +540,7 @@ class DatabaseService: # 名称到ID的反向查询方法 + @database_retry(max_retries=3, delay=1.0) def get_style_id_by_name(self, style_name: str) -> Optional[int]: """ 根据风格名称获取风格ID @@ -490,6 +573,7 @@ class DatabaseService: logger.error(f"查询风格ID失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_audience_id_by_name(self, audience_name: str) -> Optional[int]: """ 根据受众名称获取受众ID @@ -522,6 +606,7 @@ class DatabaseService: logger.error(f"查询受众ID失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_scenic_spot_id_by_name(self, spot_name: str) -> Optional[int]: """ 根据景区名称获取景区ID @@ -554,6 +639,7 @@ class DatabaseService: logger.error(f"查询景区ID失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_product_id_by_name(self, product_name: str) -> Optional[int]: """ 根据产品名称获取产品ID @@ -588,6 +674,7 @@ class DatabaseService: + @database_retry(max_retries=3, delay=1.0) def get_image_by_id(self, image_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取图像信息 @@ -621,6 +708,7 @@ class DatabaseService: logger.error(f"查询图像信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_images_by_ids(self, image_ids: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取图像信息 @@ -649,6 +737,7 @@ class DatabaseService: logger.error(f"批量查询图像信息失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def get_content_by_id(self, content_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取内容信息 @@ -682,6 +771,7 @@ class DatabaseService: logger.error(f"查询内容信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_content_by_topic_index(self, topic_index: str) -> Optional[Dict[str, Any]]: """根据主题索引获取内容信息""" if not self.db_pool: @@ -704,6 +794,7 @@ class DatabaseService: logger.error(f"查询内容信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_images_by_folder_id(self, folder_id: int) -> List[Dict[str, Any]]: """ 根据文件夹ID获取图像列表 @@ -732,6 +823,7 @@ class DatabaseService: logger.error(f"根据文件夹ID获取图像失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def get_folder_by_id(self, folder_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取文件夹信息 @@ -765,6 +857,7 @@ class DatabaseService: logger.error(f"查询文件夹信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_related_images_for_content(self, content_id: int, limit: int = 10) -> List[Dict[str, Any]]: """ 获取与内容相关的图像列表 @@ -807,6 +900,7 @@ class DatabaseService: # 模板相关查询方法 + @database_retry(max_retries=3, delay=1.0) def get_all_poster_templates(self) -> List[Dict[str, Any]]: """ 获取所有海报模板 @@ -831,6 +925,7 @@ class DatabaseService: logger.error(f"获取海报模板列表失败: {e}") return [] + @database_retry(max_retries=3, delay=1.0) def get_poster_template_by_id(self, template_id: str) -> Optional[Dict[str, Any]]: """ 根据ID获取海报模板信息 @@ -864,6 +959,7 @@ class DatabaseService: logger.error(f"查询模板信息失败: {e}") return None + @database_retry(max_retries=3, delay=1.0) def get_active_poster_templates(self) -> List[Dict[str, Any]]: """ 获取所有激活的海报模板 @@ -935,6 +1031,7 @@ class DatabaseService: except Exception as e: logger.error(f"更新模板使用统计失败: {e}") + @database_retry(max_retries=3, delay=1.0) def get_template_usage_stats(self, template_id: str) -> Optional[Dict[str, Any]]: """ 获取模板使用统计 diff --git a/config/database.json b/config/database.json index cbc6c4b..4ec8470 100644 --- a/config/database.json +++ b/config/database.json @@ -4,5 +4,10 @@ "password": "Kj#9mP2$", "database": "travel_content", "port": 3306, - "charset": "utf8mb4" + "charset": "utf8mb4", + "pool_size": 10, + "max_retry_attempts": 3, + "query_timeout": 30, + "soft_delete_field": "isDelete", + "active_record_value": 0 } \ No newline at end of file From 66bfad2f3c96babaffffa594eb6aa2fbf48a48cd Mon Sep 17 00:00:00 2001 From: jinye_huang Date: Tue, 29 Jul 2025 20:37:43 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=BA=86=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/__pycache__/dependencies.cpython-312.pyc | Bin 3856 -> 3856 bytes api/__pycache__/main.cpython-312.pyc | Bin 3233 -> 3233 bytes .../__pycache__/document.cpython-312.pyc | Bin 14460 -> 14460 bytes .../__pycache__/poster.cpython-312.pyc | Bin 7616 -> 7616 bytes api/routers/__pycache__/tweet.cpython-312.pyc | Bin 14723 -> 17308 bytes api/routers/tweet.py | 84 +++++- .../database_service.cpython-312.pyc | Bin 50123 -> 51756 bytes .../prompt_service.cpython-312.pyc | Bin 32131 -> 32462 bytes .../__pycache__/tweet.cpython-312.pyc | Bin 16888 -> 20543 bytes api/services/database_service.py | 266 ++++++++++-------- api/services/prompt_service.py | 17 +- api/services/tweet.py | 117 ++++++-- core/algorithms/__init__.py | 8 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 263 bytes .../topic_generator.cpython-312.pyc | Bin 0 -> 7123 bytes .../__pycache__/topic_parser.cpython-312.pyc | Bin 0 -> 2622 bytes core/algorithms/content_generator.py | 142 ++++++++++ core/algorithms/content_judger.py | 212 ++++++++++++++ core/algorithms/topic_generator.py | 149 ++++++++++ core/algorithms/topic_parser.py | 59 ++++ .../config/__pycache__/models.cpython-312.pyc | Bin 9594 -> 9594 bytes docs/ID映射机制优化说明.md | 148 ++++++++++ tweet/__pycache__/__init__.cpython-312.pyc | Bin 253 -> 253 bytes .../content_generator.cpython-312.pyc | Bin 7711 -> 7711 bytes .../content_judger.cpython-312.pyc | Bin 9467 -> 9467 bytes .../topic_generator.cpython-312.pyc | Bin 7113 -> 7113 bytes .../__pycache__/topic_parser.cpython-312.pyc | Bin 2612 -> 2612 bytes utils/__pycache__/prompts.cpython-312.pyc | Bin 25292 -> 25292 bytes 28 files changed, 1043 insertions(+), 159 deletions(-) create mode 100644 core/algorithms/__init__.py create mode 100644 core/algorithms/__pycache__/__init__.cpython-312.pyc create mode 100644 core/algorithms/__pycache__/topic_generator.cpython-312.pyc create mode 100644 core/algorithms/__pycache__/topic_parser.cpython-312.pyc create mode 100644 core/algorithms/content_generator.py create mode 100644 core/algorithms/content_judger.py create mode 100644 core/algorithms/topic_generator.py create mode 100644 core/algorithms/topic_parser.py create mode 100644 docs/ID映射机制优化说明.md diff --git a/api/__pycache__/dependencies.cpython-312.pyc b/api/__pycache__/dependencies.cpython-312.pyc index ff6f81cb4da7bb6dcbab4a4a2844e73aa06e354d..4ae72d294ce7a491f7e6fbed923e082df89a1264 100644 GIT binary patch delta 20 acmbOrH$jg3G%qg~0}v$a?AXZ7$`1fEv;@ci delta 20 acmbOrH$jg3G%qg~0}$v6mTlx_J(JaU-Ee^H60njFlByEKV4Z z_F~@mNfoQ2^sH(ahZ&@~2qUW`OcJj-Zj9!s%&VxR7EoDMQK?xqP^l#<$ytf&0g0+; z$*H1MOi~M&tgDzbtOl4gz!X&w(rP8DVu{MOL}gn=CAEO6WEGW`)dH1Paw-=sv_3mx zV)Ovg6EhCsMCl0);poOL=5-fwBpk%BN!_v`s~k1v`&YbH4}lSSR!2AwcSG>USdb@R zBEmJHgdPK9DhwQbfcx5ERhGeqTanaNPE-`O2vIfF^Rza*`_0Xzfb%539wK=yi z49B3q<}hp{wQiY{OS^5$jGonuwRy(1=anm1*1a# z7WayJW{?^t8DD5{j1<*lgX80Y&@i}5cPHbl3)H@w+&xFe3UbhwAYLB~aS9gb7#J!UB z!t{#^GZ%9cEm=qGi09mg;K)KOy}}D%iaY>)o`$G<;q;|%Ur*hgir<~R(&6@ML^VZ@ z2M7J6sH7PxFfJ+s3^_(qyTD#l1VTe$YPTc?12iKl4wJ`duaY_dDhf&fCK~eRz#k+B zL)61ar3w(fBZEO2Wvmn)su`)xLxE6~@I6D)jBjj^@sCXW$~Q<5z5qe{m@xbs5BTZE z&S2ONW4AVf^-AL@pr9L}^h{yjYcPvz)|0i`7gdP0bK2k7|O*QJWBS zU>sX!h(7F9h(?+@79?}~KoX*5FhT^#kbhZYrTt_m;P=tv;rw=inR%QF6A}NCf?A7c z9|Y*9Mv>@2unoas1T_e@BM2g>MetA*;TV8lx=27k5Bi*Fh2On)fH&|SsN<+y4nW>c z;w&D|l>tp0N+BwjP!dc8_9lJ?$FXUv0#4)~<?HndUqPA@<>8XqXNUtW_{Zd{RyuJ`?Zmx`T)3iAkfhK^0YTTrT>>DYMiQLNcN1=^ z3giiSZ`1Y&x)4>DwKKT%*zQh_PJHtwG`=hl;;kHC-qErb?!YO%zr zEPk(V5rfyCHhe7qxbL#&qpec&ZzEsOe;vDrdKTL;yJPCHZ@99Oj;IE!sLr};v(Y>kKLAT}fqf_+8VyeQh$5+BM9|lyY<$y$H@w^Co!(R}-@Hwz-_CdS@%=+W*Ko!; z66={aJEPf_&a@FICI!p7?1uIkBVW-hSXyrDtuw$;mb4|0zFRq8Qkx`GJ7>e-0KaoU zXgicC@x^w`n@h5-9ceENj|!HiY};luyj8GlxUDZvCFQJ1o%jtSmP z{N^71kpW@zp^U>9+dXfNIUhci#!9V#Fzk5Hwe}F#}Mg~XQ-v~!jsJ9n?lhfi{SOT~(of9hts~ z;0&j$-iUv|ZLfB(J&P3R!llk3K>y2;POH~Q-S5v=4Jyxj&=23A=)Uy9B}pEo(5IYe z9E}jeq>qmH{Ul9O?{lA5yYzoRTnHTa-1pUIb{V>B+L6l1>L>W1+KH9!m=VDp!J%#aG8@k(NNqh z4MZPi)X)0%AEV=5@=`QO0U>;aE%*qrNnz3b8Hosn=jzw}q)_G30Cv2zSW6!cjPFLF zq&@(HUMSlfw`cvfRWVDhU-#g0>Rd)_$e4oL>K7a~tO%x*UIt3?R;sC2xTaQHtrS#r za}-J%dgnrW6>%eYV=mBoK-Nh;3$mzWBI7}l+JidxA!tQ_N}#A155p&&l($kENP8o# z{OW5P%XNPT8+2RJ`{q0w2XXrdj>Gp#vO8r>`BSB{y+UQ{A~u3^=iAC<@VeUOP$QGY LC*^Zu+XejJ@XBVK delta 1715 zcmZ{kUr1Y59LMi9e@t@KYqgrBF-D73vL-PaurjRwp=)t=R_CU!O>ORNOsYw^H>uj) zG>R>i;h5{)W(s3ueJI(UI`T3mgFOzmhjR*JA&k|r*F89d!JfABJJBo-yl_8!&+q=u z_niCtoqIoh#BRtnKU7zn7;^1Cf3Pst`&G@Cb#^ToYRG-?9?LL0Opr-dD9k8xeV1X9 z8uhukQ5~}yQb}!)QM3ZvM>46ZC@D=zX+a4(MRS*t?21OvS;<(k9I2#E(J8E;hbYOY zI?@Ww3$jQ4LS@OJ^3Wlr5vWY+6+M||D6^nip{CS=ZcH$iER3a1P?fAyDv5)s%zBC%3@Fq#G3Fi`kS-|AfOE}1ULs6CU901w1H@BwVn*VtYU4`}3auuT$7_6}>TwJ#)zY&UQ`u96pw598&cS?B zD>~ban|zzE=lLeo;zeB}xMdXcqxr5OG(3g7-bB*@+$A8dNGy^#RqNj4>bHqUQ>G!a z^3;*H*wYJ7_|40C-h!H3$UTgkMle5;_l=;@8SI-ya{~5>sQWgVpTf1Bd)(=?4s-U* zxvcTU+3vipC1b_5^P6LN-i92V$UB4`!4Q#Y}99(CR#28spOy7mm_ zpAF5}(44;ilfkn;z|>jxhjZ(#9vv4VRg=Pifcj0lOMe?=Q2nF*UG}=V(9veP3jx5h z+$S9ky4pK1UIZ)wBI>TIg^jAexUB30^)FX57lW__kX47fhuu)8-7T%l5V#Cj0YD2S zAAz%4DlKVuy#?Vlf&xcRjXx9-#1-igM5@CHAt?IgL?9r_vb3)5xozAgOaQ!Oa(d5> zhJC1h*41-t4==lVCr?~mf4Dl>tO2$Fu%5I|AnOT=Cr$D~@Pi}pxp4Q78XL|0e@RHr zxb#q+=s8}Ddp*AMG@l2P6Sq`e5Ux}ZY3uQY)B5*eCVg|$hads&+`xsuwfbTs)j!+K z4F#ioCltE^<&ri?#<)`}Mb&WMU|m^nNZeT~y{)eISYE4=5)pqa6cZOi5mEZ@NPGlq zfP-`jaN?G$%6izk2C4Kv@G(&Zp0-W!2cnU<7>PsUW-p#G?LZb_E_eUpT!pTn2}R=S zuLI^w@GTd#v8WuU$BuTlSaA% ztSueSjArYzfvn~8aqRHzGbHw1{QD6!LB7-(G Optional[int]: + """ + 寻找最佳匹配的ID,支持模糊匹配 + """ + if not target_name or not mapping: + return None + + # 1. 精确匹配 + if target_name in mapping: + return mapping[target_name] + + # 2. 模糊匹配 - 去除空格后匹配 + target_clean = target_name.replace(" ", "").strip() + for name, id_val in mapping.items(): + if name.replace(" ", "").strip() == target_clean: + logger.info(f"模糊匹配成功: '{target_name}' -> '{name}' (ID: {id_val})") + return id_val + + # 3. 包含匹配 - 检查是否互相包含 + for name, id_val in mapping.items(): + if target_clean in name.replace(" ", "") or name.replace(" ", "") in target_clean: + logger.info(f"包含匹配成功: '{target_name}' -> '{name}' (ID: {id_val})") + return id_val + + # 4. 未找到匹配 + logger.warning(f"未找到匹配的ID: '{target_name}', 可用选项: {list(mapping.keys())}") + return None + enriched_topics = [] for topic in topics: # 复制原topic enriched_topic = topic.copy() - # 添加ID字段 + # 初始化ID字段 enriched_topic['styleIds'] = [] enriched_topic['audienceIds'] = [] enriched_topic['scenicSpotIds'] = [] enriched_topic['productIds'] = [] + # 记录匹配结果 + match_results = { + 'style_matched': False, + 'audience_matched': False, + 'scenic_spot_matched': False, + 'product_matched': False + } + # 根据topic中的name查找对应的ID if 'style' in topic and topic['style']: - style_name = topic['style'] - if style_name in id_name_mappings['style_mapping']: - enriched_topic['styleIds'] = [id_name_mappings['style_mapping'][style_name]] + style_id = find_best_match(topic['style'], id_name_mappings['style_mapping']) + if style_id: + enriched_topic['styleIds'] = [style_id] + match_results['style_matched'] = True if 'targetAudience' in topic and topic['targetAudience']: - audience_name = topic['targetAudience'] - if audience_name in id_name_mappings['audience_mapping']: - enriched_topic['audienceIds'] = [id_name_mappings['audience_mapping'][audience_name]] + audience_id = find_best_match(topic['targetAudience'], id_name_mappings['audience_mapping']) + if audience_id: + enriched_topic['audienceIds'] = [audience_id] + match_results['audience_matched'] = True if 'object' in topic and topic['object']: - spot_name = topic['object'] - if spot_name in id_name_mappings['scenic_spot_mapping']: - enriched_topic['scenicSpotIds'] = [id_name_mappings['scenic_spot_mapping'][spot_name]] + spot_id = find_best_match(topic['object'], id_name_mappings['scenic_spot_mapping']) + if spot_id: + enriched_topic['scenicSpotIds'] = [spot_id] + match_results['scenic_spot_matched'] = True if 'product' in topic and topic['product']: - product_name = topic['product'] - if product_name in id_name_mappings['product_mapping']: - enriched_topic['productIds'] = [id_name_mappings['product_mapping'][product_name]] + product_id = find_best_match(topic['product'], id_name_mappings['product_mapping']) + if product_id: + enriched_topic['productIds'] = [product_id] + match_results['product_matched'] = True + + # 记录匹配情况 + total_fields = sum(1 for key in ['style', 'targetAudience', 'object', 'product'] if key in topic and topic[key]) + matched_fields = sum(match_results.values()) + + if total_fields > 0: + match_rate = matched_fields / total_fields * 100 + logger.info(f"选题 {topic.get('index', 'N/A')} ID匹配率: {match_rate:.1f}% ({matched_fields}/{total_fields})") + + # 如果匹配率低于50%,记录警告 + if match_rate < 50: + logger.warning(f"选题 {topic.get('index', 'N/A')} ID匹配率较低: {match_rate:.1f}%") + + # 添加匹配元数据 + # enriched_topic['_id_match_metadata'] = { + # 'match_results': match_results, + # 'match_rate': matched_fields / max(total_fields, 1) * 100 if total_fields > 0 else 0 + # } enriched_topics.append(enriched_topic) diff --git a/api/services/__pycache__/database_service.cpython-312.pyc b/api/services/__pycache__/database_service.cpython-312.pyc index 38d98e4cdecc04b47d9770ca3e9534c392e84e16..34aac4f81d547d01347f8abca70125cd3d0934cf 100644 GIT binary patch delta 14487 zcmeHO34Bvky1(~kY0@NJNSf|#y3mr+(w3I3EJ9bxHejo=hme~>N@>eY0<@$A1{gt+ z#iN1>S_XX<6^&DdDa&X<(asy+yu{*<8hwtAlx5an^*Q6bao+dcEKN!ayyf?P^ZPv` ze)pX3eCIpMJ?Hy>-?^v1d|2@66N2ntDiq-q{Fc16d#&?DiuwXG{-4m77GDaI^z79qY(~W458g*BxQ6Qyc zm401z7}IddR3N4r6dOS*2BczGwO`o?Qn4VV9zn_uQ|OzjEi@Y&>UbeD*4JprKJg4PQ<;$+2U&9xkZqJFf=bQX zExasA?2Apj603E^YI|eH?iM~KAE3y**m&kk0r|W=fuv-ph&?ZyJRjREJV37)N224N z5!r;SES?-&9ZNJ1h0j<+v5`Nf+oU#WIkiGXQElNiNqr==GUR6qXQTX zTZ}R?njPgYX_Hx^$wzTX32YLpw8Z#zSYrLDdNoOlOCW!XQ$)*H)e*)fd4b+Xx5;ht z`gpQ^O|nQ~Q`9G-jJPer7Ezzfw-DpU37P_4Di%v3jb9>@;%2r*S~C3k`b=yxqAhBul38&< z4aeIeq4@+`)U(u4@u2c0(9yQYt&s=lz0Y}5o9Io{X2B9_6Rm~uC1}Fk5>{`~uhyH5 zM4i|{$C9@aHFh<$-YU?RXtYB2sP+2II?ia}T6B7g#kjuNVsrQ$ zTCv$+Y_^!18c9KNY^7UdSdB&)Yg6Ok^o02)ifm7g zwR5-t-J`6HW@~eE6K659x@JA6hrWsUCx;_-3;E3m{cCLG#v_9^x)ppMU|ubHQ-j$Y z4Q$QOABS8n0!V9ex$i~Rc;|8)ztM>&SrV2G<>v7W`FryA@UNn%xUsvdJWK|8B;}!b z9to9@)*k1I&$$wx?~2dwjW0YP{!+|jjdDh%4~VIhf&&$Os)R#x4$e7jZD&7Jjr9nC z3DuG_o}csdoc1k_(%z&=2c%bHQ^>WH3|aOqkuYwQM@+>gljKx=dKnGveKn7!q>=mO zd*sfT8J8vZ^+hWGGEhxZ8LR1U3dqsaWe>!uEAyBO;}n&Nk_*#BKrhmzT424CGrlrS zbSY^xq~A>xBTW-k=1AYo3d3}sv}&U0-H8*b3PtY~iXiQsZZRxaE7#arp(&wbZ=yB3 z`FHqnxKFhbH`8S1wB)I@BBfgZOGz8Xzh=HFHlkuhRSX%I9!)PNGt+m_ zO7dxXdgp>I^BFRAbSCq43Q0|hA*O^5t2I5^D{E~3T;1)3(jEyFP&fFZ{WYMi|(V5uR(WHH>ip;dim3+P) z>wX7h8}(VFlGvNe#f$plHHX8=g-K~7H+vsBHGW-KIBYN0Y9N2wmC(85(F}&H&B>;h zkOMgn(#y#8oJDqiY@s9KR)SXVcyUA=q7*+Ge)#?&p*uY|ByJUEhaFEej>p>#|4`Bb z3!a=98yE3IsB8Ws5Pm3~uCd#NQf@Ki7u0e~NK3&o=!UpreW$u~(As3Qvo;)xhpdDVivd1Uj*N;tJ@**@+8_ zgZr@l9{WHJ#Hg6obiUo?&C*f)3p_of@hWm|(&;R0+buCS!)rIOW&>^j{|{{i??Al3 z1GSiAY_vIx=<{;OaYjv6Js3`YGCPr!KATA1Dhi`zhX7brwux=xa%z8=fmur#m=$3F?P0bsR)V|lab~Yn zNVQ4Y!Ui$gM*AVlCTRs=K$n0kgD$0pK?)#^Ax|@!d4o`Hllm+8pWr))>rwxZpDo-k zK~t8%Gyu#43=6_xEZx;Hc9|vMb?_aR8JIQLaqySgWHy<%<2HIDWv2H+{vfkALI~w; zGQVz^3HecxKL?(I*uby~{Lk;5&>%mQQv*EYE84;~`I2q2O(bnjQq_>UEs6}P5L({^ z@=IW)(Nv4Vi{crS1^qJq2d+a=n>=WgsW{582%FqLh4^+;3=FshiZ=OHd5!NG^v#?x(Ij{d)i2v(@GOyARzs)^WA#?DZ38aZBx{pOC2 z{@p+Fi(WhO^0hs0_V0F-XvnXovUY7$koiMP;7-uthN{yyG_2Me)(u*!CX0E{G`fZL zO{PY-puyP4TS=}Ms|w6Ui(A-WGF#kY<7SJok>zb>ka1CX?d1lL<~KLPjVJbL>GM}% zb!lCbwUNbr;g+p8nayBj`Ao!MXJFr$-8AR5L<*=B0=r%V8%usQEtYA`_SsVz`L}73 zDOW(&{GrbjP*JM=wnuEoCVwIywSU2$1)~sX-@exk1$R; zoopyAAQPq++i!`f>E(2RH!Yy|h=m>~KV1Tq+8-Z@nBh z@hV{X+=FwS>80-!b#H!qdUw8a(Td9}^{z(i07V(>^a2KwPE)}AY z;Wq+mRK6>E++V&NMWt2H_&^okWO$3eb;gja4H42DDk}a;w)T}S%Q zK6&j)$F;{^3-_;OE4mEc4Qs`|-#mBt#%nM1A93^_-Dbu3>FW8z*WcU+)o(of^7Wmk zZyw**ziSU)(!cF||C@)dc5S=X)dl6C|3BjD-1FQJAuKW8)X&(!I|(n&@GT_%?bDu zeg}F)6ZLmgfxZJkJ~+@xyCXi1P2FA9Cr`K{PjkuB`qasN2^xEQY`fGMm(!Q1?Mp7k ze@V&sPm_5oTAC5@H6>L5EKu zN|HGxH0SntVwUQWQJp^)pwO`+ziW^B9^b#m_Yi!4er8t?zGniyUp@O$KiPg8zQY3I z%t$PRou$pTViI?C5ZwD~2E+ZqSy|#uD7%@=+!;eYn57y9{ner9@AK<}ke_Qs6}W!~ zfuFNswhhTvB>W!7_{bYk_(1=0WRxM{gDZduBgJjQ{B|U(ko*wI4j}I60O$w$unP;z zftW`?eq~T7V)jM@=GF}G0}^NXv!lQknm+-8A%WiyD7oDa80S)s>uBv&mN?}l=m$&% zKY+c94^V$R-%;&KpFXk=kXvvix5|}U^}euYS?}y6y}3)Baax}f;LM(UIc`db7ts3l zea_MvXN~UiDub(e6C$gTzMp}lb6taTiw(ehD_zO}-rMLhg!U?C5xN34dJ*%1vCi4N z35u`=xB_~b2me)cEeh5$5Am+RV%`;4au-(sRiGAvs4b=4S-PM2Km1|Gi z(HFmYsN?#J9sQ5*?SK6|K=8FAkM%!t%$f*d*S5WM_1w>{z5c|_R}TN_{9bPf0Qrqm z9U;)n89~@BYvN36OpOaUIC^tSIitS8o0K(!PYy6$4TomjqaexcL$V);{WdiAb~gl# zxdR~S2VQ(0RI-wugEi9 z^2|Q9rY|kKBfQ<_OfBw9P47$A^re>eC6@$HH!Fa;iK9bN*EylGx3J1tFuOvn0@D?2DYwhRnfVa3O z#*nQAnYq;qG-J5okk_vx7VF=2=Zw3OQ|8Jkd#Ai7vA6tz-kb*yNBSdg`{lSvA(;CQ9(hkf%uR&IJ1|`z z$|v%eJIRZ=B!7&09$NXJKo<_8?mf^w5_DX)H^m#_+QlP8xNywqk9Xm44I^BMH4hpe zg00?7aYt^^zo6?wW#p5CWU=}U!kqV}cE;@Kt0aqf<{S(HISeZM_aC}?{?Hvj zj_jLTFl8-5E*a%;R2hWrL`%&eOVatV}TOH)L zA#*r%8x=se!A{&2;8eKX1M+^1y9L)4mN!cRk>xHl0KWzj99d4iJ+jPqDf1n&p(o6} z^CsLSx?I$1^VP8M8Bc%H_C{5UAVI zTUO&Nt##I}x~yki8|<4QOss?W(6wQ+vthl{)&_y19Rf=TBHQS4j2CCa2{Oc&Z4gU- zP`AdpVKWqA4XC)9egGx(^n>_#kXgdVmrMEha@k$tOH}bqh5*i*L*h&Rlo_!uFlB?M z%zS+L4~cc&U2;dP^NIvvotH5z)-~Z$K87R^>D`8LzUm`l9KT0iL>>4=a|p)00Ib&R z!9dT>-}e|d!j?UWt9&(7;bATX0D?UScmk&kxCNB61I`qIaABz7pAnu)lr0h_fb55x0Eu8HvsP0iZRr6(+B@6l@)0~ntPT9F<{!IaV&o(d0m+is+GuYWr z$gKP0JKtUO5Iyws_Cb{452%VTg&(DYPM!HftH7}oeE5&T@(v^?kOa=A+~IKDgEisZ z5xA)0y%Z8;Q_4Zu3|~Vjjm&>!52w}S<(gTFpX2ltd5wU&^UpOaMCv7|`V}AxxK}ZG z63L#_(P6W#Me!#fuS4~jF!^E3^4orCM5uLBDV)3m+v!`OZ zu+=j_%)wRM>{LZQR(ac3g}l0|ge*7yh(1TYF#c4Debv%@^JW+wh12&+T|QkwKCVlm zm7RaCqk}%q)Af~J$B)ddFJ8irlmC=38EOt4F@Lu~hXwee{g(;4kuf;VRhZ?+8GJc| z^wr;|Sctv;4V0r5C${Fw&iZvC;owDqO-=LYjpUuC356Fy$glqrD!i-yoaPEeGwT0h zQ2%H2JDn>w{GAzWfwjzp7GAW3w$O7=Ey%2M6$gflpo+X~Jq)dsZ4?E~>B5$BXywV4 z&{lqVPp$m5rA*6w!2@osT`Ti`+AhS>@koX{x*q%} z6|fBOk&L$tAB1)fPG5sP3x56J{XzeJ^?rTkqhZr}{Tgy=XRcxZ`~4RX;+NAOc79Ep z$@S?gJ8$k9A2Lnf7L${YC6V1*oV zs=#{x1Q~Z*?;t}W)Jf#IeYp_@*a7eyU>@)`lY8GE>9w7|*#A>(-TwHELNp`(&GhR~ z<=ZiP+knYoQlY3stvEk8yPdx!D}y%b!ozx4aX&d+8?xet=w3Cv?nzHiQmnph$}T>Q zQ`YiKsX)xHx+~Xt{qgrw4w&`v=aay<GsPv(p;oa&SF z71xFg=PmkNr{T3P7DMuHQ!*wD0oy~*Mh85)}4ChS-o0NM%v z#YBF9Um^EvBxOi=Q^SuEZw2_QgV#7DTANvzM4f*&bym`PY7+fj=TA;uM?`(5>#Q30 z83!(O=*>Y=f2K;2Cj>4o5q`bDdFFmPDGhR3rgjZH_@B1WEz#*3_3Mo~9mzhIX_o?< z8-*kS3Csz_%|`;dC~gdrd?X0poE{14;u?@NBSDDdeuSh2$x}$4K=M2iJR#x!3CRmU z+``pxkC^Mk{Ana-d83Jr1NRn^w~@Su!D$Lks80t>yBVm$9C=unhw+&G6_);}lr1DtE zhV$vAo+Py>uA|07!Nak*i|sDz5p+*J)99Koe*k%&f@ml&^HA_`%rr;kKC{&|ae-eTOEldfbkw{d^-z$6 z_KJH1JvBX&3o~3Z?jJy&XCWOeN^ul-3Er53&8Bp#y4jv0=fawv$rl=3vmU^9Q#@H& zqNyFMqv(VQG}%Q{yUIb%&{NdC_Cktl+Co${l~?srFPUhGEOY

}2@?iO~}^kj4|z7XM> z`h!IS$n<{ybpPledHd*qc}qhKZ{|w|sIemTt)(<^yqOPEoF`IyR?;ewx?R=I zb`&`T9g~kWy3!^LP{8x#VnKO_s>9$Y>R5Xs#g#ePR}dx|bC?~V;NelHzzZ^aD0nzF zcMINt1p!IVOd1)m3gFSTxZ7}MMKB-Ueu^kNu4{I8%^6q~gB6Or6()C=otf_Cm(#c$ za{X^RL8R^|b_h;=&tFOoSESoKZnry&qpV;cknSl#j?I|FiUPwstcL1eo9hnYuSD9n;8{=Ggv*1riiZ8-OS;@~4 zKNmO^wLKX<=AJPZR=TFueuZ=5-GsRRv`P;BHFp@^^T+$r1-L0ElCu{}e;|WDJur>~ zY=ghvIoD{UfKgwK6>fg>J2j7l~ R{zkkpijMn|Ldy5#KLL%-3UvSg delta 12082 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 G*8U4Bx2Q+} delta 2354 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 YHC?LuK$?GE^r`4Y0jl{o1IUf=KT|;7Z~y=R diff --git a/api/services/database_service.py b/api/services/database_service.py index 641fc3b..719c63a 100644 --- a/api/services/database_service.py +++ b/api/services/database_service.py @@ -12,46 +12,12 @@ import traceback from typing import Dict, Any, Optional, List, Tuple import mysql.connector from mysql.connector import pooling -from functools import wraps from core.config import ConfigManager logger = logging.getLogger(__name__) -def database_retry(max_retries: int = 3, delay: float = 1.0): - """数据库查询重试装饰器""" - def decorator(func): - @wraps(func) - def wrapper(self, *args, **kwargs): - if not self.db_pool: - logger.error("数据库连接池未初始化,尝试重新初始化...") - self.db_pool = self._init_db_pool() - if not self.db_pool: - logger.error("数据库连接池重新初始化失败,返回兜底数据") - return self._get_fallback_data(func.__name__) - - last_exception = None - current_delay = delay - - for attempt in range(max_retries): - try: - return func(self, *args, **kwargs) - except Exception as e: - last_exception = e - logger.warning(f"数据库查询 {func.__name__} 第 {attempt + 1} 次失败: {e}") - if attempt < max_retries - 1: - time.sleep(current_delay) - current_delay *= 2 - - # 所有重试都失败,返回兜底数据 - logger.error(f"数据库查询 {func.__name__} 在 {max_retries} 次重试后仍然失败: {last_exception}") - return self._get_fallback_data(func.__name__) - - return wrapper - return decorator - - class DatabaseService: """数据库服务类""" @@ -63,46 +29,7 @@ class DatabaseService: config_manager: 配置管理器 """ self.config_manager = config_manager - - # 从配置获取数据库相关设置 - db_config = config_manager.get_raw_config('database') - self.pool_size = db_config.get('pool_size', 10) - self.max_retry_attempts = db_config.get('max_retry_attempts', 3) - self.query_timeout = db_config.get('query_timeout', 30) - self.soft_delete_field = db_config.get('soft_delete_field', 'isDelete') - self.active_record_value = db_config.get('active_record_value', 0) - self.db_pool = self._init_db_pool() - - # 兜底数据缓存 - self._fallback_cache = { - 'styles': [], - 'audiences': [], - 'scenic_spots': [], - 'products': [], - 'materials': [] - } - - def _get_fallback_data(self, func_name: str) -> Any: - """获取兜底数据""" - fallback_mapping = { - 'get_all_styles': self._fallback_cache['styles'], - 'get_all_audiences': self._fallback_cache['audiences'], - 'get_scenic_spot_by_id': None, - 'get_product_by_id': None, - 'get_style_by_id': None, - 'get_audience_by_id': None, - 'get_scenic_spots_by_ids': [], - 'get_products_by_ids': [], - 'get_styles_by_ids': [], - 'get_audiences_by_ids': [], - 'get_content_by_id': None, - 'get_content_by_topic_index': None, - } - - result = fallback_mapping.get(func_name, None) - logger.info(f"使用兜底数据 for {func_name}: {type(result)}") - return result def _init_db_pool(self): """初始化数据库连接池""" @@ -128,10 +55,23 @@ class DatabaseService: logger.info(f"尝试连接数据库 ({attempt['desc']}): {connection_info}") # 创建连接池 + # 从配置中分离MySQL连接池支持的参数和不支持的参数 + config = attempt["config"].copy() + + # MySQL连接池不支持的参数,需要移除 + unsupported_params = [ + 'max_retry_attempts', 'query_timeout', 'soft_delete_field', 'active_record_value' + ] + for param in unsupported_params: + config.pop(param, None) + + # 设置连接池参数,使用配置文件中的值或默认值 + pool_size = config.pop('pool_size', 10) + pool = pooling.MySQLConnectionPool( pool_name=f"database_service_pool_{int(time.time())}", - pool_size=self.pool_size, - **attempt["config"] + pool_size=pool_size, + **config ) # 测试连接 @@ -163,16 +103,18 @@ class DatabaseService: return processed_config - @database_retry(max_retries=3, delay=1.0) def get_scenic_spot_by_id(self, spot_id: int) -> Optional[Dict[str, Any]]: """根据ID获取单个景区信息""" + if not self.db_pool: + logger.error("数据库连接池未初始化") + return None try: with self.db_pool.get_connection() as conn: with conn.cursor(dictionary=True) as cursor: cursor.execute( - f"SELECT * FROM scenicSpot WHERE id = %s AND {self.soft_delete_field} = %s", - (spot_id, self.active_record_value) + "SELECT * FROM scenicSpot WHERE id = %s AND isDelete = 0", + (spot_id,) ) result = cursor.fetchone() if result: @@ -185,7 +127,6 @@ class DatabaseService: logger.error(f"查询景区信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_product_by_id(self, product_id: int) -> Optional[Dict[str, Any]]: """根据ID获取单个产品信息""" if not self.db_pool: @@ -205,7 +146,6 @@ class DatabaseService: logger.error(f"查询产品信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_style_by_id(self, style_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取风格信息 @@ -239,7 +179,6 @@ class DatabaseService: logger.error(f"查询风格信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_audience_by_id(self, audience_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取受众信息 @@ -273,7 +212,6 @@ class DatabaseService: logger.error(f"查询受众信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_scenic_spots_by_ids(self, spot_ids: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取景区信息 @@ -285,7 +223,8 @@ class DatabaseService: 景区信息列表 """ if not self.db_pool or not spot_ids: - return [] + logger.warning("数据库连接池未初始化或景区ID列表为空") + return self._get_fallback_scenic_spots(spot_ids) try: with self.db_pool.get_connection() as conn: @@ -295,14 +234,43 @@ class DatabaseService: query = f"SELECT * FROM scenicSpot WHERE id IN ({placeholders}) AND isDelete = 0" cursor.execute(query, spot_ids) results = cursor.fetchall() - logger.info(f"批量查询景区信息: 请求{len(spot_ids)}个,找到{len(results)}个") + + # 检查是否所有ID都找到了对应的记录 + found_ids = {result['id'] for result in results} + missing_ids = set(spot_ids) - found_ids + + if missing_ids: + logger.warning(f"部分景区ID未找到: {missing_ids}") + # 添加兜底数据 + fallback_spots = self._get_fallback_scenic_spots(list(missing_ids)) + results.extend(fallback_spots) + + logger.info(f"批量查询景区信息: 请求{len(spot_ids)}个,找到{len([r for r in results if r['id'] in spot_ids])}个") return results except Exception as e: logger.error(f"批量查询景区信息失败: {e}") - return [] + return self._get_fallback_scenic_spots(spot_ids) + + def _get_fallback_scenic_spots(self, spot_ids: List[int]) -> List[Dict[str, Any]]: + """ + 获取景区的兜底数据 + """ + fallback_spots = [] + for spot_id in spot_ids: + fallback_spots.append({ + 'id': spot_id, + 'name': f'景区{spot_id}', + 'address': '默认地址', + 'advantage': '优美的自然风光', + 'highlight': '值得一游的景点', + 'isPublic': 1, + 'isDelete': 0, + '_is_fallback': True + }) + logger.info(f"使用兜底景区数据: {len(fallback_spots)}个") + return fallback_spots - @database_retry(max_retries=3, delay=1.0) def get_products_by_ids(self, productIds: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取产品信息 @@ -314,7 +282,8 @@ class DatabaseService: 产品信息列表 """ if not self.db_pool or not productIds: - return [] + logger.warning("数据库连接池未初始化或产品ID列表为空") + return self._get_fallback_products(productIds) try: with self.db_pool.get_connection() as conn: @@ -324,14 +293,43 @@ class DatabaseService: query = f"SELECT * FROM product WHERE id IN ({placeholders}) AND isDelete = 0" cursor.execute(query, productIds) results = cursor.fetchall() - logger.info(f"批量查询产品信息: 请求{len(productIds)}个,找到{len(results)}个") + + # 检查是否所有ID都找到了对应的记录 + found_ids = {result['id'] for result in results} + missing_ids = set(productIds) - found_ids + + if missing_ids: + logger.warning(f"部分产品ID未找到: {missing_ids}") + # 添加兜底数据 + fallback_products = self._get_fallback_products(list(missing_ids)) + results.extend(fallback_products) + + logger.info(f"批量查询产品信息: 请求{len(productIds)}个,找到{len([r for r in results if r['id'] in productIds])}个") return results except Exception as e: logger.error(f"批量查询产品信息失败: {e}") - return [] + return self._get_fallback_products(productIds) + + def _get_fallback_products(self, productIds: List[int]) -> List[Dict[str, Any]]: + """ + 获取产品的兜底数据 + """ + fallback_products = [] + for product_id in productIds: + fallback_products.append({ + 'id': product_id, + 'productName': f'产品{product_id}', + 'originPrice': 999, + 'realPrice': 888, + 'packageInfo': '包含景区门票和导游服务', + 'advantage': '性价比高,服务优质', + 'isDelete': 0, + '_is_fallback': True + }) + logger.info(f"使用兜底产品数据: {len(fallback_products)}个") + return fallback_products - @database_retry(max_retries=3, delay=1.0) def get_styles_by_ids(self, styleIds: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取风格信息 @@ -343,7 +341,8 @@ class DatabaseService: 风格信息列表 """ if not self.db_pool or not styleIds: - return [] + logger.warning("数据库连接池未初始化或风格ID列表为空") + return self._get_fallback_styles(styleIds) try: with self.db_pool.get_connection() as conn: @@ -353,14 +352,40 @@ class DatabaseService: query = f"SELECT * FROM contentStyle WHERE id IN ({placeholders}) AND isDelete = 0" cursor.execute(query, styleIds) results = cursor.fetchall() - logger.info(f"批量查询风格信息: 请求{len(styleIds)}个,找到{len(results)}个") + + # 检查是否所有ID都找到了对应的记录 + found_ids = {result['id'] for result in results} + missing_ids = set(styleIds) - found_ids + + if missing_ids: + logger.warning(f"部分风格ID未找到: {missing_ids}") + # 添加兜底数据 + fallback_styles = self._get_fallback_styles(list(missing_ids)) + results.extend(fallback_styles) + + logger.info(f"批量查询风格信息: 请求{len(styleIds)}个,找到{len([r for r in results if r['id'] in styleIds])}个") return results except Exception as e: logger.error(f"批量查询风格信息失败: {e}") - return [] + return self._get_fallback_styles(styleIds) + + def _get_fallback_styles(self, styleIds: List[int]) -> List[Dict[str, Any]]: + """ + 获取风格的兜底数据 + """ + fallback_styles = [] + for style_id in styleIds: + fallback_styles.append({ + 'id': style_id, + 'styleName': f'风格{style_id}', + 'description': '默认风格描述', + 'isDelete': 0, + '_is_fallback': True + }) + logger.info(f"使用兜底风格数据: {len(fallback_styles)}个") + return fallback_styles - @database_retry(max_retries=3, delay=1.0) def get_audiences_by_ids(self, audienceIds: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取受众信息 @@ -372,7 +397,8 @@ class DatabaseService: 受众信息列表 """ if not self.db_pool or not audienceIds: - return [] + logger.warning("数据库连接池未初始化或受众ID列表为空") + return self._get_fallback_audiences(audienceIds) try: with self.db_pool.get_connection() as conn: @@ -382,14 +408,40 @@ class DatabaseService: query = f"SELECT * FROM targetAudience WHERE id IN ({placeholders}) AND isDelete = 0" cursor.execute(query, audienceIds) results = cursor.fetchall() - logger.info(f"批量查询受众信息: 请求{len(audienceIds)}个,找到{len(results)}个") + + # 检查是否所有ID都找到了对应的记录 + found_ids = {result['id'] for result in results} + missing_ids = set(audienceIds) - found_ids + + if missing_ids: + logger.warning(f"部分受众ID未找到: {missing_ids}") + # 添加兜底数据 + fallback_audiences = self._get_fallback_audiences(list(missing_ids)) + results.extend(fallback_audiences) + + logger.info(f"批量查询受众信息: 请求{len(audienceIds)}个,找到{len([r for r in results if r['id'] in audienceIds])}个") return results except Exception as e: logger.error(f"批量查询受众信息失败: {e}") - return [] + return self._get_fallback_audiences(audienceIds) + + def _get_fallback_audiences(self, audienceIds: List[int]) -> List[Dict[str, Any]]: + """ + 获取受众的兜底数据 + """ + fallback_audiences = [] + for audience_id in audienceIds: + fallback_audiences.append({ + 'id': audience_id, + 'audienceName': f'受众{audience_id}', + 'description': '默认受众描述', + 'isDelete': 0, + '_is_fallback': True + }) + logger.info(f"使用兜底受众数据: {len(fallback_audiences)}个") + return fallback_audiences - @database_retry(max_retries=3, delay=1.0) def list_all_scenic_spots(self, user_id: Optional[int] = None, is_public: Optional[bool] = None) -> List[Dict[str, Any]]: """ 获取所有景区列表 @@ -435,7 +487,6 @@ class DatabaseService: logger.error(f"获取景区列表失败: {e}") return [] - @database_retry(max_retries=3, delay=1.0) def list_all_products(self, user_id: Optional[int] = None, is_public: Optional[bool] = None) -> List[Dict[str, Any]]: """ 获取所有产品列表 @@ -483,7 +534,6 @@ class DatabaseService: logger.error(f"获取产品列表失败: {e}") return [] - @database_retry(max_retries=3, delay=1.0) def list_all_styles(self) -> List[Dict[str, Any]]: """ 获取所有风格列表 @@ -506,7 +556,6 @@ class DatabaseService: logger.error(f"获取风格列表失败: {e}") return [] - @database_retry(max_retries=3, delay=1.0) def list_all_audiences(self) -> List[Dict[str, Any]]: """ 获取所有受众列表 @@ -540,7 +589,6 @@ class DatabaseService: # 名称到ID的反向查询方法 - @database_retry(max_retries=3, delay=1.0) def get_style_id_by_name(self, style_name: str) -> Optional[int]: """ 根据风格名称获取风格ID @@ -573,7 +621,6 @@ class DatabaseService: logger.error(f"查询风格ID失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_audience_id_by_name(self, audience_name: str) -> Optional[int]: """ 根据受众名称获取受众ID @@ -606,7 +653,6 @@ class DatabaseService: logger.error(f"查询受众ID失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_scenic_spot_id_by_name(self, spot_name: str) -> Optional[int]: """ 根据景区名称获取景区ID @@ -639,7 +685,6 @@ class DatabaseService: logger.error(f"查询景区ID失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_product_id_by_name(self, product_name: str) -> Optional[int]: """ 根据产品名称获取产品ID @@ -674,7 +719,6 @@ class DatabaseService: - @database_retry(max_retries=3, delay=1.0) def get_image_by_id(self, image_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取图像信息 @@ -708,7 +752,6 @@ class DatabaseService: logger.error(f"查询图像信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_images_by_ids(self, image_ids: List[int]) -> List[Dict[str, Any]]: """ 根据ID列表批量获取图像信息 @@ -737,7 +780,6 @@ class DatabaseService: logger.error(f"批量查询图像信息失败: {e}") return [] - @database_retry(max_retries=3, delay=1.0) def get_content_by_id(self, content_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取内容信息 @@ -771,7 +813,6 @@ class DatabaseService: logger.error(f"查询内容信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_content_by_topic_index(self, topic_index: str) -> Optional[Dict[str, Any]]: """根据主题索引获取内容信息""" if not self.db_pool: @@ -794,7 +835,6 @@ class DatabaseService: logger.error(f"查询内容信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_images_by_folder_id(self, folder_id: int) -> List[Dict[str, Any]]: """ 根据文件夹ID获取图像列表 @@ -823,7 +863,6 @@ class DatabaseService: logger.error(f"根据文件夹ID获取图像失败: {e}") return [] - @database_retry(max_retries=3, delay=1.0) def get_folder_by_id(self, folder_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取文件夹信息 @@ -857,7 +896,6 @@ class DatabaseService: logger.error(f"查询文件夹信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_related_images_for_content(self, content_id: int, limit: int = 10) -> List[Dict[str, Any]]: """ 获取与内容相关的图像列表 @@ -900,7 +938,6 @@ class DatabaseService: # 模板相关查询方法 - @database_retry(max_retries=3, delay=1.0) def get_all_poster_templates(self) -> List[Dict[str, Any]]: """ 获取所有海报模板 @@ -925,7 +962,6 @@ class DatabaseService: logger.error(f"获取海报模板列表失败: {e}") return [] - @database_retry(max_retries=3, delay=1.0) def get_poster_template_by_id(self, template_id: str) -> Optional[Dict[str, Any]]: """ 根据ID获取海报模板信息 @@ -959,7 +995,6 @@ class DatabaseService: logger.error(f"查询模板信息失败: {e}") return None - @database_retry(max_retries=3, delay=1.0) def get_active_poster_templates(self) -> List[Dict[str, Any]]: """ 获取所有激活的海报模板 @@ -1031,7 +1066,6 @@ class DatabaseService: except Exception as e: logger.error(f"更新模板使用统计失败: {e}") - @database_retry(max_retries=3, delay=1.0) def get_template_usage_stats(self, template_id: str) -> Optional[Dict[str, Any]]: """ 获取模板使用统计 diff --git a/api/services/prompt_service.py b/api/services/prompt_service.py index 37125e8..ce792cc 100644 --- a/api/services/prompt_service.py +++ b/api/services/prompt_service.py @@ -114,10 +114,23 @@ class PromptService: logger.info(f"尝试连接数据库 ({attempt['desc']}): {connection_info}") # 创建连接池 + # 从配置中分离MySQL连接池支持的参数和不支持的参数 + config = attempt["config"].copy() + + # MySQL连接池不支持的参数,需要移除 + unsupported_params = [ + 'max_retry_attempts', 'query_timeout', 'soft_delete_field', 'active_record_value' + ] + for param in unsupported_params: + config.pop(param, None) + + # 设置连接池参数,使用配置文件中的值或默认值 + pool_size = config.pop('pool_size', 5) + pool = pooling.MySQLConnectionPool( pool_name=f"prompt_service_pool_{int(time.time())}", - pool_size=5, - **attempt["config"] + pool_size=pool_size, + **config ) # 测试连接 diff --git a/api/services/tweet.py b/api/services/tweet.py index 2064969..028afbe 100644 --- a/api/services/tweet.py +++ b/api/services/tweet.py @@ -19,6 +19,7 @@ from tweet.content_generator import ContentGenerator from tweet.content_judger import ContentJudger from api.services.prompt_builder import PromptBuilderService from api.services.prompt_service import PromptService +from api.services.database_service import DatabaseService logger = logging.getLogger(__name__) @@ -109,22 +110,21 @@ class TweetService: logger.info(f"选题生成完成,请求ID: {requestId}, 数量: {len(topics)}") return requestId, topics - async def generate_content(self, topic: Optional[Dict[str, Any]] = None, - style_objects: Optional[List[Dict[str, Any]]] = None, - audience_objects: Optional[List[Dict[str, Any]]] = None, - scenic_spot_objects: Optional[List[Dict[str, Any]]] = None, - product_objects: Optional[List[Dict[str, Any]]] = None, - autoJudge: bool = False) -> Tuple[str, str, Dict[str, Any]]: + async def generate_content(self, topic: Optional[Dict[str, Any]] = None, autoJudge: bool = False, + style_objects: Optional[List[Dict[str, Any]]] = None, + audience_objects: Optional[List[Dict[str, Any]]] = None, + scenic_spot_objects: Optional[List[Dict[str, Any]]] = None, + product_objects: Optional[List[Dict[str, Any]]] = None) -> Tuple[str, str, Dict[str, Any]]: """ - 为选题生成内容 + 为单个选题生成内容 Args: - topic: 选题信息 - styles: 风格列表 - audiences: 受众列表 - scenic_spots: 景区列表 - products: 产品列表 - autoJudge: 是否自动进行内容审核 + topic: 选题信息(可能包含ID字段) + autoJudge: 是否进行内嵌审核 + style_objects: 风格对象列表(可选,用于兼容) + audience_objects: 受众对象列表(可选,用于兼容) + scenic_spot_objects: 景区对象列表(可选,用于兼容) + product_objects: 产品对象列表(可选,用于兼容) Returns: 请求ID、选题索引和生成的内容(包含judgeSuccess状态) @@ -135,22 +135,24 @@ class TweetService: topicIndex = topic.get('index', 'N/A') logger.info(f"开始为选题 {topicIndex} 生成内容{'(含审核)' if autoJudge else ''}") - # 核心修改:创建一个增强版的topic,将所有需要的信息预先填充好 - enhanced_topic = topic.copy() - if style_objects: + # 增强版的topic处理:优先使用ID获取最新数据 + enhanced_topic = await self._enhance_topic_with_database_data(topic) + + # 如果没有通过ID获取到数据,使用传入的对象参数作为兜底 + if style_objects and not enhanced_topic.get('style_object'): enhanced_topic['style_object'] = style_objects[0] enhanced_topic['style'] = style_objects[0].get('styleName') - if audience_objects: + if audience_objects and not enhanced_topic.get('audience_object'): enhanced_topic['audience_object'] = audience_objects[0] enhanced_topic['targetAudience'] = audience_objects[0].get('audienceName') - if scenic_spot_objects: + if scenic_spot_objects and not enhanced_topic.get('scenic_spot_object'): enhanced_topic['scenic_spot_object'] = scenic_spot_objects[0] enhanced_topic['object'] = scenic_spot_objects[0].get('name') - if product_objects: + if product_objects and not enhanced_topic.get('product_object'): enhanced_topic['product_object'] = product_objects[0] enhanced_topic['product'] = product_objects[0].get('productName') - # 使用PromptBuilderService构建提示词,现在它只需要enhanced_topic + # 使用PromptBuilderService构建提示词 system_prompt, user_prompt = self.prompt_builder.build_content_prompt(enhanced_topic, "content") # 使用预构建的提示词生成内容 @@ -179,23 +181,81 @@ class TweetService: content = {k: v for k, v in judged_content.items() if k != 'judge_success'} content['judgeSuccess'] = True else: - logger.warning(f"选题 {topicIndex} 内容审核失败,保持原始内容") - # 审核失败:保持原始内容,添加judgeSuccess=False标记 + logger.warning(f"选题 {topicIndex} 内容审核未通过") + # 审核失败:使用原始内容,添加judgeSuccess状态 content['judgeSuccess'] = False except Exception as e: - logger.error(f"选题 {topicIndex} 内嵌审核失败: {e},保持原始内容") - # 审核异常:保持原始内容,添加judgeSuccess=False标记 + logger.error(f"选题 {topicIndex} 内容审核过程中发生错误: {e}", exc_info=True) + # 审核出错:使用原始内容,标记审核失败 content['judgeSuccess'] = False - else: - # 未启用审核:添加judgeSuccess=None标记,表示未进行审核 - content['judgeSuccess'] = None # 生成请求ID requestId = f"content-{datetime.now().strftime('%Y%m%d-%H%M%S')}-{str(uuid.uuid4())[:8]}" - logger.info(f"内容生成完成,请求ID: {requestId}, 选题索引: {topicIndex}") + logger.info(f"选题 {topicIndex} 内容生成完成,请求ID: {requestId}") return requestId, topicIndex, content + + async def _enhance_topic_with_database_data(self, topic: Dict[str, Any]) -> Dict[str, Any]: + """ + 使用数据库数据增强选题信息 + + Args: + topic: 原始选题数据 + + Returns: + 增强后的选题数据 + """ + enhanced_topic = topic.copy() + + try: + # 通过数据库服务获取详细信息 + db_service = DatabaseService(self.config_manager) + + if not db_service.is_available(): + logger.warning("数据库服务不可用,无法增强选题数据") + return enhanced_topic + + # 处理风格ID + if 'styleIds' in topic and topic['styleIds']: + style_id = topic['styleIds'][0] if isinstance(topic['styleIds'], list) else topic['styleIds'] + style_data = db_service.get_style_by_id(style_id) + if style_data: + enhanced_topic['style_object'] = style_data + enhanced_topic['style'] = style_data.get('styleName') + logger.info(f"从数据库加载风格数据: {style_data.get('styleName')} (ID: {style_id})") + + # 处理受众ID + if 'audienceIds' in topic and topic['audienceIds']: + audience_id = topic['audienceIds'][0] if isinstance(topic['audienceIds'], list) else topic['audienceIds'] + audience_data = db_service.get_audience_by_id(audience_id) + if audience_data: + enhanced_topic['audience_object'] = audience_data + enhanced_topic['targetAudience'] = audience_data.get('audienceName') + logger.info(f"从数据库加载受众数据: {audience_data.get('audienceName')} (ID: {audience_id})") + + # 处理景区ID + if 'scenicSpotIds' in topic and topic['scenicSpotIds']: + spot_id = topic['scenicSpotIds'][0] if isinstance(topic['scenicSpotIds'], list) else topic['scenicSpotIds'] + spot_data = db_service.get_scenic_spot_by_id(spot_id) + if spot_data: + enhanced_topic['scenic_spot_object'] = spot_data + enhanced_topic['object'] = spot_data.get('name') + logger.info(f"从数据库加载景区数据: {spot_data.get('name')} (ID: {spot_id})") + + # 处理产品ID + if 'productIds' in topic and topic['productIds']: + product_id = topic['productIds'][0] if isinstance(topic['productIds'], list) else topic['productIds'] + product_data = db_service.get_product_by_id(product_id) + if product_data: + enhanced_topic['product_object'] = product_data + enhanced_topic['product'] = product_data.get('productName') + logger.info(f"从数据库加载产品数据: {product_data.get('productName')} (ID: {product_id})") + + except Exception as e: + logger.error(f"增强选题数据时发生错误: {e}", exc_info=True) + + return enhanced_topic async def generate_content_with_prompt(self, topic: Dict[str, Any], system_prompt: str, user_prompt: str) -> Tuple[str, str, Dict[str, Any]]: """ @@ -303,6 +363,7 @@ class TweetService: for topic in topics: topicIndex = topic.get('index', 'unknown') + # 直接传递带有ID的选题数据,不再需要传递额外的对象参数 _, _, content = await self.generate_content(topic, autoJudge=autoJudge) if autoJudge: diff --git a/core/algorithms/__init__.py b/core/algorithms/__init__.py new file mode 100644 index 0000000..44de453 --- /dev/null +++ b/core/algorithms/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +Tweet模块 - 负责文字内容的生成、审核和管理 +""" + +__version__ = '1.0.0' \ No newline at end of file diff --git a/core/algorithms/__pycache__/__init__.cpython-312.pyc b/core/algorithms/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82ac86ae2c549f251d3b7dd1f6bcdc198c6a5acc GIT binary patch literal 263 zcmX@j%ge<81a8&&8QnnoF^B^Lj8MjB9w1{nLkdF_LkeRQV2L;cKXw{)~D-sKA+X{e9HW19TOfmG(KIo@Y#YLPbc*} zU$^l2gf^}!Rzp1lJp(^Yrd#as@hSPq@$t8~PO4oIJJ1-ASBeFJ#0O?ZM#hgEOpGiaI2icF8o7&DfuaDNb6QjY literal 0 HcmV?d00001 diff --git a/core/algorithms/__pycache__/topic_generator.cpython-312.pyc b/core/algorithms/__pycache__/topic_generator.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b60b5ae8e1364a4a90e3edb0e39774e285910842 GIT binary patch literal 7123 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 literal 0 HcmV?d00001 diff --git a/core/algorithms/__pycache__/topic_parser.cpython-312.pyc b/core/algorithms/__pycache__/topic_parser.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a7cd73bc2601b07304dd94759598fdc1a8476814 GIT binary patch literal 2622 zcmai0eM}t36`$SP+uQpBVq-91sVCz|IgFuE&bv=Mbc}Ch3=ad%$1C!BVCE)i=Ah z2c?xV($2g$^X512y?O7=e(v{s5VYc-^b<#22;F6j-GmCUmI5M&WE4U&C-X5b#F--> z;$d{e9C0Be#GN5$TnvdElM!OBxD=8RfNZm|K^N9?)ESzO4=qXS5GoEm9NbC zhWMaMZvxKK(`&ptW_&Wf{Qi~lr8CBRg>qrixR~>-Fwtj&4k{c}RE-Mzm8eD?J&B

h#?ttWo?fQ; zrMH&GFPF1tmVSP6B{Q<}iwPr}Tb?YKd#FSrubQe-kzh?HiD<=1=814U$N}HlF>q53 zY33yB2HEVUCUdrfIixx5#Wb>Khq*MTHg3RR=$o3%+tM5wKz&Ga+X87$GsA}Q_NA$< z-+<=O{OkK|UX2Kz*<8nm{3KBmRN#&yo7>>#mqB3sh@Ad8_*EiKv- z92z~a!GkTE1c8;Mv{QD!4iwnyNsHh+VZ-iz;hSQ@yTNx=uLG8Eqwmsu9}4>Qzrryd z7RjuYZk{quT{hl-t9VRctpxh#+-UjQ>FQ+Z z<`u{)ORRihqLcUyIc9qK?Dg{4jJ~tFW9jC*UcNDTH}exqMXQd0M%^q^sUSx*Or1(X#;;LlEIFV=sZ-U4Vwj4_@tOHS_MaTfwdURRAi74)7<$`)l;e+NzUq#a5w*I%cIjOU^U}<_fu)->m>6CR% zm*ze(uD@;EdUNGMW@UV)oIO|0&lz)Y8)M}UuPjZ@bOrPV*k+kv%#4EB;6CQK-{%7Q zR#;+S);(FqgW16vDuP`Dm{37U^e2f3a@2_lNfPQ&R3)Km5io}eG4lb?hEkY1VmLwF zIHAWeVGa^x_AE$>s_Ms849~=m@sLWzNGgRBGIhNgAqgch5Ofh%+Nd|hs<>=E8PxYm zB&Nt=ja7G*`U!qnR|qVc2>xVBQ<8~D%wjnObkuNMWp~3O9ooOUI!Q9Absi>>S8!}! zGNIvwwvXV5mL#3gB*C4L*g%pf+KIT@X`T``k0%|eA?h^A*B{vMwse?NtN^KAxP%P4 z-+$YrfskUii z_ms9++itJ5JU%CX`u&CO=ZcR%Uu=3|(Raualxi64nfTPcf_GupBgNLe#l1(0^)D_; zKUx*h_U5dQZa<#&)9ue>{iVi0zU9x2dqxgqd$QVBaK^4FVX^kX!k*i0PtLbJ`Dy)U zO@CTZ&T48u$c$1w2#=hZB)F z4u`2H9F9Zq#~AGkhhNqsvC4`o9F~(&HVxsUDbYCAP9$aOP&GnWVh|STguR$#H*~*7 zq@B@itYbe7W>*J<`Y3cK(LJ7bN~;nQx2*CHI(JMsRuK$&A%D2gT2KoQO&*&MJn}W0 zTXTTWJtky9GPIzdd>aPpVv!D!NCo!0LPZE>;5I2l{ko>a)Q)}y>{2QT;fBnH6ooV) ztW**97FmjZhf!7Ksq_QOBi)4kq_8wrAA@ey!ExL@iRXkh330VwBJbbO&Uv)+FR1hH f?&fUcZFlp$yLsZf3+`PR@n7O@ZpYUMIurOm5xF9i literal 0 HcmV?d00001 diff --git a/core/algorithms/content_generator.py b/core/algorithms/content_generator.py new file mode 100644 index 0000000..0e3d99e --- /dev/null +++ b/core/algorithms/content_generator.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +内容生成模块 +""" + +import logging +import json +from typing import Dict, Any, Tuple, Optional + +from core.ai import AIAgent +from core.config import ConfigManager, GenerateTopicConfig, GenerateContentConfig +from utils.prompts import ContentPromptBuilder +from utils.file_io import OutputManager, process_llm_json_text + +logger = logging.getLogger(__name__) + + +class ContentGenerator: + """负责为单个选题生成内容""" + + def __init__(self, ai_agent: AIAgent, config_manager: ConfigManager, output_manager: OutputManager): + self.ai_agent = ai_agent + self.config_manager = config_manager + self.topic_config = config_manager.get_config('topic_gen', GenerateTopicConfig) + self.content_config = config_manager.get_config('content_gen', GenerateContentConfig) + self.output_manager = output_manager + self.prompt_builder = ContentPromptBuilder(config_manager) + + async def generate_content_for_topic(self, topic: Dict[str, Any]) -> Dict[str, Any]: + """ + 为单个选题生成内容 + + Args: + topic: 选题信息字典 + + Returns: + 包含生成内容的字典 + """ + topic_index = topic.get('index', 'N/A') + logger.info(f"开始为选题 {topic_index} 生成内容...") + + # 1. 构建提示 + # 使用模板构建器分别获取系统和用户提示 + system_prompt = self.prompt_builder.get_system_prompt() + user_prompt = self.prompt_builder.build_user_prompt(topic=topic) + + # 保存提示以供调试 + output_dir = self.output_manager.get_topic_dir(topic_index) + self.output_manager.save_text(system_prompt, "content_system_prompt.txt", subdir=output_dir.name) + self.output_manager.save_text(user_prompt, "content_user_prompt.txt", subdir=output_dir.name) + + # 获取模型参数 + model_params = {} + if hasattr(self.content_config, 'model') and isinstance(self.content_config.model, dict): + model_params = { + 'temperature': self.content_config.model.get('temperature'), + 'top_p': self.content_config.model.get('top_p'), + 'presence_penalty': self.content_config.model.get('presence_penalty') + } + # 移除None值 + model_params = {k: v for k, v in model_params.items() if v is not None} + + # 2. 调用AI + try: + raw_result, _, _, _ = await self.ai_agent.generate_text( + system_prompt=system_prompt, + user_prompt=user_prompt, + use_stream=True, + stage="内容生成", + **model_params + ) + self.output_manager.save_text(raw_result, "content_raw_response.txt", subdir=output_dir.name) + except Exception as e: + logger.critical(f"为选题 {topic_index} 生成内容时AI调用失败: {e}", exc_info=True) + return {"error": str(e)} + + # 3. 解析和保存结果 + content_data = process_llm_json_text(raw_result) + if content_data: + self.output_manager.save_json(content_data, "article.json", subdir=output_dir.name) + logger.info(f"成功为选题 {topic_index} 生成并保存内容。") + return content_data + else: + logger.error(f"解析内容JSON失败 for {topic_index}") + return {"error": "JSONDecodeError", "raw_content": raw_result} + + async def generate_content_with_prompt(self, topic: Dict[str, Any], system_prompt: str, user_prompt: str) -> Dict[str, Any]: + """ + 使用已构建的提示词生成内容 + + Args: + topic: 选题信息字典 + system_prompt: 已构建好的系统提示词 + user_prompt: 已构建好的用户提示词 + + Returns: + 包含生成内容的字典 + """ + topic_index = topic.get('index', 'N/A') + logger.info(f"使用预构建提示词为选题 {topic_index} 生成内容...") + + # 保存提示以供调试 + output_dir = self.output_manager.get_topic_dir(topic_index) + self.output_manager.save_text(system_prompt, "content_system_prompt.txt", subdir=output_dir.name) + self.output_manager.save_text(user_prompt, "content_user_prompt.txt", subdir=output_dir.name) + + # 获取模型参数 + model_params = {} + if hasattr(self.content_config, 'model') and isinstance(self.content_config.model, dict): + model_params = { + 'temperature': self.content_config.model.get('temperature'), + 'top_p': self.content_config.model.get('top_p'), + 'presence_penalty': self.content_config.model.get('presence_penalty') + } + # 移除None值 + model_params = {k: v for k, v in model_params.items() if v is not None} + + # 调用AI + try: + raw_result, _, _, _ = await self.ai_agent.generate_text( + system_prompt=system_prompt, + user_prompt=user_prompt, + use_stream=True, + stage="内容生成", + **model_params + ) + self.output_manager.save_text(raw_result, "content_raw_response.txt", subdir=output_dir.name) + except Exception as e: + logger.critical(f"为选题 {topic_index} 生成内容时AI调用失败: {e}", exc_info=True) + return {"error": str(e)} + + # 解析和保存结果 + content_data = process_llm_json_text(raw_result) + if content_data: + self.output_manager.save_json(content_data, "article.json", subdir=output_dir.name) + logger.info(f"成功为选题 {topic_index} 生成并保存内容。") + return content_data + else: + logger.error(f"解析内容JSON失败 for {topic_index}") + return {"error": "JSONDecodeError", "raw_content": raw_result} \ No newline at end of file diff --git a/core/algorithms/content_judger.py b/core/algorithms/content_judger.py new file mode 100644 index 0000000..30a40b2 --- /dev/null +++ b/core/algorithms/content_judger.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +内容审核模块 +""" + +import logging +import json +from typing import Dict, Any, Union + +from core.ai import AIAgent +from core.config import ConfigManager, GenerateTopicConfig, GenerateContentConfig +from utils.prompts import JudgerPromptBuilder +from utils.file_io import process_llm_json_text + +logger = logging.getLogger(__name__) + + +class ContentJudger: + """内容审核类,使用AI评估和修正内容""" + + def __init__(self, ai_agent: AIAgent, config_manager: ConfigManager, output_manager=None): + """ + 初始化内容审核器 + + Args: + ai_agent: AIAgent实例 + config_manager: 配置管理器 + output_manager: 输出管理器,用于保存提示词和响应 + """ + self.ai_agent = ai_agent + self.config_manager = config_manager + self.topic_config = config_manager.get_config('topic_gen', GenerateTopicConfig) + self.content_config = config_manager.get_config('content_gen', GenerateContentConfig) + self.prompt_builder = JudgerPromptBuilder(config_manager) + self.output_manager = output_manager + + async def judge_content(self, generated_content: Union[str, Dict[str, Any]], topic: Dict[str, Any]) -> Dict[str, Any]: + """ + 调用AI审核生成的内容 + + Args: + generated_content: 已生成的原始内容(JSON字符串或字典对象) + topic: 与内容相关的原始选题字典 + + Returns: + 一个包含审核结果的字典 + """ + logger.info("开始审核生成的内容...") + + # 获取主题索引,用于保存文件 + topic_index = topic.get('index', 'unknown') + topic_dir = f"topic_{topic_index}" + + # 从原始内容中提取tag + original_tag = [] + original_content = process_llm_json_text(generated_content) + if original_content and isinstance(original_content, dict) and "tag" in original_content: + original_tag = original_content.get("tag", []) + logger.info(f"从原始内容中提取到标签: {original_tag}") + else: + logger.warning("从原始内容提取标签失败") + + # 将字典转换为JSON字符串,以便在提示中使用 + if isinstance(generated_content, dict): + generated_content_str = json.dumps(generated_content, ensure_ascii=False, indent=2) + else: + generated_content_str = str(generated_content) + + # 1. 构建提示 + system_prompt = self.prompt_builder.get_system_prompt() + user_prompt = self.prompt_builder.build_user_prompt( + generated_content=generated_content_str, + topic=topic + ) + + # 保存提示词 + if self.output_manager: + self.output_manager.save_text(system_prompt, f"{topic_dir}/judger_system_prompt.txt") + self.output_manager.save_text(user_prompt, f"{topic_dir}/judger_user_prompt.txt") + + # 获取模型参数 + model_params = {} + if hasattr(self.content_config, 'judger_model') and isinstance(self.content_config.judger_model, dict): + model_params = { + 'temperature': self.content_config.judger_model.get('temperature'), + 'top_p': self.content_config.judger_model.get('top_p'), + 'presence_penalty': self.content_config.judger_model.get('presence_penalty') + } + # 移除None值 + model_params = {k: v for k, v in model_params.items() if v is not None} + + # 2. 调用AI进行审核 + try: + raw_result, _, _, _ = await self.ai_agent.generate_text( + system_prompt=system_prompt, + user_prompt=user_prompt, + use_stream=True, + stage="内容审核", + **model_params + ) + + # 保存原始响应 + if self.output_manager: + self.output_manager.save_text(raw_result, f"{topic_dir}/judger_raw_response.txt") + + except Exception as e: + logger.critical(f"内容审核时AI调用失败: {e}", exc_info=True) + return {"judge_success": False, "error": str(e)} + + # 3. 解析结果 + judged_data = process_llm_json_text(raw_result) + if judged_data and isinstance(judged_data, dict) and "title" in judged_data and "content" in judged_data: + judged_data["judge_success"] = True + + # 直接使用原始内容中的标签 + if original_tag: + judged_data["tag"] = original_tag + # 如果原始内容中没有标签,则使用默认标签 + logger.info(f"内容审核成功完成,使用标签: {judged_data.get('tag', [])}") + + # 保存审核后的内容 + if self.output_manager: + self.output_manager.save_json(judged_data, f"{topic_dir}/article_judged.json") + + return judged_data + else: + logger.warning(f"审核响应JSON格式不正确或缺少键") + return {"judge_success": False, "error": "Invalid JSON response", "raw_response": raw_result} + + async def judge_content_with_prompt(self, generated_content: Union[str, Dict[str, Any]], topic: Dict[str, Any], system_prompt: str, user_prompt: str) -> Dict[str, Any]: + """ + 使用预构建的提示词审核生成的内容 + + Args: + generated_content: 已生成的原始内容(JSON字符串或字典对象) + topic: 与内容相关的原始选题字典 + system_prompt: 系统提示词 + user_prompt: 用户提示词 + + Returns: + 一个包含审核结果的字典 + """ + logger.info("开始使用预构建提示词审核生成的内容...") + + # 获取主题索引,用于保存文件 + topic_index = topic.get('index', 'unknown') + topic_dir = f"topic_{topic_index}" + + # 从原始内容中提取tag + original_tag = [] + original_content = process_llm_json_text(generated_content) + if original_content and isinstance(original_content, dict) and "tag" in original_content: + original_tag = original_content.get("tag", []) + logger.info(f"从原始内容中提取到标签: {original_tag}") + else: + logger.warning("从原始内容提取标签失败") + + # 保存提示词 + if self.output_manager: + self.output_manager.save_text(system_prompt, f"{topic_dir}/judger_system_prompt.txt") + self.output_manager.save_text(user_prompt, f"{topic_dir}/judger_user_prompt.txt") + + # 获取模型参数 + model_params = {} + if hasattr(self.content_config, 'judger_model') and isinstance(self.content_config.judger_model, dict): + model_params = { + 'temperature': self.content_config.judger_model.get('temperature'), + 'top_p': self.content_config.judger_model.get('top_p'), + 'presence_penalty': self.content_config.judger_model.get('presence_penalty') + } + # 移除None值 + model_params = {k: v for k, v in model_params.items() if v is not None} + + # 2. 调用AI进行审核 + try: + raw_result, _, _, _ = await self.ai_agent.generate_text( + system_prompt=system_prompt, + user_prompt=user_prompt, + use_stream=True, + stage="内容审核", + **model_params + ) + + # 保存原始响应 + if self.output_manager: + self.output_manager.save_text(raw_result, f"{topic_dir}/judger_raw_response.txt") + + except Exception as e: + logger.critical(f"内容审核时AI调用失败: {e}", exc_info=True) + return {"judge_success": False, "error": str(e)} + + # 3. 解析结果 + judged_data = process_llm_json_text(raw_result) + if judged_data and isinstance(judged_data, dict) and "title" in judged_data and "content" in judged_data: + judged_data["judge_success"] = True + judged_data.pop("analysis") + # 直接使用原始内容中的标签 + if original_tag: + judged_data["tag"] = original_tag + # 如果原始内容中没有标签,则使用默认标签 + logger.info(f"内容审核成功完成,使用标签: {judged_data.get('tag', [])}") + + # 保存审核后的内容 + if self.output_manager: + self.output_manager.save_json(judged_data, f"{topic_dir}/article_judged.json") + + return judged_data + else: + logger.warning(f"审核响应JSON格式不正确或缺少键") + return {"judge_success": False, "error": "Invalid JSON response", "raw_response": raw_result} \ No newline at end of file diff --git a/core/algorithms/topic_generator.py b/core/algorithms/topic_generator.py new file mode 100644 index 0000000..407496d --- /dev/null +++ b/core/algorithms/topic_generator.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +选题生成模块 +""" + +import logging +from typing import Dict, Any, List, Optional, Tuple + +from core.ai import AIAgent +from core.config import ConfigManager, GenerateTopicConfig +from utils.prompts import TopicPromptBuilder +from utils.file_io import OutputManager, process_llm_json_text +from .topic_parser import TopicParser + +logger = logging.getLogger(__name__) + + +class TopicGenerator: + """ + 选题生成器 + 负责生成旅游相关的选题 + """ + + def __init__(self, ai_agent: AIAgent, config_manager: ConfigManager, output_manager: OutputManager): + """ + 初始化选题生成器 + + Args: + ai_agent: AI代理 + config_manager: 配置管理器 + output_manager: 输出管理器 + """ + self.ai_agent = ai_agent + self.config_manager = config_manager + self.config = config_manager.get_config('topic_gen', GenerateTopicConfig) + self.output_manager = output_manager + self.prompt_builder = TopicPromptBuilder(config_manager) + self.parser = TopicParser() + + async def generate_topics(self) -> Optional[List[Dict[str, Any]]]: + """ + 执行完整的选题生成流程:构建提示 -> 调用AI -> 解析结果 -> 保存产物 + """ + logger.info("开始执行选题生成流程...") + + # 1. 构建提示 + system_prompt = self.prompt_builder.get_system_prompt() + user_prompt = self.prompt_builder.build_user_prompt( + numTopics=self.config.topic.num, + month=self.config.topic.date + ) + self.output_manager.save_text(system_prompt, "topic_system_prompt.txt") + self.output_manager.save_text(user_prompt, "topic_user_prompt.txt") + + # 获取模型参数 + model_params = {} + if hasattr(self.config, 'model') and isinstance(self.config.model, dict): + model_params = { + 'temperature': self.config.model.get('temperature'), + 'top_p': self.config.model.get('top_p'), + 'presence_penalty': self.config.model.get('presence_penalty') + } + # 移除None值 + model_params = {k: v for k, v in model_params.items() if v is not None} + + # 2. 调用AI生成 + try: + raw_result, _, _, _ = await self.ai_agent.generate_text( + system_prompt=system_prompt, + user_prompt=user_prompt, + use_stream=True, # 选题生成通常不需要流式输出 + stage="选题生成", + **model_params + ) + self.output_manager.save_text(raw_result, "topics_raw_response.txt") + except Exception as e: + logger.critical(f"AI调用失败,无法生成选题: {e}", exc_info=True) + return None + + # 3. 解析结果 + topics = self.parser.parse(raw_result) + if not topics: + logger.error("未能从AI响应中解析出任何有效选题") + return None + + # 4. 保存结果 + self.output_manager.save_json(topics, "topics.json") + logger.info(f"成功生成并保存 {len(topics)} 个选题") + + return topics + + async def generate_topics_with_prompt(self, system_prompt: str, user_prompt: str) -> Optional[List[Dict[str, Any]]]: + """ + 使用预构建的提示词生成选题 + + Args: + system_prompt: 已构建好的系统提示词 + user_prompt: 已构建好的用户提示词 + + Returns: + 生成的选题列表,如果失败则返回None + """ + logger.info("使用预构建提示词开始执行选题生成流程...") + + # 保存提示以供调试 + self.output_manager.save_text(system_prompt, "topic_system_prompt.txt") + self.output_manager.save_text(user_prompt, "topic_user_prompt.txt") + + # 获取模型参数 + model_params = {} + if hasattr(self.config, 'model') and isinstance(self.config.model, dict): + model_params = { + 'temperature': self.config.model.get('temperature'), + 'top_p': self.config.model.get('top_p'), + 'presence_penalty': self.config.model.get('presence_penalty') + } + # 移除None值 + model_params = {k: v for k, v in model_params.items() if v is not None} + + # 调用AI生成 + try: + raw_result, _, _, _ = await self.ai_agent.generate_text( + system_prompt=system_prompt, + user_prompt=user_prompt, + use_stream=True, + stage="选题生成", + **model_params + ) + self.output_manager.save_text(raw_result, "topics_raw_response.txt") + except Exception as e: + logger.critical(f"AI调用失败,无法生成选题: {e}", exc_info=True) + return None + + # 解析结果 + topics = self.parser.parse(raw_result) + if not topics: + logger.error("未能从AI响应中解析出任何有效选题") + return None + + # 保存结果 + self.output_manager.save_json(topics, "topics.json") + logger.info(f"成功生成并保存 {len(topics)} 个选题") + + return topics + + + \ No newline at end of file diff --git a/core/algorithms/topic_parser.py b/core/algorithms/topic_parser.py new file mode 100644 index 0000000..0c225ee --- /dev/null +++ b/core/algorithms/topic_parser.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +AI响应解析器模块 +""" + +import logging +import json +from typing import List, Dict, Any +from utils.file_io import process_llm_json_text + +logger = logging.getLogger(__name__) + + +class TopicParser: + """ + 解析和验证由AI模型生成的选题列表 + """ + + @staticmethod + def parse(raw_text: str) -> List[Dict[str, Any]]: + """ + 从原始文本解析、修复和验证JSON + + Args: + raw_text: AI模型返回的原始字符串 + + Returns: + 一个字典列表,每个字典代表一个有效的选题 + """ + logger.info("开始解析AI生成的选题...") + + # 使用通用JSON解析函数解析原始文本 + parsed_json = process_llm_json_text(raw_text) + + if not parsed_json: + logger.error("解析AI响应失败,无法获取JSON数据") + return [] + + if not isinstance(parsed_json, list): + logger.error(f"解析结果不是列表,而是 {type(parsed_json)}") + return [] + + logger.info(f"成功解析 {len(parsed_json)} 个选题对象。开始验证...") + + # 验证每个选题是否包含所有必需的键 + valid_topics = [] + required_keys = {"index", "date", "logic", "object", "product", "style", "targetAudience"} + optional_keys = {"productLogic", "styleLogic", "targetAudienceLogic"} + + for i, item in enumerate(parsed_json): + if isinstance(item, dict) and required_keys.issubset(item.keys()): + valid_topics.append(item) + else: + logger.warning(f"第 {i+1} 个选题缺少必需键或格式不正确: {item}") + + logger.info(f"验证完成,获得 {len(valid_topics)} 个有效选题。") + return valid_topics \ No newline at end of file diff --git a/core/config/__pycache__/models.cpython-312.pyc b/core/config/__pycache__/models.cpython-312.pyc index c0851bf971edf1a9540663ad31774e845e9cb714..16b50c23eaf1ce4476cadac2480eb0ecf5fe1f85 100644 GIT binary patch delta 20 acmez6^~;Oe$GguL=N3bp|K^ delta 20 acmez6^~;O Optional[int]: + # 1. 精确匹配 + if target_name in mapping: + return mapping[target_name] + + # 2. 模糊匹配 - 去除空格后匹配 + target_clean = target_name.replace(" ", "").strip() + for name, id_val in mapping.items(): + if name.replace(" ", "").strip() == target_clean: + return id_val + + # 3. 包含匹配 - 检查是否互相包含 + for name, id_val in mapping.items(): + if target_clean in name.replace(" ", "") or name.replace(" ", "") in target_clean: + return id_val + + # 4. 未找到匹配 + logger.warning(f"未找到匹配的ID: '{target_name}'") + return None +``` + +#### 匹配率监控 +- 记录每个选题的ID匹配情况 +- 计算匹配率并在匹配率低于50%时发出警告 +- 添加匹配元数据用于调试 + +### 2. 数据库服务兜底机制 (`api/services/database_service.py`) + +#### 批量查询增强 +```python +def get_styles_by_ids(self, styleIds: List[int]) -> List[Dict[str, Any]]: + # 检查哪些ID没有找到对应记录 + found_ids = {result['id'] for result in results} + missing_ids = set(styleIds) - found_ids + + if missing_ids: + # 添加兜底数据 + fallback_styles = self._get_fallback_styles(list(missing_ids)) + results.extend(fallback_styles) +``` + +#### 兜底数据提供 +- 当数据库查询失败时,提供默认的结构化数据 +- 标记兜底数据 (`_is_fallback: True`) +- 确保系统可以继续运行 + +### 3. 内容生成阶段优化 (`api/services/tweet.py`) + +#### 智能数据增强 +```python +async def _enhance_topic_with_database_data(self, topic: Dict[str, Any]) -> Dict[str, Any]: + # 优先使用ID从数据库获取最新数据 + if 'styleIds' in topic and topic['styleIds']: + style_data = db_service.get_style_by_id(style_id) + if style_data: + enhanced_topic['style_object'] = style_data + enhanced_topic['style'] = style_data.get('styleName') +``` + +#### 多级兜底策略 +1. **第一级**:通过ID从数据库获取最新数据 +2. **第二级**:使用传入的对象参数作为兜底 +3. **第三级**:使用数据库服务的兜底数据 + +## 🔄 完整流程 + +### 选题生成阶段 +1. 接收ID列表 → 查询数据库获取完整对象 +2. 构建ID到名称的映射关系 +3. AI生成选题(包含名称) +4. 将生成的选题名称映射回ID +5. 返回包含ID的选题数据 + +### 内容生成阶段 +1. 接收带ID的选题数据 +2. 通过ID从数据库获取最新的详细信息 +3. 增强选题数据 +4. 生成内容 + +## 🎉 预期效果 + +### 1. 数据一致性 +- 确保整个流程中ID的连续性 +- 避免名称不匹配导致的数据丢失 + +### 2. 系统稳定性 +- 多级兜底机制确保系统不会因为数据库问题而崩溃 +- 详细的日志记录便于问题排查 + +### 3. 数据准确性 +- 内容生成时使用最新的数据库数据 +- 避免使用过期或不准确的缓存数据 + +### 4. 可观测性 +- 匹配率监控 +- 详细的日志记录 +- 兜底数据标记 + +## 🚀 使用建议 + +### 1. 监控日志 +关注以下日志信息: +- ID匹配率低于50%的警告 +- 兜底数据使用情况 +- 数据库查询失败的频率 + +### 2. 数据维护 +- 定期清理重复数据 +- 更新"请修改产品名字"等占位数据 +- 确保软删除字段的正确使用 + +### 3. 性能优化 +- 考虑为常用数据添加缓存 +- 优化数据库查询性能 +- 定期清理无效数据 + +## 📋 测试验证 + +### 1. 功能测试 +- 测试各种ID组合的选题生成 +- 验证名称匹配的准确性 +- 确认兜底机制的有效性 + +### 2. 性能测试 +- 大批量选题生成的性能 +- 数据库查询的响应时间 +- 内存使用情况 + +### 3. 错误处理测试 +- 数据库连接失败时的行为 +- 无效ID的处理 +- 数据缺失时的兜底效果 \ No newline at end of file diff --git a/tweet/__pycache__/__init__.cpython-312.pyc b/tweet/__pycache__/__init__.cpython-312.pyc index 5d0e44ae1ba42d7a5d164295364865311ad172c8..9e3ddb73a83d2a7c3bda6f6e303a116f3a0e90a4 100644 GIT binary patch delta 19 Zcmey%_?MCUG%qg~0}xp5?wH8^2>?421_1y7 delta 19 Zcmey%_?MCUG%qg~0}!}X=TGGR1OPd`1+@SG diff --git a/tweet/__pycache__/content_generator.cpython-312.pyc b/tweet/__pycache__/content_generator.cpython-312.pyc index f25325917b91f38ba139cfd788a3856109ca69fd..492ce5f7cdb9c07fbc5da6bb9d352ad93836c5f6 100644 GIT binary patch delta 20 acmbPlGv9{$G%qg~0}xp5?%2pJA_o9EP6ZnP delta 20 acmbPlGv9{$G%qg~0}x0YDcHy@A_o9DB?RjL diff --git a/tweet/__pycache__/content_judger.cpython-312.pyc b/tweet/__pycache__/content_judger.cpython-312.pyc index e61319dbff9794324dc5cc30171afdddefd8abbb..c6893e8edc0913384ee1577536d21b23e0cfb8f9 100644 GIT binary patch delta 20 acmezE`P-BGG%qg~0}xp5?%2rvK?MLyhz8F9 delta 20 acmezE`P-BGG%qg~0}vdjFW<=hK?ML!d