work_center.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. """Unified governance work-center API."""
  2. from __future__ import annotations
  3. from flask import g, jsonify, request
  4. from app import db
  5. from app.api.system import bp
  6. from app.core.events.email_delivery import smtp_sender
  7. from app.core.governance.work_center import UnifiedWorkCenterService
  8. from app.core.governance.work_center_repository import SqlAlchemyWorkCenterRepository
  9. from app.core.system.permissions import (
  10. WORK_CENTER_MANAGE,
  11. WORK_CENTER_OPERATE,
  12. WORK_CENTER_READ,
  13. permissions_for_roles,
  14. require_permissions,
  15. )
  16. from app.models.result import failed, success
  17. def _service():
  18. return UnifiedWorkCenterService(
  19. SqlAlchemyWorkCenterRepository(db.session),
  20. commit=db.session.commit,
  21. rollback=db.session.rollback,
  22. )
  23. def _expected_version() -> int:
  24. raw = str(request.headers.get("If-Match") or "").strip()
  25. if raw.startswith("W/"):
  26. raw = raw[2:].strip()
  27. raw = raw.strip('"')
  28. if not raw.isdigit():
  29. raise ValueError("missing valid If-Match version")
  30. return int(raw)
  31. def _etag(response, version):
  32. response.headers["ETag"] = f'"{int(version)}"'
  33. return response
  34. def _error(exc):
  35. db.session.rollback()
  36. if "If-Match" in str(exc):
  37. status = 428
  38. elif isinstance(exc, LookupError):
  39. status = 404
  40. elif isinstance(exc, PermissionError):
  41. status = 403
  42. elif isinstance(exc, RuntimeError):
  43. status = 409
  44. else:
  45. status = 400
  46. return jsonify(failed(str(exc), code=status)), status
  47. @bp.route("/work-center/workflows", methods=["GET"])
  48. @require_permissions(WORK_CENTER_READ)
  49. def list_work_center_workflows():
  50. return jsonify(success(_service().list_workflows()))
  51. @bp.route("/work-center/workflows", methods=["POST"])
  52. @require_permissions(WORK_CENTER_MANAGE)
  53. def create_work_center_workflow():
  54. try:
  55. result = _service().create_workflow(
  56. request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]
  57. )
  58. return _etag(jsonify(success(result, "流程草稿已创建", code=201)), 1), 201
  59. except (ValueError, RuntimeError) as exc:
  60. return _error(exc)
  61. @bp.route("/work-center/workflows/<workflow_uid>/revisions", methods=["POST"])
  62. @require_permissions(WORK_CENTER_MANAGE)
  63. def revise_work_center_workflow(workflow_uid):
  64. try:
  65. result = _service().revise_workflow(
  66. workflow_uid,
  67. request.get_json(silent=True) or {},
  68. expected_version=_expected_version(),
  69. actor_uid=g.current_user["id"],
  70. )
  71. return _etag(jsonify(success(result, "流程版本已创建")), result["current_version"])
  72. except (ValueError, LookupError, PermissionError, RuntimeError) as exc:
  73. return _error(exc)
  74. @bp.route("/work-center/workflows/<workflow_uid>/publish", methods=["POST"])
  75. @require_permissions(WORK_CENTER_MANAGE)
  76. def publish_work_center_workflow(workflow_uid):
  77. try:
  78. result = _service().publish_workflow(
  79. workflow_uid,
  80. expected_version=_expected_version(),
  81. actor_uid=g.current_user["id"],
  82. )
  83. return _etag(jsonify(success(result, "流程版本已发布")), result["current_version"])
  84. except (ValueError, LookupError, PermissionError, RuntimeError) as exc:
  85. return _error(exc)
  86. @bp.route("/work-center/tasks", methods=["GET"])
  87. @require_permissions(WORK_CENTER_READ)
  88. def list_work_center_tasks():
  89. permissions = permissions_for_roles(g.current_user.get("roles", []))
  90. filters = {
  91. key: request.args.get(key)
  92. for key in ("status", "task_type", "subject_type", "assignee_uid")
  93. if request.args.get(key)
  94. }
  95. filters.update(
  96. {
  97. "requester_uid": g.current_user["id"],
  98. "can_manage": WORK_CENTER_MANAGE in permissions,
  99. }
  100. )
  101. return jsonify(success(_service().list_tasks(**filters)))
  102. @bp.route("/work-center/tasks", methods=["POST"])
  103. @require_permissions(WORK_CENTER_OPERATE)
  104. def create_work_center_task():
  105. try:
  106. result = _service().create_task(
  107. request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]
  108. )
  109. return _etag(jsonify(success(result, "统一任务已创建", code=201)), result["current_version"]), 201
  110. except (ValueError, LookupError, RuntimeError) as exc:
  111. return _error(exc)
  112. @bp.route("/work-center/tasks/<task_uid>", methods=["GET"])
  113. @require_permissions(WORK_CENTER_READ)
  114. def get_work_center_task(task_uid):
  115. try:
  116. permissions = permissions_for_roles(g.current_user.get("roles", []))
  117. result = _service().task_detail(
  118. task_uid,
  119. requester_uid=g.current_user["id"],
  120. can_manage=WORK_CENTER_MANAGE in permissions,
  121. )
  122. return _etag(jsonify(success(result)), result["current_version"])
  123. except (ValueError, LookupError) as exc:
  124. return _error(exc)
  125. def _task_action(task_uid, action):
  126. service = _service()
  127. body = request.get_json(silent=True) or {}
  128. arguments = {
  129. "expected_version": _expected_version(),
  130. "actor_uid": g.current_user["id"],
  131. }
  132. if action == "review":
  133. return service.review_task(task_uid, body, **arguments)
  134. if action == "transfer":
  135. return service.transfer_review(task_uid, body, **arguments)
  136. if action == "close":
  137. return service.close_task(task_uid, body, **arguments)
  138. return service.reopen_task(task_uid, body, **arguments)
  139. @bp.route("/work-center/tasks/<task_uid>/<action>", methods=["POST"])
  140. @require_permissions(WORK_CENTER_OPERATE)
  141. def operate_work_center_task(task_uid, action):
  142. if action not in {"review", "transfer", "close", "reopen"}:
  143. return jsonify(failed("unsupported task action", code=404)), 404
  144. try:
  145. result = _task_action(task_uid, action)
  146. return _etag(jsonify(success(result, "任务状态已更新")), result["current_version"])
  147. except (ValueError, LookupError, PermissionError, RuntimeError) as exc:
  148. return _error(exc)
  149. @bp.route("/work-center/tasks/<task_uid>/comments", methods=["POST"])
  150. @require_permissions(WORK_CENTER_OPERATE)
  151. def add_work_center_comment(task_uid):
  152. try:
  153. result = _service().add_comment(
  154. task_uid,
  155. request.get_json(silent=True) or {},
  156. actor_uid=g.current_user["id"],
  157. )
  158. return jsonify(success(result, "评论已添加", code=201)), 201
  159. except (ValueError, LookupError, PermissionError, RuntimeError) as exc:
  160. return _error(exc)
  161. @bp.route("/work-center/tasks/<task_uid>/attachments", methods=["POST"])
  162. @require_permissions(WORK_CENTER_OPERATE)
  163. def add_work_center_attachment(task_uid):
  164. try:
  165. result = _service().add_attachment(
  166. task_uid,
  167. request.get_json(silent=True) or {},
  168. actor_uid=g.current_user["id"],
  169. )
  170. return jsonify(success(result, "附件证据已登记", code=201)), 201
  171. except (ValueError, LookupError, PermissionError, RuntimeError) as exc:
  172. return _error(exc)
  173. @bp.route("/work-center/timeouts/process", methods=["POST"])
  174. @require_permissions(WORK_CENTER_MANAGE)
  175. def process_work_center_timeouts():
  176. try:
  177. body = request.get_json(silent=True) or {}
  178. return jsonify(
  179. success(
  180. _service().process_timeouts(
  181. at=body.get("at"), actor_uid=g.current_user["id"]
  182. ),
  183. "逾期任务已处理",
  184. )
  185. )
  186. except (ValueError, RuntimeError) as exc:
  187. return _error(exc)
  188. @bp.route("/work-center/notifications", methods=["GET"])
  189. @require_permissions(WORK_CENTER_READ)
  190. def list_work_center_notifications():
  191. return jsonify(
  192. success(
  193. _service().list_notifications(
  194. g.current_user["id"],
  195. unread_only=request.args.get("unread_only") == "true",
  196. )
  197. )
  198. )
  199. @bp.route("/work-center/notifications/<notification_uid>/read", methods=["POST"])
  200. @require_permissions(WORK_CENTER_OPERATE)
  201. def read_work_center_notification(notification_uid):
  202. try:
  203. return jsonify(
  204. success(
  205. _service().mark_notification_read(
  206. notification_uid, actor_uid=g.current_user["id"]
  207. ),
  208. "消息已读",
  209. )
  210. )
  211. except (ValueError, LookupError, RuntimeError) as exc:
  212. return _error(exc)
  213. @bp.route("/work-center/notifications/deliver", methods=["POST"])
  214. @require_permissions(WORK_CENTER_MANAGE)
  215. def deliver_work_center_notifications():
  216. try:
  217. body = request.get_json(silent=True) or {}
  218. result = _service().deliver_notifications(
  219. "email", smtp_sender, at=body.get("at"), limit=body.get("limit", 50)
  220. )
  221. return jsonify(success(result, "邮件投递批次已处理"))
  222. except (ValueError, RuntimeError) as exc:
  223. return _error(exc)
  224. @bp.route("/work-center/templates", methods=["GET"])
  225. @require_permissions(WORK_CENTER_READ)
  226. def list_work_center_templates():
  227. return jsonify(success(_service().list_notification_templates()))
  228. @bp.route("/work-center/templates", methods=["POST"])
  229. @require_permissions(WORK_CENTER_MANAGE)
  230. def create_work_center_template():
  231. try:
  232. result = _service().create_notification_template(
  233. request.get_json(silent=True) or {}, actor_uid=g.current_user["id"]
  234. )
  235. return _etag(jsonify(success(result, "通知模板已创建", code=201)), 1), 201
  236. except (ValueError, RuntimeError) as exc:
  237. return _error(exc)
  238. @bp.route("/work-center/templates/<template_uid>", methods=["PATCH"])
  239. @require_permissions(WORK_CENTER_MANAGE)
  240. def revise_work_center_template(template_uid):
  241. try:
  242. result = _service().revise_notification_template(
  243. template_uid,
  244. request.get_json(silent=True) or {},
  245. expected_version=_expected_version(),
  246. actor_uid=g.current_user["id"],
  247. )
  248. return _etag(jsonify(success(result, "通知模板已更新")), result["current_version"])
  249. except (ValueError, LookupError, RuntimeError) as exc:
  250. return _error(exc)
  251. @bp.route("/work-center/preferences", methods=["GET"])
  252. @require_permissions(WORK_CENTER_READ)
  253. def get_work_center_preferences():
  254. result = _service().get_notification_preferences(g.current_user["id"])
  255. return _etag(jsonify(success(result)), result["revision"])
  256. @bp.route("/work-center/preferences", methods=["PUT"])
  257. @require_permissions(WORK_CENTER_OPERATE)
  258. def replace_work_center_preferences():
  259. try:
  260. result = _service().replace_notification_preferences(
  261. request.get_json(silent=True) or {},
  262. expected_revision=_expected_version(),
  263. actor_uid=g.current_user["id"],
  264. )
  265. return _etag(jsonify(success(result, "通知偏好已更新")), result["revision"])
  266. except (ValueError, RuntimeError) as exc:
  267. return _error(exc)
  268. @bp.route("/work-center/dashboard", methods=["GET"])
  269. @require_permissions(WORK_CENTER_READ)
  270. def get_work_center_dashboard():
  271. try:
  272. return jsonify(success(_service().dashboard(at=request.args.get("at"))))
  273. except ValueError as exc:
  274. return _error(exc)