routes.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. from flask import request, jsonify, send_from_directory, send_file, current_app
  2. from app.api.meta_data import bp
  3. from app.models.result import success, failed
  4. import logging
  5. import json
  6. import io
  7. import os
  8. from minio import Minio
  9. from minio.error import S3Error
  10. from app.services.neo4j_driver import neo4j_driver
  11. from app.services.package_function import create_or_get_node, relationship_exists
  12. from app.core.meta_data import (
  13. translate_and_parse,
  14. get_formatted_time,
  15. meta_list,
  16. meta_kinship_graph,
  17. meta_impact_graph,
  18. parse_text,
  19. parse_entity_relation,
  20. handle_txt_graph,
  21. get_file_content,
  22. text_resource_solve,
  23. handle_id_unstructured,
  24. solve_unstructured_data
  25. )
  26. from app.core.system.auth import require_auth
  27. logger = logging.getLogger("app")
  28. def get_minio_client():
  29. """获取 MinIO 客户端实例"""
  30. return Minio(
  31. current_app.config['MINIO_HOST'],
  32. access_key=current_app.config['MINIO_USER'],
  33. secret_key=current_app.config['MINIO_PASSWORD'],
  34. secure=current_app.config['MINIO_SECURE']
  35. )
  36. def get_minio_config():
  37. """获取 MinIO 配置"""
  38. return {
  39. 'bucket_name': current_app.config['BUCKET_NAME'],
  40. 'prefix': current_app.config['PREFIX'],
  41. 'allowed_extensions': current_app.config['ALLOWED_EXTENSIONS']
  42. }
  43. def allowed_file(filename):
  44. """检查文件扩展名是否允许"""
  45. return '.' in filename and filename.rsplit('.', 1)[1].lower() in get_minio_config()['allowed_extensions']
  46. # 元数据列表
  47. @bp.route('/node/list', methods=['POST'])
  48. def meta_node_list():
  49. try:
  50. # 从请求中获取分页参数
  51. page = int(request.json.get('current', 1))
  52. page_size = int(request.json.get('size', 10))
  53. # 获取搜索参数
  54. search = request.json.get('search', '')
  55. en_name_filter = request.json.get('en_name', None)
  56. name_filter = request.json.get('name', None)
  57. category_filter = request.json.get('category', None)
  58. time_filter = request.json.get('time', None)
  59. tag_filter = request.json.get('tag', None)
  60. # 调用核心业务逻辑
  61. result, total_count = meta_list(
  62. page,
  63. page_size,
  64. search,
  65. en_name_filter,
  66. name_filter,
  67. category_filter,
  68. time_filter,
  69. tag_filter
  70. )
  71. # 返回结果
  72. return jsonify(success({
  73. "records": result,
  74. "total": total_count,
  75. "size": page_size,
  76. "current": page
  77. }))
  78. except Exception as e:
  79. logger.error(f"获取元数据列表失败: {str(e)}")
  80. return jsonify(failed(str(e)))
  81. # 元数据图谱
  82. @bp.route('/node/graph', methods=['POST'])
  83. def meta_node_graph():
  84. try:
  85. # 从请求中获取节点ID
  86. node_id = request.json.get('nodeId')
  87. # 调用核心业务逻辑
  88. result = meta_kinship_graph(node_id)
  89. # 返回结果
  90. return jsonify(success(result))
  91. except Exception as e:
  92. logger.error(f"获取元数据图谱失败: {str(e)}")
  93. return jsonify(failed(str(e)))
  94. # 删除元数据
  95. @bp.route('/node/delete', methods=['POST'])
  96. def meta_node_delete():
  97. try:
  98. # 从请求中获取节点ID
  99. node_id = request.json.get('id')
  100. # 删除节点逻辑
  101. with neo4j_driver.get_session() as session:
  102. cypher = "MATCH (n) WHERE id(n) = $node_id DETACH DELETE n"
  103. session.run(cypher, node_id=int(node_id))
  104. # 返回结果
  105. return jsonify(success({}))
  106. except Exception as e:
  107. logger.error(f"删除元数据失败: {str(e)}")
  108. return jsonify(failed(str(e)))
  109. # 编辑元数据
  110. @bp.route('/node/edit', methods=['POST'])
  111. def meta_node_edit():
  112. try:
  113. # 从请求中获取节点信息
  114. node_id = request.json.get('id')
  115. node_name = request.json.get('name')
  116. node_type = request.json.get('type')
  117. node_desc = request.json.get('desc', '')
  118. node_properties = request.json.get('properties', {})
  119. with neo4j_driver.get_session() as session:
  120. # 更新节点属性
  121. cypher = """
  122. MATCH (n) WHERE id(n) = $node_id
  123. SET n.name = $name, n.type = $type, n.desc = $desc,
  124. n.properties = $properties, n.updateTime = $update_time
  125. RETURN n
  126. """
  127. update_time = get_formatted_time()
  128. result = session.run(
  129. cypher,
  130. node_id=int(node_id),
  131. name=node_name,
  132. type=node_type,
  133. desc=node_desc,
  134. properties=node_properties,
  135. update_time=update_time
  136. )
  137. node = result.single()
  138. if node:
  139. return jsonify(success(dict(node["n"])))
  140. else:
  141. return jsonify(failed("节点不存在"))
  142. except Exception as e:
  143. logger.error(f"编辑元数据失败: {str(e)}")
  144. return jsonify(failed(str(e)))
  145. # 增加元数据
  146. @bp.route('/node/add', methods=['POST'])
  147. def meta_node_add():
  148. try:
  149. # 从请求中获取节点信息
  150. node_name = request.json.get('name')
  151. node_type = request.json.get('type')
  152. node_desc = request.json.get('desc', '')
  153. node_properties = request.json.get('properties', {})
  154. # 创建节点
  155. with neo4j_driver.get_session() as session:
  156. cypher = """
  157. CREATE (n:meta_data {name: $name, type: $type, desc: $desc,
  158. properties: $properties, createTime: $create_time,
  159. updateTime: $update_time})
  160. RETURN n
  161. """
  162. create_time = update_time = get_formatted_time()
  163. result = session.run(
  164. cypher,
  165. name=node_name,
  166. type=node_type,
  167. desc=node_desc,
  168. properties=node_properties,
  169. create_time=create_time,
  170. update_time=update_time
  171. )
  172. node = result.single()
  173. return jsonify(success(dict(node["n"])))
  174. except Exception as e:
  175. logger.error(f"添加元数据失败: {str(e)}")
  176. return jsonify(failed(str(e)))
  177. # 搜索元数据
  178. @bp.route('/search', methods=['GET'])
  179. def search_metadata_route():
  180. try:
  181. keyword = request.args.get('keyword', '')
  182. if not keyword:
  183. return jsonify(success([]))
  184. cypher = """
  185. MATCH (n:meta_data)
  186. WHERE n.name CONTAINS $keyword
  187. RETURN n LIMIT 100
  188. """
  189. with neo4j_driver.get_session() as session:
  190. result = session.run(cypher, keyword=keyword)
  191. metadata_list = [dict(record["n"]) for record in result]
  192. return jsonify(success(metadata_list))
  193. except Exception as e:
  194. logger.error(f"搜索元数据失败: {str(e)}")
  195. return jsonify(failed(str(e)))
  196. # 全文检索查询
  197. @bp.route('/full/text/query', methods=['POST'])
  198. def full_text_query():
  199. try:
  200. # 获取查询条件
  201. query = request.json.get('query', '')
  202. if not query:
  203. return jsonify(failed("查询条件不能为空"))
  204. # 执行Neo4j全文索引查询
  205. with neo4j_driver.get_session() as session:
  206. cypher = """
  207. CALL db.index.fulltext.queryNodes("meta_dataFulltext", $query)
  208. YIELD node, score
  209. RETURN node, score
  210. ORDER BY score DESC
  211. LIMIT 20
  212. """
  213. result = session.run(cypher, query=query)
  214. # 处理查询结果
  215. search_results = []
  216. for record in result:
  217. node_data = dict(record["node"])
  218. node_data["id"] = record["node"].id
  219. node_data["score"] = record["score"]
  220. search_results.append(node_data)
  221. return jsonify(success(search_results))
  222. except Exception as e:
  223. logger.error(f"全文检索查询失败: {str(e)}")
  224. return jsonify(failed(str(e)))
  225. # 非结构化文本查询
  226. @bp.route('/unstructure/text/query', methods=['POST'])
  227. def unstructure_text_query():
  228. try:
  229. # 获取查询参数
  230. node_id = request.json.get('id')
  231. if not node_id:
  232. return jsonify(failed("节点ID不能为空"))
  233. # 获取节点信息
  234. node_data = handle_id_unstructured(node_id)
  235. if not node_data:
  236. return jsonify(failed("节点不存在"))
  237. # 获取对象路径
  238. object_name = node_data.get('url')
  239. if not object_name:
  240. return jsonify(failed("文档路径不存在"))
  241. # 从MinIO获取文件内容
  242. file_content = get_file_content(minio_client, bucket_name, object_name)
  243. # 解析文本内容
  244. parsed_data = parse_text(file_content)
  245. # 返回结果
  246. result = {
  247. "node": node_data,
  248. "parsed": parsed_data,
  249. "content": file_content[:1000] + "..." if len(file_content) > 1000 else file_content
  250. }
  251. return jsonify(success(result))
  252. except Exception as e:
  253. logger.error(f"非结构化文本查询失败: {str(e)}")
  254. return jsonify(failed(str(e)))
  255. # 文件上传
  256. @bp.route('/resource/upload', methods=['POST'])
  257. def upload_file():
  258. try:
  259. # 检查请求中是否有文件
  260. if 'file' not in request.files:
  261. return jsonify(failed("没有找到上传的文件"))
  262. file = request.files['file']
  263. # 检查文件名
  264. if file.filename == '':
  265. return jsonify(failed("未选择文件"))
  266. # 检查文件类型
  267. if not allowed_file(file.filename):
  268. return jsonify(failed("不支持的文件类型"))
  269. # 获取 MinIO 配置
  270. minio_client = get_minio_client()
  271. config = get_minio_config()
  272. # 上传到MinIO
  273. file_content = file.read()
  274. file_size = len(file_content)
  275. file_type = file.filename.rsplit('.', 1)[1].lower()
  276. # 提取文件名(不包含扩展名)
  277. filename_without_ext = file.filename.rsplit('.', 1)[0]
  278. # 生成紧凑的时间戳 (yyyyMMddHHmmss)
  279. import time
  280. timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime())
  281. # 生成唯一文件名
  282. object_name = f"{config['prefix']}/{filename_without_ext}_{timestamp}.{file_type}"
  283. # 上传文件
  284. minio_client.put_object(
  285. config['bucket_name'],
  286. object_name,
  287. io.BytesIO(file_content),
  288. file_size,
  289. content_type=f"application/{file_type}"
  290. )
  291. # 返回结果
  292. return jsonify(success({
  293. "filename": file.filename,
  294. "size": file_size,
  295. "type": file_type,
  296. "url": object_name
  297. }))
  298. except Exception as e:
  299. logger.error(f"文件上传失败: {str(e)}")
  300. return jsonify(failed(str(e)))
  301. # 文件下载显示
  302. @bp.route('/resource/display', methods=['POST'])
  303. def upload_file_display():
  304. response = None
  305. try:
  306. object_name = request.json.get('url')
  307. if not object_name:
  308. return jsonify(failed("文件路径不能为空"))
  309. # 获取 MinIO 配置
  310. minio_client = get_minio_client()
  311. config = get_minio_config()
  312. # 获取文件内容
  313. response = minio_client.get_object(config['bucket_name'], object_name)
  314. file_data = response.read()
  315. # 获取文件名
  316. file_name = object_name.split('/')[-1]
  317. # 确定文件类型
  318. file_extension = file_name.split('.')[-1].lower()
  319. # 为不同文件类型设置合适的MIME类型
  320. mime_types = {
  321. 'pdf': 'application/pdf',
  322. 'doc': 'application/msword',
  323. 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  324. 'xls': 'application/vnd.ms-excel',
  325. 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  326. 'txt': 'text/plain',
  327. 'csv': 'text/csv'
  328. }
  329. content_type = mime_types.get(file_extension, 'application/octet-stream')
  330. # 返回结果
  331. return jsonify(success({
  332. "filename": file_name,
  333. "type": file_extension,
  334. "contentType": content_type,
  335. "size": len(file_data),
  336. "url": f"/api/meta/resource/download?url={object_name}"
  337. }))
  338. except S3Error as e:
  339. logger.error(f"MinIO操作失败: {str(e)}")
  340. return jsonify(failed(f"文件访问失败: {str(e)}"))
  341. except Exception as e:
  342. logger.error(f"文件显示信息获取失败: {str(e)}")
  343. return jsonify(failed(str(e)))
  344. finally:
  345. if response:
  346. response.close()
  347. response.release_conn()
  348. # 文件下载接口
  349. @bp.route('/resource/download', methods=['GET'])
  350. def download_file():
  351. response = None
  352. try:
  353. object_name = request.args.get('url')
  354. if not object_name:
  355. return jsonify(failed("文件路径不能为空"))
  356. # URL解码,处理特殊字符
  357. import urllib.parse
  358. object_name = urllib.parse.unquote(object_name)
  359. # 记录下载请求信息,便于调试
  360. logger.info(f"下载文件请求: {object_name}")
  361. # 获取 MinIO 配置
  362. minio_client = get_minio_client()
  363. config = get_minio_config()
  364. # 获取文件
  365. try:
  366. response = minio_client.get_object(config['bucket_name'], object_name)
  367. file_data = response.read()
  368. except S3Error as e:
  369. logger.error(f"MinIO获取文件失败: {str(e)}")
  370. return jsonify(failed(f"文件获取失败: {str(e)}"))
  371. # 获取文件名,并处理特殊字符
  372. file_name = object_name.split('/')[-1]
  373. # 直接从内存返回文件,不创建临时文件
  374. file_stream = io.BytesIO(file_data)
  375. # 返回文件
  376. return send_file(
  377. file_stream,
  378. as_attachment=True,
  379. download_name=file_name,
  380. mimetype="application/octet-stream"
  381. )
  382. except Exception as e:
  383. logger.error(f"文件下载失败: {str(e)}")
  384. return jsonify(failed(str(e)))
  385. finally:
  386. if response:
  387. response.close()
  388. response.release_conn()
  389. # 文本资源翻译
  390. @bp.route('/resource/translate', methods=['POST'])
  391. def text_resource_translate():
  392. try:
  393. # 获取参数
  394. name = request.json.get('name', '')
  395. keyword = request.json.get('keyword', '')
  396. if not name:
  397. return jsonify(failed("名称不能为空"))
  398. # 调用资源处理逻辑
  399. result = text_resource_solve(None, name, keyword)
  400. return jsonify(success(result))
  401. except Exception as e:
  402. logger.error(f"文本资源翻译失败: {str(e)}")
  403. return jsonify(failed(str(e)))
  404. # 创建文本资源节点
  405. @bp.route('/resource/node', methods=['POST'])
  406. def text_resource_node():
  407. try:
  408. # 获取参数
  409. name = request.json.get('name', '')
  410. en_name = request.json.get('en_name', '')
  411. keywords = request.json.get('keywords', [])
  412. keywords_en = request.json.get('keywords_en', [])
  413. object_name = request.json.get('url', '')
  414. if not name or not en_name or not object_name:
  415. return jsonify(failed("参数不完整"))
  416. # 创建节点
  417. with neo4j_driver.get_session() as session:
  418. # 创建资源节点
  419. cypher = """
  420. CREATE (n:meta_data {
  421. name: $name,
  422. en_name: $en_name,
  423. keywords: $keywords,
  424. keywords_en: $keywords_en,
  425. url: $object_name,
  426. createTime: $create_time,
  427. updateTime: $update_time
  428. })
  429. RETURN n
  430. """
  431. create_time = update_time = get_formatted_time()
  432. result = session.run(
  433. cypher,
  434. name=name,
  435. en_name=en_name,
  436. keywords=keywords,
  437. keywords_en=keywords_en,
  438. object_name=object_name,
  439. create_time=create_time,
  440. update_time=update_time
  441. )
  442. node = result.single()["n"]
  443. # 为每个关键词创建标签节点并关联
  444. for i, keyword in enumerate(keywords):
  445. if keyword:
  446. # 创建标签节点
  447. tag_cypher = """
  448. MERGE (t:Tag {name: $name})
  449. ON CREATE SET t.en_name = $en_name, t.createTime = $create_time
  450. RETURN t
  451. """
  452. tag_result = session.run(
  453. tag_cypher,
  454. name=keyword,
  455. en_name=keywords_en[i] if i < len(keywords_en) else "",
  456. create_time=create_time
  457. )
  458. tag_node = tag_result.single()["t"]
  459. # 创建关系
  460. rel_cypher = """
  461. MATCH (n), (t)
  462. WHERE id(n) = $node_id AND id(t) = $tag_id
  463. CREATE (n)-[r:HAS_TAG]->(t)
  464. RETURN r
  465. """
  466. session.run(
  467. rel_cypher,
  468. node_id=node.id,
  469. tag_id=tag_node.id
  470. )
  471. # 返回创建的节点
  472. return jsonify(success(dict(node)))
  473. except Exception as e:
  474. logger.error(f"创建文本资源节点失败: {str(e)}")
  475. return jsonify(failed(str(e)))
  476. # 处理非结构化数据
  477. @bp.route('/unstructured/process', methods=['POST'])
  478. def processing_unstructured_data():
  479. try:
  480. # 获取参数
  481. node_id = request.json.get('id')
  482. if not node_id:
  483. return jsonify(failed("节点ID不能为空"))
  484. # 调用处理逻辑
  485. result = solve_unstructured_data(node_id, minio_client, prefix)
  486. if result:
  487. return jsonify(success({"message": "处理成功"}))
  488. else:
  489. return jsonify(failed("处理失败"))
  490. except Exception as e:
  491. logger.error(f"处理非结构化数据失败: {str(e)}")
  492. return jsonify(failed(str(e)))
  493. # 创建文本图谱
  494. @bp.route('/text/graph', methods=['POST'])
  495. def create_text_graph():
  496. try:
  497. # 获取参数
  498. node_id = request.json.get('id')
  499. entity = request.json.get('entity')
  500. entity_en = request.json.get('entity_en')
  501. if not all([node_id, entity, entity_en]):
  502. return jsonify(failed("参数不完整"))
  503. # 创建图谱
  504. result = handle_txt_graph(node_id, entity, entity_en)
  505. if result:
  506. return jsonify(success({"message": "图谱创建成功"}))
  507. else:
  508. return jsonify(failed("图谱创建失败"))
  509. except Exception as e:
  510. logger.error(f"创建文本图谱失败: {str(e)}")
  511. return jsonify(failed(str(e)))
  512. @bp.route('/config', methods=['GET'])
  513. @require_auth
  514. def get_meta_config():
  515. """获取元数据配置信息"""
  516. config = get_minio_config()
  517. return jsonify({
  518. 'bucket_name': config['bucket_name'],
  519. 'prefix': config['prefix'],
  520. 'allowed_extensions': list(config['allowed_extensions'])
  521. })