monitor.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. """监控模块接口(操作日志等)"""
  2. from __future__ import annotations
  3. from datetime import datetime
  4. from typing import List, Optional
  5. from fastapi import APIRouter, Depends
  6. from pydantic import BaseModel, ConfigDict, Field
  7. from sqlalchemy.orm import Session
  8. from app.core.database import get_db
  9. from app.core.menu_guard import require_admin_or_super_admin
  10. from app.core.response import error_response, success_response
  11. from app.crud.monitor.oper_log import apply_oper_log_filters, get_oper_log, list_oper_logs
  12. from app.schemas.monitor.operlog import OperLogDetailBody, OperLogListBody
  13. from app.utils.excel_export import (
  14. EXPORT_MAX_ROWS,
  15. ExportColumn,
  16. ExportValidationError,
  17. export_rows_to_xlsx_response,
  18. filter_export_columns,
  19. )
  20. router = APIRouter(prefix="/web/monitor", tags=["系统监控"])
  21. def _user_info(user: dict) -> dict:
  22. dept_id = (user.get("region_id") or user.get("dept_id") or "").strip()
  23. return {
  24. "user_id": (user.get("user_id") or user.get("id") or "").strip(),
  25. "role": (user.get("role") or "").strip(),
  26. "tenant_no": (user.get("tenant_no") or "").strip(),
  27. "dept_id": dept_id,
  28. "region_id": dept_id,
  29. }
  30. @router.post("/operlog/list")
  31. def operlog_list(
  32. body: OperLogListBody = None,
  33. user: dict = Depends(require_admin_or_super_admin),
  34. db: Session = Depends(get_db),
  35. ):
  36. """操作日志分页列表(基于 API 请求日志适配 RuoYi OperLog 结构)"""
  37. try:
  38. page = body.page if body else 1
  39. page_size = body.page_size if body else 10
  40. info = _user_info(user)
  41. rows, total = list_oper_logs(
  42. db,
  43. page=page,
  44. page_size=page_size,
  45. title=body.title if body else None,
  46. business_type=body.business_type if body else None,
  47. business_id=body.business_id if body else None,
  48. business_title=body.business_title if body else None,
  49. oper_name=body.oper_name if body else None,
  50. title_in=body.title_in if body else None,
  51. oper_name_in=body.oper_name_in if body else None,
  52. status=body.status if body else None,
  53. user_info=info,
  54. tenant_no=info.get("tenant_no"),
  55. )
  56. total_pages = (total + page_size - 1) // page_size if page_size else 0
  57. return success_response(
  58. {
  59. "rows": rows,
  60. "list": rows,
  61. "total": total,
  62. "page": page,
  63. "pageSize": page_size,
  64. "totalPages": total_pages,
  65. }
  66. )
  67. except Exception as e:
  68. return error_response(500, str(e))
  69. class OperLogColumnFacetBody(BaseModel):
  70. """操作日志列头可选值(去重;可附带列表上下文)"""
  71. model_config = ConfigDict(populate_by_name=True, extra="allow")
  72. field: str = Field(..., description="title | operName")
  73. keyword: Optional[str] = Field(default=None, description="列表模块名称关键字")
  74. list_title: Optional[str] = Field(default=None, alias="listTitle", description="顶部模块名称搜索")
  75. list_oper_name: Optional[str] = Field(default=None, alias="listOperName", description="顶部操作人员搜索")
  76. limit: Optional[int] = Field(default=500, ge=1, le=500, description="返回条数上限")
  77. @router.post("/operlog/column-facet-values")
  78. def operlog_column_facet_values(
  79. body: OperLogColumnFacetBody,
  80. user: dict = Depends(require_admin_or_super_admin),
  81. db: Session = Depends(get_db),
  82. ):
  83. """操作日志列头可选值:在列表上下文(排除本列)下去重。"""
  84. from sqlalchemy import func
  85. from app.models.models import SysApiRequestLog
  86. from app.services.pl.facet_context import strip_simple_facet_self_params
  87. if body.field not in ("title", "operName"):
  88. return error_response(400, "不支持的列")
  89. try:
  90. info = _user_info(user)
  91. column = SysApiRequestLog.business_title if body.field == "title" else SysApiRequestLog.create_name
  92. data = body.model_dump(by_alias=True, exclude_none=True)
  93. extra = getattr(body, "model_extra", None) or {}
  94. for key, value in extra.items():
  95. if key not in data and value not in (None, ""):
  96. data[key] = value
  97. params = strip_simple_facet_self_params(data, body.field)
  98. # 顶部搜索与列头多选分开传递,排除本列时仍保留顶部搜索。
  99. params["title"] = body.list_title or body.keyword or params.get("title")
  100. params["operName"] = body.list_oper_name or params.get("operName")
  101. filters = OperLogListBody.model_validate(params)
  102. q = (
  103. db.query(column.label("value"), func.count(SysApiRequestLog.id).label("cnt"))
  104. .filter(
  105. SysApiRequestLog.is_del == 0,
  106. column.isnot(None),
  107. column != "",
  108. )
  109. )
  110. q = apply_oper_log_filters(
  111. q,
  112. db,
  113. **filters.model_dump(exclude={"page", "page_size"}),
  114. user_info=info,
  115. tenant_no=info.get("tenant_no"),
  116. )
  117. lim = min(max(1, int(body.limit or 500)), 500)
  118. rows = q.group_by(column).order_by(column.asc()).limit(lim).all()
  119. out = []
  120. for row in rows:
  121. value = str(row.value).strip() if row.value is not None else ""
  122. if not value:
  123. continue
  124. out.append({"value": value, "label": value, "count": int(row.cnt or 0)})
  125. return success_response({"list": out})
  126. except ValueError as e:
  127. return error_response(400, str(e))
  128. except Exception as e:
  129. return error_response(500, str(e))
  130. _OPER_STATUS_LABEL = {0: "成功", 1: "失败"}
  131. _BUSINESS_TYPE_LABEL = {
  132. 0: "其它",
  133. 1: "新增",
  134. 2: "修改",
  135. 3: "删除",
  136. 4: "授权",
  137. 5: "导出",
  138. 6: "导入",
  139. 7: "强退",
  140. 8: "生成代码",
  141. 9: "清空数据",
  142. }
  143. OPERLOG_EXPORT_COLUMNS = [
  144. ExportColumn(title="操作模块", field="title"),
  145. ExportColumn(title="操作类型", field="businessTypeLabel"),
  146. ExportColumn(title="操作人员", field="operName"),
  147. ExportColumn(title="操作时间", field="operTime"),
  148. ExportColumn(title="状态", field="statusLabel"),
  149. ExportColumn(title="耗时(ms)", field="costTime"),
  150. ]
  151. class OperLogExportBody(BaseModel):
  152. model_config = ConfigDict(populate_by_name=True)
  153. title: Optional[str] = None
  154. business_type: Optional[int] = Field(default=None, alias="businessType")
  155. oper_name: Optional[str] = Field(default=None, alias="operName")
  156. status: Optional[int] = None
  157. ids: Optional[List[str]] = None
  158. columns: Optional[List[str]] = None
  159. @router.post("/operlog/export")
  160. def operlog_export(
  161. body: OperLogExportBody = None,
  162. user: dict = Depends(require_admin_or_super_admin),
  163. db: Session = Depends(get_db),
  164. ):
  165. """导出操作日志(当前筛选全量,或 ids 已选)。"""
  166. try:
  167. body = body or OperLogExportBody()
  168. info = _user_info(user)
  169. rows, _ = list_oper_logs(
  170. db,
  171. page=1,
  172. page_size=EXPORT_MAX_ROWS,
  173. title=body.title,
  174. business_type=body.business_type,
  175. oper_name=body.oper_name,
  176. status=body.status,
  177. ids=body.ids,
  178. max_page_size=EXPORT_MAX_ROWS,
  179. user_info=info,
  180. tenant_no=info.get("tenant_no"),
  181. )
  182. for row in rows:
  183. bt = row.get("businessType")
  184. row["businessTypeLabel"] = _BUSINESS_TYPE_LABEL.get(int(bt) if bt is not None else -1, str(bt if bt is not None else ""))
  185. st = row.get("status")
  186. row["statusLabel"] = _OPER_STATUS_LABEL.get(int(st) if st is not None else -1, str(st if st is not None else ""))
  187. requested = None
  188. if body.columns:
  189. requested = []
  190. for key in body.columns:
  191. if key == "businessType":
  192. requested.append("businessTypeLabel")
  193. elif key == "status":
  194. requested.append("statusLabel")
  195. else:
  196. requested.append(key)
  197. cols = filter_export_columns(OPERLOG_EXPORT_COLUMNS, requested)
  198. date_str = datetime.now().strftime("%Y%m%d")
  199. return export_rows_to_xlsx_response(
  200. db,
  201. tenant_no=info.get("tenant_no") or None,
  202. sheet_title="操作日志",
  203. columns=cols,
  204. rows=rows,
  205. filename_stem=f"操作日志_{date_str}",
  206. )
  207. except ExportValidationError as e:
  208. return error_response(400, str(e))
  209. except Exception as e:
  210. return error_response(500, str(e))
  211. @router.post("/operlog/detail")
  212. def operlog_detail(
  213. body: OperLogDetailBody,
  214. user: dict = Depends(require_admin_or_super_admin),
  215. db: Session = Depends(get_db),
  216. ):
  217. """操作日志详情"""
  218. try:
  219. info = _user_info(user)
  220. row = get_oper_log(db, body.id, user_info=info, tenant_no=info.get("tenant_no"))
  221. if not row:
  222. return error_response(404, "操作日志不存在")
  223. return success_response(row)
  224. except Exception as e:
  225. return error_response(500, str(e))