routes.py 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804
  1. from io import BytesIO, StringIO
  2. import os
  3. import pandas as pd
  4. from flask import request, jsonify, send_file, current_app
  5. from app.api.data_resource import bp
  6. from app.models.result import success, failed
  7. import logging
  8. import json
  9. import re
  10. from minio import Minio
  11. from app.core.graph.graph_operations import MyEncoder
  12. from app.services.neo4j_driver import neo4j_driver
  13. from app.core.data_resource.resource import (
  14. resource_list,
  15. handle_node,
  16. resource_kinship_graph,
  17. resource_impact_all_graph,
  18. model_resource_list,
  19. select_create_ddl,
  20. data_resource_edit,
  21. handle_id_resource,
  22. id_data_search_list,
  23. table_sql,
  24. select_sql,
  25. id_resource_graph,
  26. status_query
  27. )
  28. from app.core.meta_data import (
  29. translate_and_parse,
  30. infer_column_type,
  31. text_resource_solve,
  32. get_file_content,
  33. get_formatted_time
  34. )
  35. import traceback
  36. from app.core.system.auth import require_auth
  37. from app.core.llm.ddl_parser import DDLParser
  38. logger = logging.getLogger("app")
  39. def get_minio_client():
  40. """获取 MinIO 客户端实例"""
  41. return Minio(
  42. current_app.config['MINIO_HOST'],
  43. access_key=current_app.config['MINIO_USER'],
  44. secret_key=current_app.config['MINIO_PASSWORD'],
  45. secure=current_app.config['MINIO_SECURE']
  46. )
  47. def get_minio_config():
  48. """获取 MinIO 配置"""
  49. return {
  50. 'bucket_name': current_app.config['BUCKET_NAME'],
  51. 'prefix': current_app.config['PREFIX'],
  52. 'allowed_extensions': current_app.config['ALLOWED_EXTENSIONS']
  53. }
  54. def is_english(text):
  55. """检查文本是否为英文"""
  56. return text.isascii() and bool(re.match(r'^[a-zA-Z0-9_\s.,;:!?()\'"-]+$', text))
  57. @bp.route('/translate', methods=['POST'])
  58. def data_resource_translate():
  59. # 获取表单数据
  60. data_resource = request.form.get('data_resource')
  61. meta_data = request.form.get('meta_data')
  62. file = request.files.get('file')
  63. if not data_resource or not file:
  64. return jsonify(failed("缺少必要参数:data_resource 或文件"))
  65. # 处理meta_data可能为None的情况
  66. if meta_data:
  67. try:
  68. # 修复JSON解析问题,处理可能包含特殊引号的情况
  69. # 替换可能存在的特殊引号字符
  70. meta_data = meta_data.replace('â', '"').replace('"', '"').replace('"', '"')
  71. meta_data_list = json.loads(meta_data)
  72. except json.JSONDecodeError as e:
  73. logger.error(f"解析meta_data失败: {meta_data}, 错误: {str(e)}")
  74. # 尝试进行基本的字符串解析,以处理简单的数组格式
  75. if meta_data.startswith('[') and meta_data.endswith(']'):
  76. try:
  77. # 使用ast.literal_eval作为备用解析方法
  78. import ast
  79. meta_data_list = ast.literal_eval(meta_data)
  80. except Exception:
  81. # 如果仍然失败,使用简单的字符串分割
  82. meta_data = meta_data.strip('[]')
  83. meta_data_list = [item.strip('"\'') for item in meta_data.split(',')]
  84. else:
  85. meta_data_list = []
  86. else:
  87. logger.warning("meta_data为空,将使用空列表")
  88. meta_data_list = []
  89. # 构建翻译后的内容组合
  90. translated_meta_data_list = []
  91. for meta_item in meta_data_list:
  92. if is_english(meta_item): # 检查是否为英文
  93. translated_meta_data_list.append(meta_item) # 如果是英文,则直接添加
  94. else:
  95. translated_meta_data_list.append(translate_and_parse(meta_item)[0]) # 否则翻译后添加
  96. # 对 data_resource 进行翻译
  97. translated_data_resource = translate_and_parse(data_resource)
  98. if translated_data_resource and len(translated_data_resource) > 0:
  99. translated_data_resource = translated_data_resource[0]
  100. else:
  101. translated_data_resource = data_resource # 翻译失败时使用原值
  102. try:
  103. # 构建最终的翻译结果
  104. resource = {"name_zh": data_resource, "name_en": translated_data_resource}
  105. parsed_data = []
  106. # 读取文件内容
  107. file_content = file.read()
  108. # 重置文件指针
  109. file.seek(0)
  110. try:
  111. df = pd.read_excel(BytesIO(file_content))
  112. except Exception as e:
  113. return jsonify(failed(f"文件格式错误: {str(e)}"))
  114. # 获取列名和对应的数据类型
  115. # 如果meta_data为空,使用DataFrame的列名
  116. if not meta_data_list and not df.empty:
  117. meta_data_list = df.columns.tolist()
  118. translated_meta_data_list = []
  119. for col in meta_data_list:
  120. if is_english(col):
  121. translated_meta_data_list.append(col)
  122. else:
  123. translated_meta_data_list.append(translate_and_parse(col)[0])
  124. columns_and_types = infer_column_type(df)
  125. for i in range(len(meta_data_list)):
  126. zh = meta_data_list[i]
  127. en = translated_meta_data_list[i]
  128. data_type = columns_and_types[i] if i < len(columns_and_types) else "varchar(255)"
  129. parsed_item = {"name_zh": zh, "name_en": en, "data_type": data_type}
  130. parsed_data.append(parsed_item)
  131. response_data = {
  132. "head_data": parsed_data,
  133. "data_resource": resource
  134. }
  135. return jsonify(success(response_data, "success"))
  136. except Exception as e:
  137. logger.error(f"翻译处理失败: {str(e)}", exc_info=True)
  138. return jsonify(failed(str(e)))
  139. @bp.route('/save', methods=['POST'])
  140. def data_resource_save():
  141. """保存数据资源"""
  142. try:
  143. # 获取表单数据
  144. receiver = request.get_json()
  145. if not receiver:
  146. return jsonify(failed("参数不完整:缺少receiver"))
  147. # 检查url(允许为空)
  148. if 'url' not in receiver or not receiver['url']:
  149. logger.debug(f"url 为空")
  150. additional_info = receiver.get('additional_info')
  151. if not additional_info:
  152. return jsonify(failed("参数不完整: 缺少additional_info"))
  153. head_data = additional_info.get('head_data')
  154. # 获取 storage_location 和 data_source
  155. storage_location = receiver.get('storage_location', '').strip()
  156. # 向后兼容:data_source 可能在 receiver 顶层(新客户端)或 additional_info 内(旧客户端)
  157. # 使用显式 None 检查以支持 0 作为有效的节点ID
  158. data_source = receiver.get('data_source')
  159. if data_source is None:
  160. data_source = additional_info.get('data_source', '')
  161. # 验证:至少需要 storage_location 或 data_source 之一
  162. # 使用显式检查以支持 data_source=0(有效的节点ID)
  163. if not storage_location and data_source in (None, ''):
  164. return jsonify(failed("参数不完整:至少需要提供 storage_location 或 data_source"))
  165. # 获取资源类型(直接从前端上传的type字段获取)
  166. resource_type = receiver.get('type')
  167. if not resource_type:
  168. return jsonify(failed("参数不完整:缺少type字段"))
  169. # 调用业务逻辑创建数据资源
  170. # 只在 data_source 为 None 或空字符串时传 None,保留 0 作为有效值
  171. resource_id = handle_node(receiver, head_data, data_source=data_source if data_source not in (None, '') else None, resource_type=resource_type)
  172. return jsonify(success({"id": resource_id}))
  173. except Exception as e:
  174. logger.error(f"保存数据资源失败: {str(e)}")
  175. error_traceback = traceback.format_exc()
  176. logger.error(f"错误详情: {error_traceback}")
  177. return jsonify(failed(str(e)))
  178. @bp.route('/delete', methods=['POST'])
  179. def data_resource_delete():
  180. """删除数据资源"""
  181. try:
  182. # 获取资源ID
  183. if not request.json:
  184. return jsonify(failed("请求数据不能为空"))
  185. resource_id = request.json.get('id')
  186. if resource_id is None:
  187. return jsonify(failed("资源ID不能为空"))
  188. with neo4j_driver.get_session() as session:
  189. # 删除数据资源节点及其关系
  190. cypher = """
  191. MATCH (n:DataResource)
  192. WHERE id(n) = $resource_id
  193. DETACH DELETE n
  194. """
  195. session.run(cypher, resource_id=int(resource_id))
  196. return jsonify(success({"message": "数据资源删除成功"}))
  197. except Exception as e:
  198. logger.error(f"删除数据资源失败: {str(e)}")
  199. return jsonify(failed(str(e)))
  200. @bp.route('/update', methods=['POST'])
  201. def data_resource_update():
  202. """更新数据资源"""
  203. try:
  204. # 获取更新数据
  205. data = request.json
  206. if not data or "id" not in data:
  207. return jsonify(failed("参数不完整"))
  208. # 调用业务逻辑更新数据资源
  209. updated_data = data_resource_edit(data)
  210. return jsonify(success(updated_data))
  211. except Exception as e:
  212. logger.error(f"更新数据资源失败: {str(e)}")
  213. return jsonify(failed(str(e)))
  214. # 解析ddl,使用正则表达式匹配,但没有进行翻译,也没有对注释进行识别
  215. # 使用ddl创建数据资源时,调用该API
  216. @bp.route('/ddl', methods=['POST'])
  217. def id_data_ddl():
  218. """解析数据资源的DDL"""
  219. try:
  220. # 获取SQL内容
  221. if not request.json:
  222. return jsonify(failed("请求数据不能为空"))
  223. sql_content = request.json.get('sql', '')
  224. if not sql_content:
  225. return jsonify(failed("SQL内容不能为空"))
  226. # 记录原始SQL用于调试
  227. logger.debug(f"原始SQL: {sql_content}")
  228. # 提取创建表的DDL语句
  229. create_ddl_list = select_create_ddl(sql_content)
  230. if not create_ddl_list:
  231. return jsonify(failed("未找到有效的CREATE TABLE语句"))
  232. # 解析每个表定义
  233. tables_dict = {} # 最终返回的表字典
  234. for ddl in create_ddl_list:
  235. table_info = table_sql(ddl)
  236. if table_info:
  237. # table_info格式: {"table_name": {"exist": bool, "meta": [...], "table_comment": "..."}}
  238. # 合并到结果字典中
  239. tables_dict.update(table_info)
  240. if not tables_dict:
  241. return jsonify(failed("解析表结构失败"))
  242. # 记录结果
  243. logger.debug(f"解析结果: {json.dumps(tables_dict, ensure_ascii=False)}")
  244. # 直接返回解析结果
  245. return jsonify(success(tables_dict))
  246. except Exception as e:
  247. logger.error(f"解析DDL失败: {str(e)}")
  248. logger.error(traceback.format_exc()) # 添加详细错误堆栈
  249. return jsonify(failed(str(e)))
  250. @bp.route('/list', methods=['POST'])
  251. def data_resource_list():
  252. """获取数据资源列表"""
  253. try:
  254. # 获取分页和筛选参数
  255. if not request.json:
  256. return jsonify(failed('请求数据不能为空'))
  257. page = int(request.json.get('current', 1))
  258. page_size = int(request.json.get('size', 10))
  259. name_en_filter = request.json.get('name_en')
  260. name_zh_filter = request.json.get('name_zh')
  261. type_filter = request.json.get('type', 'all')
  262. category_filter = request.json.get('category')
  263. tag_filter = request.json.get('tag')
  264. # 调用业务逻辑查询数据资源列表
  265. resources, total_count = resource_list(
  266. page,
  267. page_size,
  268. name_en_filter,
  269. name_zh_filter,
  270. type_filter,
  271. category_filter,
  272. tag_filter
  273. )
  274. # 返回结果
  275. return jsonify(success({
  276. "records": resources,
  277. "total": total_count,
  278. "size": page_size,
  279. "current": page
  280. }))
  281. except Exception as e:
  282. logger.error(f"获取数据资源列表失败: {str(e)}")
  283. return jsonify(failed(str(e)))
  284. @bp.route('/search', methods=['POST'])
  285. def id_data_search():
  286. """数据资源关联元数据搜索"""
  287. try:
  288. # 获取分页和筛选参数
  289. if not request.json:
  290. return jsonify(failed('请求数据不能为空'))
  291. page = int(request.json.get('current', 1))
  292. page_size = int(request.json.get('size', 10))
  293. resource_id = request.json.get('id')
  294. name_en_filter = request.json.get('name_en')
  295. name_zh_filter = request.json.get('name_zh')
  296. category_filter = request.json.get('category')
  297. tag_filter = request.json.get('tag')
  298. if resource_id is None:
  299. return jsonify(failed("资源ID不能为空"))
  300. # 确保传入的ID为整数
  301. try:
  302. resource_id = int(resource_id)
  303. except (ValueError, TypeError):
  304. return jsonify(failed(f"资源ID必须为整数, 收到的是: {resource_id}"))
  305. # 记录请求信息
  306. logger.info(f"获取资源关联元数据请求,ID: {resource_id}")
  307. # 调用业务逻辑查询关联元数据
  308. metadata_list, total_count = id_data_search_list(
  309. resource_id,
  310. page,
  311. page_size,
  312. name_en_filter,
  313. name_zh_filter,
  314. category_filter,
  315. tag_filter
  316. )
  317. # 返回结果
  318. return jsonify(success({
  319. "records": metadata_list,
  320. "total": total_count,
  321. "size": page_size,
  322. "current": page
  323. }))
  324. except Exception as e:
  325. logger.error(f"数据资源关联元数据搜索失败: {str(e)}")
  326. return jsonify(failed(str(e)))
  327. def dynamic_type_conversion(value, target_type):
  328. """动态类型转换"""
  329. if value is None:
  330. return None
  331. if target_type == "int" or target_type == "INT":
  332. return int(value)
  333. elif target_type == "float" or target_type == "FLOAT" or target_type == "double" or target_type == "DOUBLE":
  334. return float(value)
  335. elif target_type == "bool" or target_type == "BOOL" or target_type == "boolean" or target_type == "BOOLEAN":
  336. if isinstance(value, str):
  337. return value.lower() in ('true', 'yes', '1', 't', 'y')
  338. return bool(value)
  339. else:
  340. return str(value)
  341. @bp.route('/graph/all', methods=['POST'])
  342. def data_resource_graph_all():
  343. """获取数据资源完整图谱"""
  344. try:
  345. # 获取参数
  346. if not request.json:
  347. return jsonify(failed('请求数据不能为空'))
  348. resource_id = request.json.get('id')
  349. meta = request.json.get('meta', True)
  350. if resource_id is None:
  351. return jsonify(failed("资源ID不能为空"))
  352. # 确保传入的ID为整数
  353. try:
  354. resource_id = int(resource_id)
  355. except (ValueError, TypeError):
  356. return jsonify(failed(f"资源ID必须为整数, 收到的是: {resource_id}"))
  357. # 调用业务逻辑获取完整图谱
  358. graph_data = resource_impact_all_graph(resource_id, meta)
  359. return jsonify(success(graph_data))
  360. except Exception as e:
  361. logger.error(f"获取数据资源完整图谱失败: {str(e)}")
  362. return jsonify(failed(str(e)))
  363. @bp.route('/graph', methods=['POST'])
  364. def data_resource_list_graph():
  365. """获取数据资源亲缘关系图谱"""
  366. try:
  367. # 获取参数
  368. if not request.json:
  369. return jsonify(failed('请求数据不能为空'))
  370. resource_id = request.json.get('id')
  371. meta = request.json.get('meta', True)
  372. if resource_id is None:
  373. return jsonify(failed("资源ID不能为空"))
  374. # 确保传入的ID为整数
  375. try:
  376. resource_id = int(resource_id)
  377. except (ValueError, TypeError):
  378. return jsonify(failed(f"资源ID必须为整数, 收到的是: {resource_id}"))
  379. # 记录请求信息
  380. logger.info(f"获取图谱请求,ID: {resource_id}")
  381. # 调用业务逻辑获取图谱
  382. graph_data = resource_kinship_graph(resource_id, meta)
  383. return jsonify(success(graph_data))
  384. except Exception as e:
  385. logger.error(f"获取数据资源亲缘关系图谱失败: {str(e)}")
  386. return jsonify(failed(str(e)))
  387. @bp.route('/save/metadata', methods=['POST'])
  388. def id_data_save():
  389. """保存数据资源关联的元数据"""
  390. try:
  391. # 获取参数
  392. if not request.json:
  393. return jsonify(failed('请求数据不能为空'))
  394. resource_id = request.json.get('id')
  395. metadata_list = request.json.get('data', [])
  396. if resource_id is None:
  397. return jsonify(failed("资源ID不能为空"))
  398. if not metadata_list:
  399. return jsonify(failed("元数据列表不能为空"))
  400. # 处理元数据保存
  401. with neo4j_driver.get_session() as session:
  402. # 获取数据资源名称
  403. resource_query = """
  404. MATCH (n:DataResource)
  405. WHERE id(n) = $resource_id
  406. RETURN n.name as resource_name
  407. """
  408. resource_result = session.run(resource_query, resource_id=int(resource_id))
  409. resource_record = resource_result.single()
  410. if not resource_record:
  411. return jsonify(failed(f"未找到ID为{resource_id}的数据资源"))
  412. resource_name = resource_record["resource_name"]
  413. # 先删除现有关系
  414. cypher_delete = """
  415. MATCH (n:DataResource)-[r:INCLUDES]->()
  416. WHERE id(n) = $resource_id
  417. DELETE r
  418. """
  419. session.run(cypher_delete, resource_id=int(resource_id))
  420. # 添加新关系
  421. for meta in metadata_list:
  422. # 创建元数据节点
  423. meta_cypher = """
  424. MERGE (m:DataMeta {name_zh: $name_zh})
  425. ON CREATE SET m.name_en = $name_en,
  426. m.create_time = $create_time,
  427. m.data_type = $type
  428. ON MATCH SET m.data_type = $type
  429. RETURN m
  430. """
  431. create_time = get_formatted_time()
  432. meta_result = session.run(
  433. meta_cypher,
  434. name_zh=meta["name_zh"],
  435. name_en=meta["name_en"],
  436. create_time=create_time,
  437. type=meta["data_type"]
  438. )
  439. meta_record = meta_result.single()
  440. if not meta_record:
  441. logger.error(f"创建元数据节点失败: {meta['name_zh']}")
  442. continue
  443. meta_node = meta_record["m"]
  444. meta_id = meta_node.id
  445. # 打印节点ID信息,便于调试
  446. logger.info(f"元数据节点ID: {meta_id}, 类型: {type(meta_id)}")
  447. logger.info(f"数据资源节点ID: {resource_id}, 类型: {type(resource_id)}")
  448. # 使用明确的属性名匹配而不是ID
  449. rel_cypher = """
  450. MATCH (a:DataResource {name: $r_name}), (m:DataMeta {name: $m_name})
  451. MERGE (a)-[r:INCLUDES]->(m)
  452. RETURN r
  453. """
  454. rel_result = session.run(
  455. rel_cypher,
  456. r_name=resource_name,
  457. m_name=meta["name"]
  458. )
  459. # 检查关系是否创建成功
  460. if rel_result.single():
  461. logger.info(f"成功创建关系: {resource_name} -> {meta['name']}")
  462. else:
  463. logger.warning(f"关系创建结果为空")
  464. # 额外验证关系是否创建
  465. verify_cypher = """
  466. MATCH (a:DataResource {name: $r_name})-[r:INCLUDES]->(m:DataMeta {name: $m_name})
  467. RETURN count(r) as rel_count
  468. """
  469. verify_result = session.run(
  470. verify_cypher,
  471. r_name=resource_name,
  472. m_name=meta["name"]
  473. )
  474. verify_record = verify_result.single()
  475. count = verify_record["rel_count"] if verify_record else 0
  476. logger.info(f"验证关系数量: {count}")
  477. return jsonify(success({"message": "元数据保存成功"}))
  478. except Exception as e:
  479. logger.error(f"保存数据资源关联的元数据失败: {str(e)}")
  480. return jsonify(failed(str(e)))
  481. @bp.route('/sql/test', methods=['POST'])
  482. def sql_test():
  483. """测试SQL查询"""
  484. try:
  485. # 获取参数
  486. if not request.json:
  487. return jsonify(failed('请求数据不能为空'))
  488. sql_query = request.json.get('sql', '')
  489. if not sql_query:
  490. return jsonify(failed("SQL查询不能为空"))
  491. # 解析SQL
  492. parsed_sql = select_sql(sql_query)
  493. if not parsed_sql:
  494. return jsonify(failed("解析SQL失败"))
  495. # 返回解析结果
  496. return jsonify(success(parsed_sql))
  497. except Exception as e:
  498. logger.error(f"测试SQL查询失败: {str(e)}")
  499. return jsonify(failed(str(e)))
  500. # 使用LLM识别DDL语句,用来代替原来的正则的方式
  501. # 用于在数据资源创建时,识别DDL语句 /api/resource/ddl/parse
  502. @bp.route('/ddl/parse', methods=['POST'])
  503. def ddl_identify():
  504. """识别DDL语句"""
  505. try:
  506. # 获取参数 - 支持两种方式:上传文件或JSON
  507. sql_content = ''
  508. # 检查是否有文件上传
  509. if 'file' in request.files:
  510. file = request.files['file']
  511. # 检查文件是否存在且文件名不为空
  512. if file and file.filename:
  513. # 检查是否是SQL文件
  514. if not file.filename.lower().endswith('.sql'):
  515. return jsonify(failed("只接受SQL文件"))
  516. # 读取文件内容
  517. sql_content = file.read().decode('utf-8')
  518. logger.info(f"从上传的文件中读取SQL内容,文件名: {file.filename}")
  519. # 如果没有文件上传,检查是否有JSON输入
  520. elif request.is_json and request.json:
  521. sql_content = request.json.get('sql', '')
  522. # 如果两种方式都没有提供SQL内容,则返回错误
  523. if not sql_content:
  524. return jsonify(failed("SQL内容不能为空,请上传SQL文件或提供SQL内容"))
  525. parser = DDLParser()
  526. # 提取创建表的DDL语句
  527. ddl_list = parser.parse_ddl(sql_content)
  528. if not ddl_list:
  529. return jsonify(failed("未找到有效的CREATE TABLE语句"))
  530. # 处理表的存在状态
  531. if isinstance(ddl_list, dict):
  532. # 获取所有表名
  533. table_names = list(ddl_list.keys())
  534. # 首先为所有表设置默认的exist状态
  535. for table_name in table_names:
  536. # 确保 ddl_list[table_name] 是字典类型
  537. if isinstance(ddl_list[table_name], dict):
  538. ddl_list[table_name]["exist"] = False
  539. else:
  540. logger.warning(f"表 {table_name} 的值不是字典类型: {type(ddl_list[table_name])}")
  541. if table_names:
  542. try:
  543. # 查询表是否存在
  544. with neo4j_driver.get_session() as session:
  545. table_query = """
  546. UNWIND $names AS name
  547. OPTIONAL MATCH (n:DataResource {name_en: name})
  548. RETURN name, n IS NOT NULL AS exists
  549. """
  550. table_results = session.run(table_query, names=table_names)
  551. # 更新存在的表的状态
  552. for record in table_results:
  553. table_name = record["name"]
  554. exists = record["exists"]
  555. # 确保表名存在且对应的值是字典类型
  556. if table_name in ddl_list and isinstance(ddl_list[table_name], dict):
  557. ddl_list[table_name]["exist"] = exists
  558. except Exception as e:
  559. logger.error(f"检查表存在状态失败: {str(e)}")
  560. # 如果查询失败,所有表保持默认的False状态
  561. logger.debug(f"识别到的DDL语句: {json.dumps(ddl_list, ensure_ascii=False)}")
  562. return jsonify(success(ddl_list))
  563. except Exception as e:
  564. logger.error(f"识别DDL语句失败: {str(e)}")
  565. logger.error(traceback.format_exc()) # 添加详细错误堆栈
  566. return jsonify(failed(str(e)))
  567. # 废弃的识别DDL语句方法,该API 与 ddl API 功能类似,但功能简化了
  568. @bp.route('/ddl/identify', methods=['POST'])
  569. def sql_ddl_identify():
  570. """识别DDL语句"""
  571. try:
  572. # 获取参数
  573. if not request.json:
  574. return jsonify(failed('请求数据不能为空'))
  575. sql_content = request.json.get('sql', '')
  576. if not sql_content:
  577. return jsonify(failed("SQL内容不能为空"))
  578. # 提取创建表的DDL语句
  579. create_ddl_list = select_create_ddl(sql_content)
  580. if not create_ddl_list:
  581. return jsonify(failed("未找到有效的CREATE TABLE语句"))
  582. return jsonify(success({"count": len(create_ddl_list)}))
  583. except Exception as e:
  584. logger.error(f"识别DDL语句失败: {str(e)}")
  585. return jsonify(failed(str(e)))
  586. @bp.route('/model/list', methods=['POST'])
  587. def resource_model_list():
  588. """获取模型资源列表"""
  589. try:
  590. # 获取分页和筛选参数
  591. if not request.json:
  592. return jsonify(failed('请求数据不能为空'))
  593. page = int(request.json.get('current', 1))
  594. page_size = int(request.json.get('size', 10))
  595. name_filter = request.json.get('name')
  596. # 调用业务逻辑查询模型资源列表
  597. resources, total_count = model_resource_list(page, page_size, name_filter)
  598. # 返回结果
  599. return jsonify(success({
  600. "records": resources,
  601. "total": total_count,
  602. "size": page_size,
  603. "current": page
  604. }))
  605. except Exception as e:
  606. logger.error(f"获取模型资源列表失败: {str(e)}")
  607. return jsonify(failed(str(e)))
  608. @bp.route('/detail', methods=['POST'])
  609. def data_resource_detail():
  610. """获取数据资源详情"""
  611. try:
  612. # 获取资源ID
  613. if not request.json:
  614. return jsonify(failed('请求数据不能为空'))
  615. resource_id = request.json.get('id')
  616. if resource_id is None:
  617. return jsonify(failed("资源ID不能为空"))
  618. # 确保传入的ID为整数
  619. try:
  620. resource_id = int(resource_id)
  621. except (ValueError, TypeError):
  622. return jsonify(failed(f"资源ID必须为整数, 收到的是: {resource_id}"))
  623. # 记录请求信息
  624. logger.info(f"获取资源详情请求,ID: {resource_id}")
  625. # 调用业务逻辑查询数据资源详情
  626. resource_data = handle_id_resource(resource_id)
  627. if not resource_data:
  628. logger.error(f"资源不存在,ID: {resource_id}")
  629. return jsonify(failed("资源不存在"))
  630. # 记录从handle_id_resource返回的数据
  631. logger.info(f"handle_id_resource返回数据,describe字段: {resource_data.get('describe')}")
  632. # 确保返回的数据格式符合要求
  633. response_data = {
  634. "parsed_data": resource_data.get("parsed_data", []),
  635. "tag": resource_data.get("tag", {"name_zh": None, "name_en": None, "id": None}),
  636. "leader": resource_data.get("leader", ""),
  637. "organization": resource_data.get("organization", ""),
  638. "name_zh": resource_data.get("name_zh", ""),
  639. "name_en": resource_data.get("name_en", ""),
  640. "data_sensitivity": resource_data.get("data_sensitivity", ""),
  641. "storage_location": resource_data.get("storage_location", "/"),
  642. "create_time": resource_data.get("create_time", ""),
  643. "update_time": resource_data.get("update_time", ""),
  644. "type": resource_data.get("type", ""),
  645. "category": resource_data.get("category", ""),
  646. "url": resource_data.get("url", ""),
  647. "frequency": resource_data.get("frequency", ""),
  648. "status": resource_data.get("status", True),
  649. "id": resource_data.get("id"),
  650. "keywords": resource_data.get("keywords", []),
  651. "describe": resource_data.get("describe", ""),
  652. "data_source": resource_data.get("data_source") # 新增:数据源节点ID
  653. }
  654. # 记录最终返回的数据
  655. logger.info(f"最终返回的response_data,describe字段: {response_data.get('describe')}")
  656. return jsonify(success(response_data))
  657. except Exception as e:
  658. logger.error(f"获取数据资源详情失败: {str(e)}")
  659. return jsonify(failed(str(e)))
  660. @bp.route('/config', methods=['GET'])
  661. @require_auth
  662. def get_resource_config():
  663. """获取数据资源配置信息"""
  664. config = get_minio_config()
  665. return jsonify({
  666. 'allowed_extensions': list(config['allowed_extensions']),
  667. 'bucket_name': config['bucket_name'],
  668. 'prefix': config['prefix']
  669. })