routes.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810
  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.core.graph.graph_operations 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. 'MINIO_BUCKET': current_app.config['MINIO_BUCKET'],
  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. name_en_filter = request.json.get('name_en', None)
  56. name_zh_filter = request.json.get('name_zh', 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. name_en_filter,
  66. name_zh_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. # 从请求中获取节点ID
  114. node_id = request.json.get('id')
  115. if not node_id:
  116. return jsonify(failed("节点ID不能为空"))
  117. # 获取节点
  118. with neo4j_driver.get_session() as session:
  119. # 查询节点信息
  120. cypher = """
  121. MATCH (n:DataMeta)
  122. WHERE id(n) = $node_id
  123. RETURN n
  124. """
  125. result = session.run(cypher, node_id=int(node_id))
  126. node = result.single()
  127. if not node or not node["n"]:
  128. return jsonify(failed("节点不存在"))
  129. # 获取节点数据
  130. node_data = dict(node["n"])
  131. node_data["id"] = node["n"].id
  132. # 获取标签信息
  133. tag_cypher = """
  134. MATCH (n:DataMeta)-[:LABEL]->(t:DataLabel)
  135. WHERE id(n) = $node_id
  136. RETURN t
  137. """
  138. tag_result = session.run(tag_cypher, node_id=int(node_id))
  139. tag = tag_result.single()
  140. # 获取主数据信息
  141. master_data_cypher = """
  142. MATCH (n:DataMeta)-[:master_data]->(m:master_data)
  143. WHERE id(n) = $node_id
  144. RETURN m
  145. """
  146. master_data_result = session.run(master_data_cypher, node_id=int(node_id))
  147. master_data = master_data_result.single()
  148. # 构建返回数据
  149. response_data = [{
  150. "master_data": master_data["m"].id if master_data and master_data["m"] else None,
  151. "name_zh": node_data.get("name_zh", ""),
  152. "name_en": node_data.get("name_en", ""),
  153. "time": node_data.get("updateTime", ""),
  154. "status": bool(node_data.get("status", True)),
  155. "data_type": node_data.get("data_type", ""),
  156. "tag": {
  157. "name": tag["t"].get("name", "") if tag and tag["t"] else None,
  158. "id": tag["t"].id if tag and tag["t"] else None
  159. },
  160. "affiliation": node_data.get("affiliation"),
  161. "category": node_data.get("category"),
  162. "alias": node_data.get("alias"),
  163. "describe": node_data.get("describe")
  164. }]
  165. logger.info(f"成功获取元数据节点: ID={node_data['id']}")
  166. return jsonify(success(response_data))
  167. except Exception as e:
  168. logger.error(f"获取元数据节点失败: {str(e)}")
  169. return jsonify(failed(str(e)))
  170. # 增加元数据
  171. @bp.route('/node/add', methods=['POST'])
  172. def meta_node_add():
  173. try:
  174. # 从请求中获取节点信息
  175. node_name_zh = request.json.get('name_zh')
  176. node_type = request.json.get('data_type')
  177. node_category = request.json.get('category')
  178. node_alias = request.json.get('alias')
  179. node_affiliation = request.json.get('affiliation')
  180. node_tag = request.json.get('tag')
  181. node_desc = request.json.get('describe')
  182. node_status = bool(request.json.get('status', True))
  183. node_name_en = request.json.get('name_en')
  184. if not node_name_zh:
  185. return jsonify(failed("节点名称不能为空"))
  186. if not node_type:
  187. return jsonify(failed("节点类型不能为空"))
  188. # 创建节点
  189. with neo4j_driver.get_session() as session:
  190. cypher = """
  191. MERGE (n:DataMeta {name_zh: $name_zh})
  192. ON CREATE SET n.name_en = $name_en,
  193. n.data_type = $data_type,
  194. n.category = $category,
  195. n.alias = $alias,
  196. n.affiliation = $affiliation,
  197. n.describe = $describe,
  198. n.create_time = $create_time,
  199. n.updateTime = $update_time,
  200. n.status = $status,
  201. n.name_en = $name_en
  202. ON MATCH SET n.data_type = $data_type,
  203. n.category = $category,
  204. n.alias = $alias,
  205. n.affiliation = $affiliation,
  206. n.describe = $describe,
  207. n.updateTime = $update_time,
  208. n.status = $status,
  209. n.name_en = $name_en
  210. RETURN n
  211. """
  212. create_time = update_time = get_formatted_time()
  213. result = session.run(
  214. cypher,
  215. name_zh=node_name_zh,
  216. data_type=node_type,
  217. category=node_category,
  218. alias=node_alias,
  219. affiliation=node_affiliation,
  220. describe=node_desc,
  221. create_time=create_time,
  222. update_time=update_time,
  223. status=node_status,
  224. name_en=node_name_en
  225. )
  226. node = result.single()
  227. if node and node["n"]:
  228. node_data = dict(node["n"])
  229. node_data["id"] = node["n"].id
  230. # 如果提供了标签ID,创建标签关系
  231. if node_tag:
  232. tag_cypher = """
  233. MATCH (n:DataMeta), (t:DataLabel)
  234. WHERE id(n) = $node_id AND id(t) = $tag_id
  235. MERGE (n)-[r:LABEL]->(t)
  236. RETURN r
  237. """
  238. session.run(tag_cypher, node_id=node["n"].id, tag_id=int(node_tag))
  239. logger.info(f"成功创建或更新元数据节点: ID={node_data['id']}, name={node_name_zh}")
  240. return jsonify(success(node_data))
  241. else:
  242. logger.error(f"创建元数据节点失败: {node_name_zh}")
  243. return jsonify(failed("创建元数据节点失败"))
  244. except Exception as e:
  245. logger.error(f"添加元数据失败: {str(e)}")
  246. return jsonify(failed(str(e)))
  247. # 搜索元数据
  248. @bp.route('/search', methods=['GET'])
  249. def search_metadata_route():
  250. try:
  251. keyword = request.args.get('keyword', '')
  252. if not keyword:
  253. return jsonify(success([]))
  254. cypher = """
  255. MATCH (n:DataMeta)
  256. WHERE n.name_zh CONTAINS $keyword
  257. RETURN n LIMIT 100
  258. """
  259. with neo4j_driver.get_session() as session:
  260. result = session.run(cypher, keyword=keyword)
  261. metadata_list = [dict(record["n"]) for record in result]
  262. return jsonify(success(metadata_list))
  263. except Exception as e:
  264. logger.error(f"搜索元数据失败: {str(e)}")
  265. return jsonify(failed(str(e)))
  266. # 全文检索查询
  267. @bp.route('/full/text/query', methods=['POST'])
  268. def full_text_query():
  269. try:
  270. # 获取查询条件
  271. query = request.json.get('query', '')
  272. if not query:
  273. return jsonify(failed("查询条件不能为空"))
  274. # 执行Neo4j全文索引查询
  275. with neo4j_driver.get_session() as session:
  276. cypher = """
  277. CALL db.index.fulltext.queryNodes("DataMetaFulltext", $query)
  278. YIELD node, score
  279. RETURN node, score
  280. ORDER BY score DESC
  281. LIMIT 20
  282. """
  283. result = session.run(cypher, query=query)
  284. # 处理查询结果
  285. search_results = []
  286. for record in result:
  287. node_data = dict(record["node"])
  288. node_data["id"] = record["node"].id
  289. node_data["score"] = record["score"]
  290. search_results.append(node_data)
  291. return jsonify(success(search_results))
  292. except Exception as e:
  293. logger.error(f"全文检索查询失败: {str(e)}")
  294. return jsonify(failed(str(e)))
  295. # 非结构化文本查询
  296. @bp.route('/unstructure/text/query', methods=['POST'])
  297. def unstructure_text_query():
  298. try:
  299. # 获取查询参数
  300. node_id = request.json.get('id')
  301. if not node_id:
  302. return jsonify(failed("节点ID不能为空"))
  303. # 获取节点信息
  304. node_data = handle_id_unstructured(node_id)
  305. if not node_data:
  306. return jsonify(failed("节点不存在"))
  307. # 获取对象路径
  308. object_name = node_data.get('url')
  309. if not object_name:
  310. return jsonify(failed("文档路径不存在"))
  311. # 获取 MinIO 配置
  312. minio_client = get_minio_client()
  313. config = get_minio_config()
  314. bucket_name = config['MINIO_BUCKET']
  315. # 从MinIO获取文件内容
  316. file_content = get_file_content(minio_client, bucket_name, object_name)
  317. # 解析文本内容
  318. parsed_data = parse_text(file_content)
  319. # 返回结果
  320. result = {
  321. "node": node_data,
  322. "parsed": parsed_data,
  323. "content": file_content[:1000] + "..." if len(file_content) > 1000 else file_content
  324. }
  325. return jsonify(success(result))
  326. except Exception as e:
  327. logger.error(f"非结构化文本查询失败: {str(e)}")
  328. return jsonify(failed(str(e)))
  329. # 文件上传
  330. @bp.route('/resource/upload', methods=['POST'])
  331. def upload_file():
  332. try:
  333. # 检查请求中是否有文件
  334. if 'file' not in request.files:
  335. return jsonify(failed("没有找到上传的文件"))
  336. file = request.files['file']
  337. # 检查文件名
  338. if file.filename == '':
  339. return jsonify(failed("未选择文件"))
  340. # 检查文件类型
  341. if not allowed_file(file.filename):
  342. return jsonify(failed("不支持的文件类型"))
  343. # 获取 MinIO 配置
  344. minio_client = get_minio_client()
  345. config = get_minio_config()
  346. # 上传到MinIO
  347. file_content = file.read()
  348. file_size = len(file_content)
  349. file_type = file.filename.rsplit('.', 1)[1].lower()
  350. # 提取文件名(不包含扩展名)
  351. filename_without_ext = file.filename.rsplit('.', 1)[0]
  352. # 生成紧凑的时间戳 (yyyyMMddHHmmss)
  353. import time
  354. timestamp = time.strftime("%Y%m%d%H%M%S", time.localtime())
  355. # 生成唯一文件名
  356. object_name = f"{config['PREFIX']}/{filename_without_ext}_{timestamp}.{file_type}"
  357. # 上传文件
  358. minio_client.put_object(
  359. config['MINIO_BUCKET'],
  360. object_name,
  361. io.BytesIO(file_content),
  362. file_size,
  363. content_type=f"application/{file_type}"
  364. )
  365. # 返回结果
  366. return jsonify(success({
  367. "filename": file.filename,
  368. "size": file_size,
  369. "type": file_type,
  370. "url": object_name
  371. }))
  372. except Exception as e:
  373. logger.error(f"文件上传失败: {str(e)}")
  374. return jsonify(failed(str(e)))
  375. # 文件下载显示
  376. @bp.route('/resource/display', methods=['POST'])
  377. def upload_file_display():
  378. response = None
  379. try:
  380. object_name = request.json.get('url')
  381. if not object_name:
  382. return jsonify(failed("文件路径不能为空"))
  383. # 获取 MinIO 配置
  384. minio_client = get_minio_client()
  385. config = get_minio_config()
  386. # 获取文件内容
  387. response = minio_client.get_object(config['MINIO_BUCKET'], object_name)
  388. file_data = response.read()
  389. # 获取文件名
  390. file_name = object_name.split('/')[-1]
  391. # 确定文件类型
  392. file_extension = file_name.split('.')[-1].lower()
  393. # 为不同文件类型设置合适的MIME类型
  394. mime_types = {
  395. 'pdf': 'application/pdf',
  396. 'doc': 'application/msword',
  397. 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  398. 'xls': 'application/vnd.ms-excel',
  399. 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
  400. 'txt': 'text/plain',
  401. 'csv': 'text/csv'
  402. }
  403. content_type = mime_types.get(file_extension, 'application/octet-stream')
  404. # 返回结果
  405. return jsonify(success({
  406. "filename": file_name,
  407. "type": file_extension,
  408. "contentType": content_type,
  409. "size": len(file_data),
  410. "url": f"/api/meta/resource/download?url={object_name}"
  411. }))
  412. except S3Error as e:
  413. logger.error(f"MinIO操作失败: {str(e)}")
  414. return jsonify(failed(f"文件访问失败: {str(e)}"))
  415. except Exception as e:
  416. logger.error(f"文件显示信息获取失败: {str(e)}")
  417. return jsonify(failed(str(e)))
  418. finally:
  419. if response:
  420. response.close()
  421. response.release_conn()
  422. # 文件下载接口
  423. @bp.route('/resource/download', methods=['GET'])
  424. def download_file():
  425. response = None
  426. try:
  427. object_name = request.args.get('url')
  428. if not object_name:
  429. return jsonify(failed("文件路径不能为空"))
  430. # URL解码,处理特殊字符
  431. import urllib.parse
  432. object_name = urllib.parse.unquote(object_name)
  433. # 记录下载请求信息,便于调试
  434. logger.info(f"下载文件请求: {object_name}")
  435. # 获取 MinIO 配置
  436. minio_client = get_minio_client()
  437. config = get_minio_config()
  438. # 获取文件
  439. try:
  440. response = minio_client.get_object(config['MINIO_BUCKET'], object_name)
  441. file_data = response.read()
  442. except S3Error as e:
  443. logger.error(f"MinIO获取文件失败: {str(e)}")
  444. return jsonify(failed(f"文件获取失败: {str(e)}"))
  445. # 获取文件名,并处理特殊字符
  446. file_name = object_name.split('/')[-1]
  447. # 直接从内存返回文件,不创建临时文件
  448. file_stream = io.BytesIO(file_data)
  449. # 返回文件
  450. return send_file(
  451. file_stream,
  452. as_attachment=True,
  453. download_name=file_name,
  454. mimetype="application/octet-stream"
  455. )
  456. except Exception as e:
  457. logger.error(f"文件下载失败: {str(e)}")
  458. return jsonify(failed(str(e)))
  459. finally:
  460. if response:
  461. response.close()
  462. response.release_conn()
  463. # 文本资源翻译
  464. @bp.route('/resource/translate', methods=['POST'])
  465. def text_resource_translate():
  466. try:
  467. # 获取参数
  468. name_zh = request.json.get('name_zh', '')
  469. keyword = request.json.get('keyword', '')
  470. if not name_zh:
  471. return jsonify(failed("名称不能为空"))
  472. # 调用资源处理逻辑
  473. result = text_resource_solve(None, name_zh, keyword)
  474. return jsonify(success(result))
  475. except Exception as e:
  476. logger.error(f"文本资源翻译失败: {str(e)}")
  477. return jsonify(failed(str(e)))
  478. # 创建文本资源节点
  479. @bp.route('/resource/node', methods=['POST'])
  480. def text_resource_node():
  481. try:
  482. # 获取参数
  483. name_zh = request.json.get('name_zh', '')
  484. name_en = request.json.get('name_en', '')
  485. keywords = request.json.get('keywords', [])
  486. keywords_en = request.json.get('keywords_en', [])
  487. object_name = request.json.get('url', '')
  488. if not name_zh or not name_en or not object_name:
  489. return jsonify(failed("参数不完整"))
  490. # 创建节点
  491. with neo4j_driver.get_session() as session:
  492. # 创建资源节点
  493. cypher = """
  494. CREATE (n:DataMeta {
  495. name_zh: $name_zh,
  496. name_en: $name_en,
  497. keywords: $keywords,
  498. keywords_en: $keywords_en,
  499. url: $object_name,
  500. create_time: $create_time,
  501. updateTime: $update_time
  502. })
  503. RETURN n
  504. """
  505. create_time = update_time = get_formatted_time()
  506. result = session.run(
  507. cypher,
  508. name_zh=name_zh,
  509. name_en=name_en,
  510. keywords=keywords,
  511. keywords_en=keywords_en,
  512. object_name=object_name,
  513. create_time=create_time,
  514. update_time=update_time
  515. )
  516. node = result.single()["n"]
  517. # 为每个关键词创建标签节点并关联
  518. for i, keyword in enumerate(keywords):
  519. if keyword:
  520. # 创建标签节点
  521. tag_cypher = """
  522. MERGE (t:Tag {name_zh: $name_zh})
  523. ON CREATE SET t.name_en = $name_en, t.create_time = $create_time
  524. RETURN t
  525. """
  526. tag_result = session.run(
  527. tag_cypher,
  528. name_zh=keyword,
  529. name_en=keywords_en[i] if i < len(keywords_en) else "",
  530. create_time=create_time
  531. )
  532. tag_node = tag_result.single()["t"]
  533. # 创建关系
  534. rel_cypher = """
  535. MATCH (n), (t)
  536. WHERE id(n) = $node_id AND id(t) = $tag_id
  537. CREATE (n)-[r:HAS_TAG]->(t)
  538. RETURN r
  539. """
  540. session.run(
  541. rel_cypher,
  542. node_id=node.id,
  543. tag_id=tag_node.id
  544. )
  545. # 返回创建的节点
  546. return jsonify(success(dict(node)))
  547. except Exception as e:
  548. logger.error(f"创建文本资源节点失败: {str(e)}")
  549. return jsonify(failed(str(e)))
  550. # 处理非结构化数据
  551. @bp.route('/unstructured/process', methods=['POST'])
  552. def processing_unstructured_data():
  553. try:
  554. # 获取参数
  555. node_id = request.json.get('id')
  556. if not node_id:
  557. return jsonify(failed("节点ID不能为空"))
  558. # 获取 MinIO 配置
  559. minio_client = get_minio_client()
  560. config = get_minio_config()
  561. prefix = config['PREFIX']
  562. # 调用处理逻辑
  563. result = solve_unstructured_data(node_id, minio_client, prefix)
  564. if result:
  565. return jsonify(success({"message": "处理成功"}))
  566. else:
  567. return jsonify(failed("处理失败"))
  568. except Exception as e:
  569. logger.error(f"处理非结构化数据失败: {str(e)}")
  570. return jsonify(failed(str(e)))
  571. # 创建文本图谱
  572. @bp.route('/text/graph', methods=['POST'])
  573. def create_text_graph():
  574. try:
  575. # 获取参数
  576. node_id = request.json.get('id')
  577. entity = request.json.get('entity_zh')
  578. entity_en = request.json.get('entity_en')
  579. if not all([node_id, entity_zh, entity_en]):
  580. return jsonify(failed("参数不完整"))
  581. # 创建图谱
  582. result = handle_txt_graph(node_id, entity_zh, entity_en)
  583. if result:
  584. return jsonify(success({"message": "图谱创建成功"}))
  585. else:
  586. return jsonify(failed("图谱创建失败"))
  587. except Exception as e:
  588. logger.error(f"创建文本图谱失败: {str(e)}")
  589. return jsonify(failed(str(e)))
  590. @bp.route('/config', methods=['GET'])
  591. @require_auth
  592. def get_meta_config():
  593. """获取元数据配置信息"""
  594. config = get_minio_config()
  595. return jsonify({
  596. 'bucket_name': config['MINIO_BUCKET'],
  597. 'prefix': config['PREFIX'],
  598. 'allowed_extensions': list(config['ALLOWED_EXTENSIONS'])
  599. })
  600. # 更新元数据
  601. @bp.route('/node/update', methods=['POST'])
  602. def meta_node_update():
  603. try:
  604. # 从请求中获取节点ID和更新数据
  605. node_id = request.json.get('id')
  606. if not node_id:
  607. return jsonify(failed("节点ID不能为空"))
  608. # 验证并转换节点ID为整数
  609. try:
  610. node_id = int(node_id)
  611. except (ValueError, TypeError):
  612. return jsonify(failed(f"节点ID必须为整数,当前值: {node_id}"))
  613. # 更新节点
  614. with neo4j_driver.get_session() as session:
  615. # 检查节点是否存在并获取当前值
  616. check_cypher = """
  617. MATCH (n:DataMeta)
  618. WHERE id(n) = $node_id
  619. RETURN n
  620. """
  621. result = session.run(check_cypher, node_id=node_id)
  622. node = result.single()
  623. if not node or not node["n"]:
  624. return jsonify(failed("节点不存在"))
  625. # 获取当前节点的所有属性
  626. current_node = node["n"]
  627. current_properties = dict(current_node)
  628. # 构建更新语句,只更新提供的属性
  629. update_cypher = """
  630. MATCH (n:DataMeta)
  631. WHERE id(n) = $node_id
  632. SET n.updateTime = $update_time
  633. """
  634. # 准备更新参数
  635. update_params = {
  636. 'node_id': node_id,
  637. 'update_time': get_formatted_time()
  638. }
  639. # 处理每个可能的更新字段
  640. fields_to_update = {
  641. 'name_zh': request.json.get('name_zh'),
  642. 'category': request.json.get('category'),
  643. 'alias': request.json.get('alias'),
  644. 'affiliation': request.json.get('affiliation'),
  645. 'data_type': request.json.get('data_type'),
  646. 'describe': request.json.get('describe'),
  647. 'status': request.json.get('status'),
  648. 'name_en': request.json.get('name_en')
  649. }
  650. # 只更新提供了新值的字段
  651. for field, new_value in fields_to_update.items():
  652. if new_value is not None:
  653. # 特殊处理 data_type 字段映射
  654. if field == 'data_type':
  655. update_cypher += f", n.data_type = ${field}\n"
  656. else:
  657. update_cypher += f", n.{field} = ${field}\n"
  658. update_params[field] = new_value
  659. update_cypher += "RETURN n"
  660. result = session.run(update_cypher, **update_params)
  661. updated_node = result.single()
  662. if updated_node and updated_node["n"]:
  663. node_data = dict(updated_node["n"])
  664. node_data["id"] = updated_node["n"].id
  665. # 如果更新了标签,处理标签关系
  666. tag = request.json.get('tag')
  667. if tag is not None:
  668. # 先删除现有标签关系
  669. delete_tag_cypher = """
  670. MATCH (n:DataMeta)-[r:LABEL]->(t:DataLabel)
  671. WHERE id(n) = $node_id
  672. DELETE r
  673. """
  674. session.run(delete_tag_cypher, node_id=node_id)
  675. # 创建新的标签关系
  676. if tag and isinstance(tag, dict) and 'id' in tag and tag['id']:
  677. try:
  678. tag_id = int(tag['id'])
  679. create_tag_cypher = """
  680. MATCH (n:DataMeta), (t:DataLabel)
  681. WHERE id(n) = $node_id AND id(t) = $tag_id
  682. MERGE (n)-[r:LABEL]->(t)
  683. RETURN r
  684. """
  685. session.run(create_tag_cypher, node_id=node_id, tag_id=tag_id)
  686. except (ValueError, TypeError):
  687. logger.warning(f"标签ID无效: {tag.get('id')}")
  688. logger.info(f"成功更新元数据节点: ID={node_data['id']}")
  689. return jsonify(success(node_data))
  690. else:
  691. logger.error(f"更新元数据节点失败: ID={node_id}")
  692. return jsonify(failed("更新元数据节点失败"))
  693. except Exception as e:
  694. logger.error(f"更新元数据失败: {str(e)}")
  695. return jsonify(failed(str(e)))