routes.py 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030
  1. from flask import request, jsonify, 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 io
  6. from minio import Minio
  7. from minio.error import S3Error
  8. from app.services.neo4j_driver import neo4j_driver
  9. from app.core.meta_data import (
  10. get_formatted_time,
  11. meta_list,
  12. meta_kinship_graph,
  13. parse_text,
  14. handle_txt_graph,
  15. get_file_content,
  16. text_resource_solve,
  17. handle_id_unstructured,
  18. solve_unstructured_data
  19. )
  20. from app.core.system.auth import require_auth
  21. logger = logging.getLogger("app")
  22. def get_minio_client():
  23. """获取 MinIO 客户端实例"""
  24. return Minio(
  25. current_app.config['MINIO_HOST'],
  26. access_key=current_app.config['MINIO_USER'],
  27. secret_key=current_app.config['MINIO_PASSWORD'],
  28. secure=current_app.config['MINIO_SECURE']
  29. )
  30. def get_minio_config():
  31. """获取 MinIO 配置"""
  32. return {
  33. 'MINIO_BUCKET': current_app.config['MINIO_BUCKET'],
  34. 'PREFIX': current_app.config['PREFIX'],
  35. 'ALLOWED_EXTENSIONS': current_app.config['ALLOWED_EXTENSIONS']
  36. }
  37. def allowed_file(filename):
  38. """检查文件扩展名是否允许"""
  39. if '.' not in filename:
  40. return False
  41. ext = filename.rsplit('.', 1)[1].lower()
  42. return ext in get_minio_config()['ALLOWED_EXTENSIONS']
  43. # 元数据列表
  44. @bp.route('/node/list', methods=['POST'])
  45. def meta_node_list():
  46. try:
  47. payload = request.get_json() or {}
  48. if not isinstance(payload, dict):
  49. return jsonify(failed("请求数据格式错误,应为 JSON 对象"))
  50. def to_int(value, default):
  51. try:
  52. return int(value)
  53. except (TypeError, ValueError):
  54. return default
  55. # 分页参数
  56. page = to_int(payload.get('current', 1), 1)
  57. page_size = to_int(payload.get('size', 10), 10)
  58. # 过滤参数
  59. name_en_filter = payload.get('name_en') or None
  60. name_zh_filter = payload.get('name_zh') or None
  61. category_filter = payload.get('category') or None
  62. time_filter = payload.get('time') or None
  63. logger.info(
  64. f"[node/list] 过滤参数: name_zh={name_zh_filter}, "
  65. f"name_en={name_en_filter}, category={category_filter}"
  66. )
  67. tag_filter = payload.get('tag')
  68. if tag_filter is not None and not isinstance(tag_filter, list):
  69. tag_filter = None
  70. # 调用核心业务逻辑
  71. result, total_count = meta_list(
  72. page,
  73. page_size,
  74. '',
  75. name_en_filter,
  76. name_zh_filter,
  77. category_filter,
  78. time_filter,
  79. tag_filter
  80. )
  81. # 返回结果
  82. return jsonify(success({
  83. "records": result,
  84. "total": total_count,
  85. "size": page_size,
  86. "current": page
  87. }))
  88. except Exception as e:
  89. logger.error(f"获取元数据列表失败: {str(e)}")
  90. return jsonify(failed(str(e)))
  91. # 元数据图谱
  92. @bp.route('/node/graph', methods=['POST'])
  93. def meta_node_graph():
  94. try:
  95. if not request.json:
  96. return jsonify(failed("请求数据不能为空"))
  97. # 从请求中获取节点ID
  98. node_id = request.json.get('nodeId')
  99. if node_id is None:
  100. return jsonify(failed("nodeId 不能为空"))
  101. try:
  102. node_id_int = int(node_id)
  103. except (TypeError, ValueError):
  104. return jsonify(failed("nodeId 必须为整数"))
  105. # 调用核心业务逻辑
  106. graph = meta_kinship_graph(node_id_int)
  107. is_dict = isinstance(graph, dict)
  108. nodes = graph.get("nodes", []) if is_dict else []
  109. relationships = graph.get("relationships", []) if is_dict else []
  110. # 当前节点属性
  111. node_info = next(
  112. (n for n in nodes if n.get("id") == node_id_int),
  113. {},
  114. )
  115. # 关联节点(包含属性,便于前端展示名称等)
  116. related_nodes = [n for n in nodes if n.get("id") != node_id_int]
  117. payload = {
  118. "node": node_info,
  119. "related_nodes": related_nodes,
  120. "relationships": relationships,
  121. }
  122. return jsonify(success(payload))
  123. except Exception as e:
  124. logger.error(f"获取元数据图谱失败: {str(e)}")
  125. return jsonify(failed(str(e)))
  126. # 删除元数据
  127. @bp.route('/node/delete', methods=['POST'])
  128. def meta_node_delete():
  129. try:
  130. if not request.json:
  131. return jsonify(failed("请求数据不能为空"))
  132. # 从请求中获取节点ID
  133. node_id = request.json.get('id')
  134. # 删除节点逻辑
  135. with neo4j_driver.get_session() as session:
  136. cypher = "MATCH (n) WHERE id(n) = $node_id DETACH DELETE n"
  137. session.run(cypher, node_id=int(node_id))
  138. # 返回结果
  139. return jsonify(success({}))
  140. except Exception as e:
  141. logger.error(f"删除元数据失败: {str(e)}")
  142. return jsonify(failed(str(e)))
  143. # 编辑元数据
  144. @bp.route('/node/edit', methods=['POST'])
  145. def meta_node_edit():
  146. try:
  147. if not request.json:
  148. return jsonify(failed("请求数据不能为空"))
  149. # 从请求中获取节点ID
  150. node_id = request.json.get('id')
  151. if not node_id:
  152. return jsonify(failed("节点ID不能为空"))
  153. # 获取节点
  154. with neo4j_driver.get_session() as session:
  155. # 查询节点信息
  156. cypher = """
  157. MATCH (n:DataMeta)
  158. WHERE id(n) = $node_id
  159. RETURN n
  160. """
  161. result = session.run(cypher, node_id=int(node_id))
  162. node = result.single()
  163. if not node or not node["n"]:
  164. return jsonify(failed("节点不存在"))
  165. # 获取节点数据
  166. node_data = dict(node["n"])
  167. node_data["id"] = node["n"].id
  168. # 获取标签信息
  169. tag_cypher = """
  170. MATCH (n:DataMeta)-[:LABEL]->(t:DataLabel)
  171. WHERE id(n) = $node_id
  172. RETURN t
  173. """
  174. tag_result = session.run(tag_cypher, node_id=int(node_id))
  175. tags: list[dict] = []
  176. for record in tag_result:
  177. tag_node = record.get("t")
  178. if tag_node:
  179. tags.append({
  180. "id": tag_node.id,
  181. "name_zh": tag_node.get("name_zh", ""),
  182. "name_en": tag_node.get("name_en", ""),
  183. })
  184. # 获取主数据信息
  185. master_data_cypher = """
  186. MATCH (n:DataMeta)-[:master_data]->(m:master_data)
  187. WHERE id(n) = $node_id
  188. RETURN m
  189. """
  190. master_data_result = session.run(
  191. master_data_cypher, node_id=int(node_id)
  192. )
  193. master_data = master_data_result.single()
  194. # 构建返回数据
  195. response_data = [{
  196. "master_data": (
  197. master_data["m"].id
  198. if master_data and master_data["m"] else None
  199. ),
  200. "name_zh": node_data.get("name_zh", ""),
  201. "name_en": node_data.get("name_en", ""),
  202. "create_time": node_data.get("create_time", ""),
  203. "update_time": node_data.get("update_time", ""),
  204. "status": bool(node_data.get("status", True)),
  205. "data_type": node_data.get("data_type", ""),
  206. "tag": tags,
  207. "affiliation": node_data.get("affiliation"),
  208. "category": node_data.get("category"),
  209. "alias": node_data.get("alias"),
  210. "describe": node_data.get("describe")
  211. }]
  212. logger.info(f"成功获取元数据节点: ID={node_data['id']}")
  213. return jsonify(success(response_data))
  214. except Exception as e:
  215. logger.error(f"获取元数据节点失败: {str(e)}")
  216. return jsonify(failed(str(e)))
  217. # 增加元数据
  218. @bp.route('/check', methods=['GET'])
  219. def meta_check():
  220. """
  221. 检查元数据中文名是否已存在
  222. 请求参数:
  223. - name_zh: 元数据中文名(URL参数)
  224. 返回:
  225. - exists: true/false 表示是否存在
  226. """
  227. try:
  228. name_zh = request.args.get('name_zh')
  229. if not name_zh:
  230. return jsonify(failed("缺少name_zh参数"))
  231. # 查询数据库检查是否存在
  232. with neo4j_driver.get_session() as session:
  233. cypher = """
  234. MATCH (n:DataMeta {name_zh: $name_zh})
  235. RETURN count(n) > 0 as exists
  236. """
  237. result = session.run(cypher, name_zh=name_zh)
  238. record = result.single()
  239. if record:
  240. exists = record["exists"]
  241. logger.info(f"检查元数据 '{name_zh}': {'存在' if exists else '不存在'}")
  242. return jsonify(success({
  243. "exists": exists,
  244. "name_zh": name_zh
  245. }, "查询成功"))
  246. else:
  247. return jsonify(success({
  248. "exists": False,
  249. "name_zh": name_zh
  250. }, "查询成功"))
  251. except Exception as e:
  252. logger.error(f"检查元数据失败: {str(e)}")
  253. return jsonify(failed(f"检查失败: {str(e)}"))
  254. @bp.route('/node/add', methods=['POST'])
  255. def meta_node_add():
  256. try:
  257. if not request.json:
  258. return jsonify(failed("请求数据不能为空"))
  259. # 从请求中获取节点信息
  260. node_name_zh = request.json.get('name_zh')
  261. node_type = request.json.get('data_type')
  262. node_category = request.json.get('category')
  263. node_alias = request.json.get('alias')
  264. node_affiliation = request.json.get('affiliation')
  265. node_tag = request.json.get('tag')
  266. node_desc = request.json.get('describe')
  267. node_status = bool(request.json.get('status', True))
  268. node_name_en = request.json.get('name_en')
  269. if not node_name_zh:
  270. return jsonify(failed("节点名称不能为空"))
  271. if not node_type:
  272. return jsonify(failed("节点类型不能为空"))
  273. # 创建节点
  274. with neo4j_driver.get_session() as session:
  275. cypher = """
  276. MERGE (n:DataMeta {name_zh: $name_zh})
  277. ON CREATE SET n.name_en = $name_en,
  278. n.data_type = $data_type,
  279. n.category = $category,
  280. n.alias = $alias,
  281. n.affiliation = $affiliation,
  282. n.describe = $describe,
  283. n.create_time = $create_time,
  284. n.updateTime = $update_time,
  285. n.status = $status,
  286. n.name_en = $name_en
  287. ON MATCH SET n.data_type = $data_type,
  288. n.category = $category,
  289. n.alias = $alias,
  290. n.affiliation = $affiliation,
  291. n.describe = $describe,
  292. n.updateTime = $update_time,
  293. n.status = $status,
  294. n.name_en = $name_en
  295. RETURN n
  296. """
  297. create_time = update_time = get_formatted_time()
  298. result = session.run(
  299. cypher,
  300. name_zh=node_name_zh,
  301. data_type=node_type,
  302. category=node_category,
  303. alias=node_alias,
  304. affiliation=node_affiliation,
  305. describe=node_desc,
  306. create_time=create_time,
  307. update_time=update_time,
  308. status=node_status,
  309. name_en=node_name_en
  310. )
  311. node = result.single()
  312. if node and node["n"]:
  313. node_data = dict(node["n"])
  314. node_data["id"] = node["n"].id
  315. # 如果提供了标签列表,创建标签关系
  316. tag_nodes = []
  317. if node_tag:
  318. tag_items = (
  319. node_tag if isinstance(node_tag, list) else [node_tag]
  320. )
  321. for tag_item in tag_items:
  322. tag_id = (
  323. tag_item.get("id")
  324. if isinstance(tag_item, dict)
  325. else tag_item
  326. )
  327. if tag_id is None:
  328. logger.warning(f"标签ID无效: {tag_item}")
  329. continue
  330. try:
  331. tag_id = int(tag_id)
  332. except (TypeError, ValueError):
  333. logger.warning(f"标签ID无效: {tag_item}")
  334. continue
  335. # 获取标签节点信息
  336. tag_fetch = session.run(
  337. "MATCH (t:DataLabel) "
  338. "WHERE id(t) = $tag_id RETURN t",
  339. tag_id=tag_id,
  340. ).single()
  341. if not tag_fetch or not tag_fetch.get("t"):
  342. logger.warning(f"未找到标签节点: {tag_id}")
  343. continue
  344. tag_node = tag_fetch["t"]
  345. tag_nodes.append({
  346. "id": tag_node.id,
  347. "name_zh": tag_node.get("name_zh", ""),
  348. "name_en": tag_node.get("name_en", ""),
  349. })
  350. tag_cypher = """
  351. MATCH (n:DataMeta), (t:DataLabel)
  352. WHERE id(n) = $node_id AND id(t) = $tag_id
  353. MERGE (n)-[r:LABEL]->(t)
  354. RETURN r
  355. """
  356. session.run(
  357. tag_cypher,
  358. node_id=node["n"].id,
  359. tag_id=tag_id
  360. )
  361. node_data["tag"] = tag_nodes
  362. logger.info(
  363. f"成功创建或更新元数据节点: "
  364. f"ID={node_data['id']}, name={node_name_zh}"
  365. )
  366. return jsonify(success(node_data))
  367. else:
  368. logger.error(f"创建元数据节点失败: {node_name_zh}")
  369. return jsonify(failed("创建元数据节点失败"))
  370. except Exception as e:
  371. logger.error(f"添加元数据失败: {str(e)}")
  372. return jsonify(failed(str(e)))
  373. # 搜索元数据
  374. @bp.route('/search', methods=['GET'])
  375. def search_metadata_route():
  376. try:
  377. keyword = request.args.get('keyword', '')
  378. if not keyword:
  379. return jsonify(success([]))
  380. cypher = """
  381. MATCH (n:DataMeta)
  382. WHERE n.name_zh CONTAINS $keyword
  383. RETURN n LIMIT 100
  384. """
  385. with neo4j_driver.get_session() as session:
  386. result = session.run(cypher, keyword=keyword)
  387. metadata_list = [dict(record["n"]) for record in result]
  388. return jsonify(success(metadata_list))
  389. except Exception as e:
  390. logger.error(f"搜索元数据失败: {str(e)}")
  391. return jsonify(failed(str(e)))
  392. # 全文检索查询
  393. @bp.route('/full/text/query', methods=['POST'])
  394. def full_text_query():
  395. try:
  396. if not request.json:
  397. return jsonify(failed("请求数据不能为空"))
  398. # 获取查询条件
  399. search_term = request.json.get('query', '')
  400. if not search_term:
  401. return jsonify(failed("查询条件不能为空"))
  402. # 执行Neo4j全文索引查询
  403. with neo4j_driver.get_session() as session:
  404. cypher = """
  405. CALL db.index.fulltext.queryNodes("DataMetaFulltext", $term)
  406. YIELD node, score
  407. RETURN node, score
  408. ORDER BY score DESC
  409. LIMIT 20
  410. """
  411. result = session.run(cypher, term=search_term)
  412. # 处理查询结果
  413. search_results = []
  414. for record in result:
  415. node_data = dict(record["node"])
  416. node_data["id"] = record["node"].id
  417. node_data["score"] = record["score"]
  418. search_results.append(node_data)
  419. return jsonify(success(search_results))
  420. except Exception as e:
  421. logger.error(f"全文检索查询失败: {str(e)}")
  422. return jsonify(failed(str(e)))
  423. # 非结构化文本查询
  424. @bp.route('/unstructure/text/query', methods=['POST'])
  425. def unstructure_text_query():
  426. try:
  427. if not request.json:
  428. return jsonify(failed("请求数据不能为空"))
  429. # 获取查询参数
  430. node_id = request.json.get('id')
  431. if not node_id:
  432. return jsonify(failed("节点ID不能为空"))
  433. # 获取节点信息
  434. node_data = handle_id_unstructured(node_id)
  435. if not node_data:
  436. return jsonify(failed("节点不存在"))
  437. # 获取对象路径
  438. object_name = node_data.get('url')
  439. if not object_name:
  440. return jsonify(failed("文档路径不存在"))
  441. # 获取 MinIO 配置
  442. minio_client = get_minio_client()
  443. config = get_minio_config()
  444. bucket_name = config['MINIO_BUCKET']
  445. # 从MinIO获取文件内容
  446. file_content = get_file_content(minio_client, bucket_name, object_name)
  447. # 解析文本内容
  448. parsed_data = parse_text(file_content)
  449. # 返回结果
  450. result = {
  451. "node": node_data,
  452. "parsed": parsed_data,
  453. "content": (
  454. file_content[:1000] + "..."
  455. if len(file_content) > 1000 else file_content
  456. )
  457. }
  458. return jsonify(success(result))
  459. except Exception as e:
  460. logger.error(f"非结构化文本查询失败: {str(e)}")
  461. return jsonify(failed(str(e)))
  462. # 文件上传
  463. @bp.route('/resource/upload', methods=['POST'])
  464. def upload_file():
  465. try:
  466. # 检查请求中是否有文件
  467. if 'file' not in request.files:
  468. return jsonify(failed("没有找到上传的文件"))
  469. file = request.files['file']
  470. # 检查文件名
  471. if not file.filename:
  472. return jsonify(failed("未选择文件"))
  473. # 保存文件名到本地变量(确保类型安全)
  474. filename = file.filename
  475. # 检查文件类型
  476. if not allowed_file(filename):
  477. return jsonify(failed("不支持的文件类型"))
  478. # 获取 MinIO 配置
  479. minio_client = get_minio_client()
  480. config = get_minio_config()
  481. # 上传到MinIO
  482. file_content = file.read()
  483. file_size = len(file_content)
  484. file_type = filename.rsplit('.', 1)[1].lower()
  485. # 提取文件名(不包含扩展名)
  486. filename_without_ext = filename.rsplit('.', 1)[0]
  487. # 生成紧凑的时间戳 (yyyyMMddHHmmss)
  488. import time
  489. timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime())
  490. # 生成唯一文件名
  491. object_name = (
  492. f"{config['PREFIX']}/"
  493. f"{filename_without_ext}_{timestamp}.{file_type}"
  494. )
  495. # 上传文件
  496. minio_client.put_object(
  497. config['MINIO_BUCKET'],
  498. object_name,
  499. io.BytesIO(file_content),
  500. file_size,
  501. content_type=f"application/{file_type}"
  502. )
  503. # 返回结果
  504. return jsonify(success({
  505. "filename": file.filename,
  506. "size": file_size,
  507. "type": file_type,
  508. "url": object_name
  509. }))
  510. except Exception as e:
  511. logger.error(f"文件上传失败: {str(e)}")
  512. return jsonify(failed(str(e)))
  513. # 文件下载显示
  514. @bp.route('/resource/display', methods=['POST'])
  515. def upload_file_display():
  516. response = None
  517. try:
  518. if not request.json:
  519. return jsonify(failed("请求数据不能为空"))
  520. object_name = request.json.get('url')
  521. if not object_name:
  522. return jsonify(failed("文件路径不能为空"))
  523. # 获取 MinIO 配置
  524. minio_client = get_minio_client()
  525. config = get_minio_config()
  526. # 获取文件内容
  527. response = minio_client.get_object(config['MINIO_BUCKET'], object_name)
  528. file_data = response.read()
  529. # 获取文件名
  530. file_name = object_name.split('/')[-1]
  531. # 确定文件类型
  532. file_extension = file_name.split('.')[-1].lower()
  533. # 为不同文件类型设置合适的MIME类型
  534. mime_types = {
  535. 'pdf': 'application/pdf',
  536. 'doc': 'application/msword',
  537. 'docx': (
  538. 'application/vnd.openxmlformats-'
  539. 'officedocument.wordprocessingml.document'
  540. ),
  541. 'xls': 'application/vnd.ms-excel',
  542. 'xlsx': (
  543. 'application/vnd.openxmlformats-'
  544. 'officedocument.spreadsheetml.sheet'
  545. ),
  546. 'txt': 'text/plain',
  547. 'csv': 'text/csv'
  548. }
  549. content_type = mime_types.get(
  550. file_extension, 'application/octet-stream'
  551. )
  552. # 返回结果
  553. return jsonify(success({
  554. "filename": file_name,
  555. "type": file_extension,
  556. "contentType": content_type,
  557. "size": len(file_data),
  558. "url": f"/api/meta/resource/download?url={object_name}"
  559. }))
  560. except S3Error as e:
  561. logger.error(f"MinIO操作失败: {str(e)}")
  562. return jsonify(failed(f"文件访问失败: {str(e)}"))
  563. except Exception as e:
  564. logger.error(f"文件显示信息获取失败: {str(e)}")
  565. return jsonify(failed(str(e)))
  566. finally:
  567. if response:
  568. response.close()
  569. response.release_conn()
  570. # 文件下载接口
  571. @bp.route('/resource/download', methods=['GET'])
  572. def download_file():
  573. response = None
  574. try:
  575. object_name = request.args.get('url')
  576. if not object_name:
  577. return jsonify(failed("文件路径不能为空"))
  578. # URL解码,处理特殊字符
  579. import urllib.parse
  580. object_name = urllib.parse.unquote(object_name)
  581. # 记录下载请求信息,便于调试
  582. logger.info(f"下载文件请求: {object_name}")
  583. # 获取 MinIO 配置
  584. minio_client = get_minio_client()
  585. config = get_minio_config()
  586. # 获取文件
  587. try:
  588. response = minio_client.get_object(
  589. config['MINIO_BUCKET'], object_name
  590. )
  591. file_data = response.read()
  592. except S3Error as e:
  593. logger.error(f"MinIO获取文件失败: {str(e)}")
  594. return jsonify(failed(f"文件获取失败: {str(e)}"))
  595. # 获取文件名,并处理特殊字符
  596. file_name = object_name.split('/')[-1]
  597. # 直接从内存返回文件,不创建临时文件
  598. file_stream = io.BytesIO(file_data)
  599. # 返回文件
  600. return send_file(
  601. file_stream,
  602. as_attachment=True,
  603. download_name=file_name,
  604. mimetype="application/octet-stream"
  605. )
  606. except Exception as e:
  607. logger.error(f"文件下载失败: {str(e)}")
  608. return jsonify(failed(str(e)))
  609. finally:
  610. if response:
  611. response.close()
  612. response.release_conn()
  613. # 文本资源翻译
  614. @bp.route('/resource/translate', methods=['POST'])
  615. def text_resource_translate():
  616. try:
  617. if not request.json:
  618. return jsonify(failed("请求数据不能为空"))
  619. # 获取参数
  620. name_zh = request.json.get('name_zh', '')
  621. keyword = request.json.get('keyword', '')
  622. if not name_zh:
  623. return jsonify(failed("名称不能为空"))
  624. # 调用资源处理逻辑
  625. result = text_resource_solve(None, name_zh, keyword)
  626. return jsonify(success(result))
  627. except Exception as e:
  628. logger.error(f"文本资源翻译失败: {str(e)}")
  629. return jsonify(failed(str(e)))
  630. # 创建文本资源节点
  631. @bp.route('/resource/node', methods=['POST'])
  632. def text_resource_node():
  633. try:
  634. if not request.json:
  635. return jsonify(failed("请求数据不能为空"))
  636. # 获取参数
  637. name_zh = request.json.get('name_zh', '')
  638. name_en = request.json.get('name_en', '')
  639. keywords = request.json.get('keywords', [])
  640. keywords_en = request.json.get('keywords_en', [])
  641. object_name = request.json.get('url', '')
  642. if not name_zh or not name_en or not object_name:
  643. return jsonify(failed("参数不完整"))
  644. # 创建节点
  645. with neo4j_driver.get_session() as session:
  646. # 创建资源节点
  647. cypher = """
  648. CREATE (n:DataMeta {
  649. name_zh: $name_zh,
  650. name_en: $name_en,
  651. keywords: $keywords,
  652. keywords_en: $keywords_en,
  653. url: $object_name,
  654. create_time: $create_time,
  655. updateTime: $update_time
  656. })
  657. RETURN n
  658. """
  659. create_time = update_time = get_formatted_time()
  660. result = session.run(
  661. cypher,
  662. name_zh=name_zh,
  663. name_en=name_en,
  664. keywords=keywords,
  665. keywords_en=keywords_en,
  666. object_name=object_name,
  667. create_time=create_time,
  668. update_time=update_time
  669. )
  670. record = result.single()
  671. if not record:
  672. return jsonify(failed("创建节点失败"))
  673. node = record["n"]
  674. # 为每个关键词创建标签节点并关联
  675. for i, keyword in enumerate(keywords):
  676. if keyword:
  677. # 创建标签节点
  678. tag_cypher = """
  679. MERGE (t:Tag {name_zh: $name_zh})
  680. ON CREATE SET t.name_en = $name_en,
  681. t.create_time = $create_time
  682. RETURN t
  683. """
  684. tag_result = session.run(
  685. tag_cypher,
  686. name_zh=keyword,
  687. name_en=keywords_en[i] if i < len(keywords_en) else "",
  688. create_time=create_time
  689. )
  690. tag_record = tag_result.single()
  691. if not tag_record:
  692. continue
  693. tag_node = tag_record["t"]
  694. # 创建关系
  695. rel_cypher = """
  696. MATCH (n), (t)
  697. WHERE id(n) = $node_id AND id(t) = $tag_id
  698. CREATE (n)-[r:HAS_TAG]->(t)
  699. RETURN r
  700. """
  701. session.run(
  702. rel_cypher,
  703. node_id=node.id,
  704. tag_id=tag_node.id
  705. )
  706. # 返回创建的节点
  707. return jsonify(success(dict(node)))
  708. except Exception as e:
  709. logger.error(f"创建文本资源节点失败: {str(e)}")
  710. return jsonify(failed(str(e)))
  711. # 处理非结构化数据
  712. @bp.route('/unstructured/process', methods=['POST'])
  713. def processing_unstructured_data():
  714. try:
  715. if not request.json:
  716. return jsonify(failed("请求数据不能为空"))
  717. # 获取参数
  718. node_id = request.json.get('id')
  719. if not node_id:
  720. return jsonify(failed("节点ID不能为空"))
  721. # 获取 MinIO 配置
  722. minio_client = get_minio_client()
  723. config = get_minio_config()
  724. prefix = config['PREFIX']
  725. # 调用处理逻辑
  726. result = solve_unstructured_data(node_id, minio_client, prefix)
  727. if result:
  728. return jsonify(success({"message": "处理成功"}))
  729. else:
  730. return jsonify(failed("处理失败"))
  731. except Exception as e:
  732. logger.error(f"处理非结构化数据失败: {str(e)}")
  733. return jsonify(failed(str(e)))
  734. # 创建文本图谱
  735. @bp.route('/text/graph', methods=['POST'])
  736. def create_text_graph():
  737. try:
  738. if not request.json:
  739. return jsonify(failed("请求数据不能为空"))
  740. # 获取参数
  741. node_id = request.json.get('id')
  742. entity_zh = request.json.get('entity_zh')
  743. entity_en = request.json.get('entity_en')
  744. if not all([node_id, entity_zh, entity_en]):
  745. return jsonify(failed("参数不完整"))
  746. # 创建图谱
  747. result = handle_txt_graph(node_id, entity_zh, entity_en)
  748. if result:
  749. return jsonify(success({"message": "图谱创建成功"}))
  750. else:
  751. return jsonify(failed("图谱创建失败"))
  752. except Exception as e:
  753. logger.error(f"创建文本图谱失败: {str(e)}")
  754. return jsonify(failed(str(e)))
  755. @bp.route('/config', methods=['GET'])
  756. @require_auth
  757. def get_meta_config():
  758. """获取元数据配置信息"""
  759. config = get_minio_config()
  760. return jsonify({
  761. 'bucket_name': config['MINIO_BUCKET'],
  762. 'prefix': config['PREFIX'],
  763. 'allowed_extensions': list(config['ALLOWED_EXTENSIONS'])
  764. })
  765. # 更新元数据
  766. @bp.route('/node/update', methods=['POST'])
  767. def meta_node_update():
  768. try:
  769. if not request.json:
  770. return jsonify(failed("请求数据不能为空"))
  771. # 从请求中获取节点ID和更新数据
  772. node_id = request.json.get('id')
  773. if not node_id:
  774. return jsonify(failed("节点ID不能为空"))
  775. # 验证并转换节点ID为整数
  776. try:
  777. node_id = int(node_id)
  778. except (ValueError, TypeError):
  779. return jsonify(failed(f"节点ID必须为整数,当前值: {node_id}"))
  780. # 更新节点
  781. with neo4j_driver.get_session() as session:
  782. # 检查节点是否存在并获取当前值
  783. check_cypher = """
  784. MATCH (n:DataMeta)
  785. WHERE id(n) = $node_id
  786. RETURN n
  787. """
  788. result = session.run(check_cypher, node_id=node_id)
  789. node = result.single()
  790. if not node or not node["n"]:
  791. return jsonify(failed("节点不存在"))
  792. # 构建更新语句,只更新提供的属性
  793. update_cypher = """
  794. MATCH (n:DataMeta)
  795. WHERE id(n) = $node_id
  796. SET n.updateTime = $update_time
  797. """
  798. # 准备更新参数
  799. update_params = {
  800. 'node_id': node_id,
  801. 'update_time': get_formatted_time()
  802. }
  803. # 处理每个可能的更新字段
  804. fields_to_update = {
  805. 'name_zh': request.json.get('name_zh'),
  806. 'category': request.json.get('category'),
  807. 'alias': request.json.get('alias'),
  808. 'affiliation': request.json.get('affiliation'),
  809. 'data_type': request.json.get('data_type'),
  810. 'describe': request.json.get('describe'),
  811. 'status': request.json.get('status'),
  812. 'name_en': request.json.get('name_en')
  813. }
  814. # 只更新提供了新值的字段
  815. for field, new_value in fields_to_update.items():
  816. if new_value is not None:
  817. # 特殊处理 data_type 字段映射
  818. if field == 'data_type':
  819. update_cypher += f", n.data_type = ${field}\n"
  820. else:
  821. update_cypher += f", n.{field} = ${field}\n"
  822. update_params[field] = new_value
  823. update_cypher += "RETURN n"
  824. result = session.run(
  825. update_cypher, # type: ignore[arg-type]
  826. **update_params
  827. )
  828. updated_node = result.single()
  829. if updated_node and updated_node["n"]:
  830. node_data = dict(updated_node["n"])
  831. node_data["id"] = updated_node["n"].id
  832. # 如果更新了标签,处理标签关系(支持列表)
  833. tag = request.json.get('tag')
  834. if tag is not None:
  835. # 先删除现有标签关系
  836. delete_tag_cypher = """
  837. MATCH (n:DataMeta)-[r:LABEL]->(t:DataLabel)
  838. WHERE id(n) = $node_id
  839. DELETE r
  840. """
  841. session.run(delete_tag_cypher, node_id=node_id)
  842. tag_items = tag if isinstance(tag, list) else [tag]
  843. for tag_item in tag_items:
  844. tag_id = (
  845. tag_item.get('id')
  846. if isinstance(tag_item, dict)
  847. else tag_item
  848. )
  849. if tag_id is None:
  850. logger.warning(f"标签ID无效: {tag_item}")
  851. continue
  852. try:
  853. tag_id = int(tag_id)
  854. except (ValueError, TypeError):
  855. logger.warning(f"标签ID无效: {tag_item}")
  856. continue
  857. create_tag_cypher = """
  858. MATCH (n:DataMeta), (t:DataLabel)
  859. WHERE id(n) = $node_id AND id(t) = $tag_id
  860. MERGE (n)-[r:LABEL]->(t)
  861. RETURN r
  862. """
  863. session.run(
  864. create_tag_cypher,
  865. node_id=node_id,
  866. tag_id=tag_id
  867. )
  868. logger.info(f"成功更新元数据节点: ID={node_data['id']}")
  869. return jsonify(success(node_data))
  870. else:
  871. logger.error(f"更新元数据节点失败: ID={node_id}")
  872. return jsonify(failed("更新元数据节点失败"))
  873. except Exception as e:
  874. logger.error(f"更新元数据失败: {str(e)}")
  875. return jsonify(failed(str(e)))