logs.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326
  1. """
  2. 日志管理接口
  3. """
  4. from fastapi import APIRouter, Depends, Query, Request
  5. from fastapi.responses import StreamingResponse
  6. from sqlalchemy.orm import Session
  7. from sqlalchemy import or_, and_
  8. from app.database import get_db
  9. from app.api.deps import get_current_admin
  10. from app.models.api_log import APILog
  11. from datetime import datetime, timedelta
  12. from app.utils.timezone import beijing_now
  13. import csv
  14. import io
  15. router = APIRouter()
  16. @router.get("")
  17. async def get_logs(
  18. page: int = Query(1, ge=1),
  19. page_size: int = Query(10, ge=1, le=100),
  20. keyword: str = Query(None, description="搜索关键词(接口路径)"),
  21. user_id: int = Query(None, description="用户ID筛选"),
  22. endpoint: str = Query(None, description="接口筛选"),
  23. method: str = Query(None, description="方法筛选"),
  24. response_code: int = Query(None, description="状态码筛选"),
  25. start_time: str = Query(None, description="开始时间"),
  26. end_time: str = Query(None, description="结束时间"),
  27. db: Session = Depends(get_db),
  28. current_admin = Depends(get_current_admin)
  29. ):
  30. """获取日志列表"""
  31. offset = (page - 1) * page_size
  32. query = db.query(APILog).filter(APILog.is_deleted == 0)
  33. # 搜索
  34. if keyword:
  35. query = query.filter(APILog.endpoint.like(f"%{keyword}%"))
  36. # 筛选
  37. if user_id is not None:
  38. query = query.filter(APILog.user_id == user_id)
  39. if endpoint:
  40. query = query.filter(APILog.endpoint.like(f"%{endpoint}%"))
  41. if method:
  42. query = query.filter(APILog.method == method)
  43. if response_code is not None:
  44. query = query.filter(APILog.response_code == response_code)
  45. if start_time:
  46. try:
  47. start_dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
  48. query = query.filter(APILog.created_at >= start_dt)
  49. except:
  50. pass
  51. if end_time:
  52. try:
  53. end_dt = datetime.fromisoformat(end_time.replace('Z', '+00:00'))
  54. query = query.filter(APILog.created_at <= end_dt)
  55. except:
  56. pass
  57. # 排序和分页
  58. total = query.count()
  59. logs = query.order_by(APILog.created_at.desc()).offset(offset).limit(page_size).all()
  60. return {
  61. "code": 200,
  62. "msg": "成功",
  63. "data": [
  64. {
  65. "id": l.id,
  66. "user_id": l.user_id,
  67. "endpoint": l.endpoint,
  68. "method": l.method,
  69. "ip_address": l.ip_address,
  70. "user_agent": l.user_agent,
  71. "response_code": l.response_code,
  72. "execution_time": l.execution_time,
  73. "created_at": l.created_at.isoformat() if l.created_at else None
  74. }
  75. for l in logs
  76. ],
  77. "total": total,
  78. "page": page,
  79. "page_size": page_size
  80. }
  81. @router.get("/{log_id}")
  82. async def get_log_detail(
  83. log_id: int,
  84. db: Session = Depends(get_db),
  85. current_admin = Depends(get_current_admin)
  86. ):
  87. """获取日志详情"""
  88. log = db.query(APILog).filter(APILog.id == log_id).first()
  89. if not log:
  90. return {"code": 404, "msg": "日志不存在"}
  91. return {
  92. "code": 200,
  93. "msg": "成功",
  94. "data": {
  95. "id": log.id,
  96. "user_id": log.user_id,
  97. "endpoint": log.endpoint,
  98. "method": log.method,
  99. "request_data": log.request_data,
  100. "response_data": log.response_data,
  101. "ip_address": log.ip_address,
  102. "user_agent": log.user_agent,
  103. "response_code": log.response_code,
  104. "execution_time": log.execution_time,
  105. "created_at": log.created_at.isoformat() if log.created_at else None
  106. }
  107. }
  108. @router.delete("")
  109. async def clear_logs(
  110. request: Request,
  111. db: Session = Depends(get_db),
  112. current_admin = Depends(get_current_admin)
  113. ):
  114. """清空日志(软删除)"""
  115. try:
  116. # 先统计要删除的日志数量
  117. log_count = db.query(APILog).filter(APILog.is_deleted == 0).count()
  118. from datetime import datetime
  119. db.query(APILog).filter(APILog.is_deleted == 0).update({"is_deleted": 1, "updated_at": beijing_now()})
  120. # 记录操作日志
  121. from app.utils.history import create_operation_log
  122. client_ip = request.client.host if request.client else None
  123. create_operation_log(
  124. db=db,
  125. admin_id=current_admin.id,
  126. module="日志管理",
  127. action="清空日志",
  128. description=f"清空API日志,共删除{log_count}条日志记录",
  129. old_data={"log_count": log_count},
  130. ip_address=client_ip
  131. )
  132. # 一次性提交所有更改
  133. db.commit()
  134. return {"code": 200, "msg": "清空成功"}
  135. except Exception as e:
  136. db.rollback()
  137. return {"code": 500, "msg": f"清空失败: {str(e)}"}
  138. @router.get("/export/csv")
  139. async def export_logs_csv(
  140. request: Request,
  141. keyword: str = Query(None),
  142. user_id: int = Query(None),
  143. endpoint: str = Query(None),
  144. method: str = Query(None),
  145. response_code: int = Query(None),
  146. start_time: str = Query(None),
  147. end_time: str = Query(None),
  148. db: Session = Depends(get_db),
  149. current_admin = Depends(get_current_admin)
  150. ):
  151. """导出日志为CSV"""
  152. query = db.query(APILog)
  153. # 应用相同的筛选条件
  154. if keyword:
  155. query = query.filter(APILog.endpoint.like(f"%{keyword}%"))
  156. if user_id is not None:
  157. query = query.filter(APILog.user_id == user_id)
  158. if endpoint:
  159. query = query.filter(APILog.endpoint.like(f"%{endpoint}%"))
  160. if method:
  161. query = query.filter(APILog.method == method)
  162. if response_code is not None:
  163. query = query.filter(APILog.response_code == response_code)
  164. if start_time:
  165. try:
  166. start_dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
  167. query = query.filter(APILog.created_at >= start_dt)
  168. except:
  169. pass
  170. if end_time:
  171. try:
  172. end_dt = datetime.fromisoformat(end_time.replace('Z', '+00:00'))
  173. query = query.filter(APILog.created_at <= end_dt)
  174. except:
  175. pass
  176. logs = query.order_by(APILog.created_at.desc()).all()
  177. # 记录操作日志
  178. from app.utils.history import create_operation_log
  179. client_ip = request.client.host if request.client else None
  180. create_operation_log(
  181. db=db,
  182. admin_id=current_admin.id,
  183. module="日志管理",
  184. action="导出日志CSV",
  185. description=f"导出API日志为CSV,共导出{len(logs)}条日志记录",
  186. new_data={
  187. "export_count": len(logs),
  188. "export_format": "CSV",
  189. "filters": {
  190. "keyword": keyword,
  191. "user_id": user_id,
  192. "endpoint": endpoint,
  193. "method": method,
  194. "response_code": response_code,
  195. "start_time": start_time,
  196. "end_time": end_time
  197. }
  198. },
  199. ip_address=client_ip
  200. )
  201. db.commit()
  202. # 生成CSV
  203. output = io.StringIO()
  204. writer = csv.writer(output)
  205. writer.writerow(['ID', '用户ID', '接口', '方法', 'IP地址', '用户代理', '状态码', '执行时间(ms)', '创建时间'])
  206. for log in logs:
  207. writer.writerow([
  208. log.id,
  209. log.user_id or '',
  210. log.endpoint,
  211. log.method,
  212. log.ip_address or '',
  213. log.user_agent or '',
  214. log.response_code or '',
  215. log.execution_time or '',
  216. log.created_at.isoformat() if log.created_at else ''
  217. ])
  218. output.seek(0)
  219. return StreamingResponse(
  220. iter([output.getvalue()]),
  221. media_type="text/csv",
  222. headers={"Content-Disposition": f"attachment; filename=logs_{beijing_now().strftime('%Y%m%d_%H%M%S')}.csv"}
  223. )
  224. @router.get("/export/excel")
  225. async def export_logs(
  226. request: Request,
  227. keyword: str = Query(None),
  228. user_id: int = Query(None),
  229. endpoint: str = Query(None),
  230. method: str = Query(None),
  231. response_code: int = Query(None),
  232. start_time: str = Query(None),
  233. end_time: str = Query(None),
  234. db: Session = Depends(get_db),
  235. current_admin = Depends(get_current_admin)
  236. ):
  237. """导出日志为Excel"""
  238. # 记录操作日志(在调用CSV导出前记录,因为CSV导出函数中也会记录)
  239. from app.utils.history import create_operation_log
  240. client_ip = request.client.host if request.client else None
  241. # 统计导出数量
  242. query = db.query(APILog)
  243. if keyword:
  244. query = query.filter(APILog.endpoint.like(f"%{keyword}%"))
  245. if user_id is not None:
  246. query = query.filter(APILog.user_id == user_id)
  247. if endpoint:
  248. query = query.filter(APILog.endpoint.like(f"%{endpoint}%"))
  249. if method:
  250. query = query.filter(APILog.method == method)
  251. if response_code is not None:
  252. query = query.filter(APILog.response_code == response_code)
  253. if start_time:
  254. try:
  255. start_dt = datetime.fromisoformat(start_time.replace('Z', '+00:00'))
  256. query = query.filter(APILog.created_at >= start_dt)
  257. except:
  258. pass
  259. if end_time:
  260. try:
  261. end_dt = datetime.fromisoformat(end_time.replace('Z', '+00:00'))
  262. query = query.filter(APILog.created_at <= end_dt)
  263. except:
  264. pass
  265. export_count = query.count()
  266. create_operation_log(
  267. db=db,
  268. admin_id=current_admin.id,
  269. module="日志管理",
  270. action="导出日志Excel",
  271. description=f"导出API日志为Excel,共导出{export_count}条日志记录",
  272. new_data={
  273. "export_count": export_count,
  274. "export_format": "Excel",
  275. "filters": {
  276. "keyword": keyword,
  277. "user_id": user_id,
  278. "endpoint": endpoint,
  279. "method": method,
  280. "response_code": response_code,
  281. "start_time": start_time,
  282. "end_time": end_time
  283. }
  284. },
  285. ip_address=client_ip
  286. )
  287. db.commit()
  288. # 暂时使用CSV格式,如果需要Excel格式可以安装openpyxl
  289. return await export_logs_csv(
  290. keyword=keyword,
  291. user_id=user_id,
  292. endpoint=endpoint,
  293. method=method,
  294. response_code=response_code,
  295. start_time=start_time,
  296. end_time=end_time,
  297. db=db,
  298. current_admin=current_admin
  299. )