routes.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471
  1. import json
  2. from flask import Response, current_app, jsonify, request
  3. from app import db
  4. from app.api.data_interface import bp
  5. from app.core.data_interface import interface
  6. from app.core.data_rules.repository import DataRuleRepository
  7. from app.core.graph.graph_operations import (
  8. MyEncoder,
  9. connect_graph,
  10. create_or_get_node,
  11. )
  12. from app.core.meta_data import get_formatted_time, translate_and_parse
  13. from app.models.result import failed, success
  14. def _repository():
  15. configured = current_app.extensions.get("data_rule_repository")
  16. if configured is not None:
  17. return configured
  18. return DataRuleRepository(db.session)
  19. _STANDARD_WRITE_FIELDS = {
  20. "category",
  21. "describe",
  22. "id",
  23. "input",
  24. "migration_metadata",
  25. "name_zh",
  26. "output",
  27. "rule_version_id",
  28. "scope",
  29. "status",
  30. "tag",
  31. }
  32. def _legacy_standard_exists(node_id):
  33. if isinstance(node_id, bool) or not isinstance(node_id, int):
  34. return False
  35. query = (
  36. "MATCH (n:data_standard) WHERE id(n) = $node_id "
  37. "RETURN n.rule_version_id AS rule_version_id"
  38. )
  39. with connect_graph().session() as session:
  40. row = session.run(query, node_id=node_id).single()
  41. return row is not None and row.get("rule_version_id") is None
  42. def _prepare_standard_write(value, *, is_new):
  43. if not isinstance(value, dict):
  44. raise ValueError("request body must be an object")
  45. if set(value) - _STANDARD_WRITE_FIELDS:
  46. raise ValueError("data standard request contains unsupported fields")
  47. receiver = dict(value)
  48. rule_version_id = receiver.get("rule_version_id")
  49. if rule_version_id is None:
  50. if is_new or not _legacy_standard_exists(receiver.get("id")):
  51. raise ValueError(
  52. "new data standard requires a published rule_version_id"
  53. )
  54. else:
  55. _repository().require_published_rule_version(rule_version_id)
  56. return receiver
  57. @bp.route("/data/standard/add", methods=["POST"])
  58. def data_standard_add():
  59. try:
  60. receiver = _prepare_standard_write(
  61. request.get_json(silent=True), is_new=True
  62. )
  63. name_zh = receiver["name_zh"]
  64. name_en = translate_and_parse(name_zh)
  65. receiver["name_en"] = name_en[0]
  66. receiver["create_time"] = get_formatted_time()
  67. receiver["tag"] = json.dumps(receiver["tag"], ensure_ascii=False)
  68. create_or_get_node("data_standard", **receiver)
  69. res = success("", "success")
  70. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  71. except (KeyError, TypeError, ValueError) as e:
  72. res = failed(str(e), 400, {})
  73. return jsonify(res), 400
  74. except Exception as e:
  75. res = failed(str(e), 500, {})
  76. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  77. @bp.route("/data/standard/detail", methods=["POST"])
  78. def data_standard_detail():
  79. try:
  80. receiver = request.get_json()
  81. nodeid = receiver["id"] # id
  82. cql = """MATCH (n:data_standard) where id(n) = $nodeId
  83. RETURN properties(n) as property"""
  84. # Create a session from the driver returned by connect_graph
  85. with connect_graph().session() as session:
  86. result = session.run(cql, nodeId=nodeid).single()
  87. property = result["property"] if result else {}
  88. if "tag" not in property:
  89. property["tag"] = None
  90. else:
  91. property["tag"] = json.loads(property["tag"])
  92. if "describe" not in property:
  93. property["describe"] = None
  94. res = success(property, "success")
  95. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  96. except Exception as e:
  97. res = failed(str(e), 500, {})
  98. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  99. @bp.route("/data/standard/code", methods=["POST"])
  100. def data_standard_code():
  101. return (
  102. jsonify(
  103. failed(
  104. "旧数据标准代码生成入口已关闭,请发布 RuleVersion 后绑定",
  105. 410,
  106. {"semantics": "read_only_migration"},
  107. )
  108. ),
  109. 410,
  110. )
  111. @bp.route("/data/standard/update", methods=["POST"])
  112. def data_standard_update():
  113. try:
  114. receiver = _prepare_standard_write(
  115. request.get_json(silent=True), is_new=False
  116. )
  117. name_zh = receiver["name_zh"]
  118. name_en = translate_and_parse(name_zh)
  119. receiver["name_en"] = name_en[0]
  120. receiver["create_time"] = get_formatted_time()
  121. create_or_get_node("data_standard", **receiver)
  122. res = success("", "success")
  123. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  124. except (KeyError, TypeError, ValueError) as e:
  125. res = failed(str(e), 400, {})
  126. return jsonify(res), 400
  127. except Exception as e:
  128. res = failed(str(e), 500, {})
  129. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  130. @bp.route("/data/standard/list", methods=["POST"])
  131. def data_standard_list():
  132. try:
  133. receiver = request.get_json()
  134. page = int(receiver.get("current", 1))
  135. page_size = int(receiver.get("size", 10))
  136. name_en_filter = receiver.get("name_en", None)
  137. name_zh_filter = receiver.get("name_zh", None)
  138. category = receiver.get("category", None)
  139. time = receiver.get("time", None)
  140. skip_count = (page - 1) * page_size
  141. data, total = interface.standard_list(
  142. skip_count,
  143. page_size,
  144. name_en_filter,
  145. name_zh_filter,
  146. category,
  147. time,
  148. )
  149. response_data = {
  150. "records": data,
  151. "total": total,
  152. "size": page_size,
  153. "current": page,
  154. }
  155. res = success(response_data, "success")
  156. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  157. except Exception as e:
  158. res = failed(str(e), 500, {})
  159. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  160. @bp.route("/data/standard/graph/all", methods=["POST"])
  161. def data_standard_graph_all():
  162. try:
  163. receiver = request.get_json()
  164. nodeid = receiver["id"]
  165. type = receiver["type"] # kinship/impact/all
  166. if type == "kinship":
  167. result = interface.standard_kinship_graph(nodeid)
  168. elif type == "impact":
  169. result = interface.standard_impact_graph(nodeid)
  170. else:
  171. result = interface.standard_all_graph(nodeid)
  172. return json.dumps(
  173. success(result, "success"),
  174. ensure_ascii=False,
  175. cls=MyEncoder,
  176. )
  177. except Exception as e:
  178. res = failed(str(e), 500, {})
  179. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  180. @bp.route("/data/label/add", methods=["POST"])
  181. def data_label_add():
  182. try:
  183. receiver = request.get_json()
  184. name_zh = receiver["name_zh"]
  185. name_en = translate_and_parse(name_zh)
  186. receiver["name_en"] = name_en[0]
  187. receiver["create_time"] = get_formatted_time()
  188. create_or_get_node("DataLabel", **receiver)
  189. res = success("", "success")
  190. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  191. except Exception as e:
  192. res = failed(str(e), 500, {})
  193. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  194. @bp.route("/data/label/detail", methods=["POST"])
  195. def data_label_detail():
  196. try:
  197. receiver = request.get_json()
  198. nodeid = receiver["id"] # id
  199. cql = """MATCH (n:DataLabel) where id(n) = $nodeId
  200. RETURN properties(n) as property"""
  201. with connect_graph().session() as session:
  202. result = session.run(cql, nodeId=nodeid).single()
  203. property = result["property"] if result else {}
  204. if "describe" not in property:
  205. property["describe"] = None
  206. # 将传入的节点ID添加到返回信息中
  207. property["id"] = nodeid
  208. res = success(property, "success")
  209. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  210. except Exception as e:
  211. res = failed(str(e), 500, {})
  212. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  213. @bp.route("/data/label/list", methods=["POST"])
  214. def data_label_list():
  215. try:
  216. receiver = request.get_json()
  217. page = int(receiver.get("current", 1))
  218. page_size = int(receiver.get("size", 10))
  219. name_en_filter = receiver.get("name_en", None)
  220. name_zh_filter = receiver.get("name_zh", None)
  221. category = receiver.get("category", None)
  222. group = receiver.get("group", None)
  223. skip_count = (page - 1) * page_size
  224. data, total = interface.label_list(
  225. skip_count,
  226. page_size,
  227. name_en_filter,
  228. name_zh_filter,
  229. category,
  230. group,
  231. )
  232. response_data = {
  233. "records": data,
  234. "total": total,
  235. "size": page_size,
  236. "current": page,
  237. }
  238. res = success(response_data, "success")
  239. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  240. except Exception as e:
  241. res = failed(str(e), 500, {})
  242. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  243. @bp.route("/labellist", methods=["POST"])
  244. def interface_label_list():
  245. """获取 DataLabel 列表(支持多条件 category_filter 过滤)"""
  246. try:
  247. receiver = request.get_json() or {}
  248. page = int(receiver.get("current", 1))
  249. page_size = int(receiver.get("size", 10))
  250. name_en_filter = receiver.get("name_en")
  251. name_zh_filter = receiver.get("name_zh")
  252. category_filter = receiver.get("category_filter")
  253. group_filter = receiver.get("group")
  254. skip_count = (page - 1) * page_size
  255. data, total = interface.label_list(
  256. skip_count,
  257. page_size,
  258. name_en_filter,
  259. name_zh_filter,
  260. category_filter,
  261. group_filter,
  262. )
  263. # 只保留 id, name_zh, name_en 三个字段
  264. records = [
  265. {
  266. "id": item.get("id"),
  267. "name_zh": item.get("name_zh"),
  268. "name_en": item.get("name_en"),
  269. }
  270. for item in data
  271. ]
  272. response_data = {
  273. "records": records,
  274. "total": total,
  275. "size": page_size,
  276. "current": page,
  277. }
  278. res = success(response_data, "success")
  279. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  280. except Exception as e:
  281. res = failed(str(e), 500, {})
  282. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  283. @bp.route("/graphall", methods=["POST"])
  284. def interface_graph_all():
  285. """获取完整关系图谱"""
  286. try:
  287. receiver = request.get_json() or {}
  288. domain_id = receiver.get("id")
  289. include_meta = receiver.get("meta", True)
  290. if domain_id is None:
  291. res = failed("节点ID不能为空")
  292. return Response(
  293. json.dumps(res, ensure_ascii=False, cls=MyEncoder),
  294. mimetype="application/json",
  295. )
  296. try:
  297. domain_id = int(domain_id)
  298. except (ValueError, TypeError):
  299. res = failed(f"节点ID必须为整数, 收到的是: {domain_id}")
  300. return Response(
  301. json.dumps(res, ensure_ascii=False, cls=MyEncoder),
  302. mimetype="application/json",
  303. )
  304. graph_data = interface.graph_all(domain_id, include_meta)
  305. res = success(graph_data)
  306. return Response(
  307. json.dumps(res, ensure_ascii=False, cls=MyEncoder),
  308. mimetype="application/json",
  309. )
  310. except Exception as e:
  311. res = failed("获取图谱失败", error=str(e))
  312. return Response(
  313. json.dumps(res, ensure_ascii=False, cls=MyEncoder),
  314. mimetype="application/json",
  315. )
  316. @bp.route("/data/label/dynamic/identify", methods=["POST"])
  317. def data_label_dynamic_identify():
  318. try:
  319. receiver = request.get_json()
  320. name_filter = receiver.get("content", None)
  321. data = interface.dynamic_label_list(name_filter)
  322. res = success(data, "success")
  323. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  324. except Exception as e:
  325. res = failed(str(e), 500, {})
  326. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  327. @bp.route("/data/label/graph/all", methods=["POST"])
  328. def data_label_graph():
  329. try:
  330. receiver = request.get_json()
  331. nodeid = receiver["id"]
  332. type = receiver["type"] # kinship/impact/all
  333. if type == "kinship":
  334. result = interface.label_kinship_graph(nodeid)
  335. elif type == "impact":
  336. result = interface.label_impact_graph(nodeid)
  337. else:
  338. result = interface.label_kinship_graph(nodeid)
  339. return json.dumps(
  340. success(result, "success"),
  341. ensure_ascii=False,
  342. cls=MyEncoder,
  343. )
  344. except Exception as e:
  345. res = failed(str(e), 500, {})
  346. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  347. @bp.route("/metric/label/standard/delete", methods=["POST"])
  348. def metric_label_standard_delete():
  349. try:
  350. receiver = request.get_json()
  351. sourceid = receiver["sourceid"]
  352. targetid = receiver["targetid"]
  353. cql = """
  354. MATCH (source)-[r]-(target)
  355. WHERE id(source) = $sourceid AND id(target) = $targetid
  356. DELETE r
  357. """
  358. with connect_graph().session() as session:
  359. session.run(cql, sourceid=sourceid, targetid=targetid)
  360. res = success("", "success")
  361. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  362. except Exception as e:
  363. res = failed(str(e), 500, {})
  364. return json.dumps(res, ensure_ascii=False, cls=MyEncoder)
  365. @bp.route("/data/label/delete", methods=["POST"])
  366. def data_label_delete():
  367. """Delete data label node"""
  368. try:
  369. receiver = request.get_json()
  370. node_id = receiver.get("id") if receiver else None
  371. if not node_id:
  372. return jsonify(failed("node id is required", 400, {}))
  373. try:
  374. node_id = int(node_id)
  375. except (ValueError, TypeError):
  376. return jsonify(failed("node id must be an integer", 400, {}))
  377. delete_result = interface.node_delete(node_id)
  378. message = delete_result.get("message") or ""
  379. if delete_result.get("success"):
  380. res = success(
  381. {
  382. "id": node_id,
  383. "message": message,
  384. },
  385. "delete success",
  386. )
  387. else:
  388. res = failed(
  389. message,
  390. 500,
  391. {
  392. "id": node_id,
  393. "message": message,
  394. },
  395. )
  396. return jsonify(res)
  397. except Exception as e:
  398. return jsonify(failed(f"delete failed: {str(e)}", 500, {}))