announcements.py 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. """
  2. 公告管理接口
  3. """
  4. from datetime import datetime
  5. from app.utils.timezone import beijing_now
  6. from typing import Optional
  7. from fastapi import APIRouter, Depends, Query, Request
  8. from pydantic import BaseModel
  9. from sqlalchemy import or_
  10. from sqlalchemy.orm import Session
  11. from app.api.deps import get_current_admin
  12. from app.database import get_db
  13. from app.models.announcement import Announcement
  14. router = APIRouter()
  15. class AnnouncementCreate(BaseModel):
  16. """创建公告请求"""
  17. title: str
  18. content: str
  19. priority: int = 0
  20. is_active: bool = True
  21. publish_time: Optional[str] = None # ISO格式时间字符串
  22. @router.get("")
  23. async def get_announcements(
  24. page: int = Query(1, ge=1),
  25. page_size: int = Query(10, ge=1, le=100),
  26. keyword: Optional[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(Announcement).filter(Announcement.is_deleted == 0)
  33. # 关键字搜索:标题或内容模糊匹配
  34. if keyword:
  35. like_pattern = f"%{keyword}%"
  36. query = query.filter(
  37. or_(
  38. Announcement.title.like(like_pattern),
  39. Announcement.content.like(like_pattern),
  40. )
  41. )
  42. # 按发布时间倒序,其次按ID倒序,确保最新的数据在前
  43. # 这里不使用 nullslast,以避免部分数据库方言不支持导致的错误
  44. query = query.order_by(Announcement.publish_time.desc(), Announcement.id.desc())
  45. total = query.count()
  46. announcements = query.offset(offset).limit(page_size).all()
  47. return {
  48. "code": 200,
  49. "msg": "成功",
  50. "data": [
  51. {
  52. "id": a.id,
  53. "title": a.title,
  54. "content": a.content,
  55. "priority": a.priority,
  56. "is_active": bool(a.is_active),
  57. "publish_time": a.publish_time.isoformat() if a.publish_time else None,
  58. "created_at": a.created_at.isoformat() if a.created_at else None,
  59. "updated_at": a.updated_at.isoformat() if a.updated_at else None,
  60. }
  61. for a in announcements
  62. ],
  63. "total": total,
  64. "page": page,
  65. "page_size": page_size,
  66. }
  67. @router.get("/{announcement_id}")
  68. async def get_announcement_detail(
  69. announcement_id: int,
  70. db: Session = Depends(get_db),
  71. current_admin = Depends(get_current_admin)
  72. ):
  73. """获取公告详情"""
  74. announcement = db.query(Announcement).filter(Announcement.id == announcement_id).first()
  75. if not announcement:
  76. return {"code": 404, "msg": "公告不存在"}
  77. return {
  78. "code": 200,
  79. "msg": "成功",
  80. "data": {
  81. "id": announcement.id,
  82. "title": announcement.title,
  83. "content": announcement.content,
  84. "priority": announcement.priority,
  85. "is_active": bool(announcement.is_active),
  86. "publish_time": announcement.publish_time.isoformat() if announcement.publish_time else None,
  87. "created_at": announcement.created_at.isoformat() if announcement.created_at else None,
  88. "updated_at": announcement.updated_at.isoformat() if announcement.updated_at else None
  89. }
  90. }
  91. @router.post("")
  92. async def create_announcement(
  93. req: AnnouncementCreate,
  94. request: Request,
  95. db: Session = Depends(get_db),
  96. current_admin = Depends(get_current_admin)
  97. ):
  98. """创建公告"""
  99. publish_time = beijing_now()
  100. if req.publish_time:
  101. try:
  102. publish_time = datetime.fromisoformat(req.publish_time.replace('Z', '+00:00'))
  103. except:
  104. pass
  105. announcement = Announcement(
  106. title=req.title,
  107. content=req.content,
  108. priority=req.priority,
  109. is_active=1 if req.is_active else 0,
  110. publish_time=publish_time,
  111. created_at=beijing_now(),
  112. updated_at=beijing_now()
  113. )
  114. db.add(announcement)
  115. # 先提交主记录,获取ID
  116. db.flush()
  117. # 创建历史记录
  118. from app.utils.history import create_announcement_history, create_operation_log
  119. create_announcement_history(db, announcement, "INSERT")
  120. # 记录操作日志
  121. client_ip = request.client.host if request.client else None
  122. create_operation_log(
  123. db=db,
  124. admin_id=current_admin.id,
  125. module="公告管理",
  126. action="创建公告",
  127. description=f"创建公告:{req.title}",
  128. new_data={
  129. "title": req.title,
  130. "content": req.content[:100] + "..." if len(req.content) > 100 else req.content,
  131. "priority": req.priority,
  132. "is_active": req.is_active
  133. },
  134. ip_address=client_ip
  135. )
  136. # 一次性提交所有更改
  137. db.commit()
  138. return {"code": 200, "msg": "创建成功", "data": {"id": announcement.id}}
  139. @router.put("/{announcement_id}")
  140. async def update_announcement(
  141. announcement_id: int,
  142. req: AnnouncementCreate,
  143. request: Request,
  144. db: Session = Depends(get_db),
  145. current_admin = Depends(get_current_admin)
  146. ):
  147. """更新公告"""
  148. announcement = db.query(Announcement).filter(Announcement.id == announcement_id).first()
  149. if not announcement:
  150. return {"code": 404, "msg": "公告不存在"}
  151. # 记录更新前的状态
  152. old_data = {
  153. "title": announcement.title,
  154. "content": announcement.content[:100] + "..." if len(announcement.content) > 100 else announcement.content,
  155. "priority": announcement.priority,
  156. "is_active": bool(announcement.is_active)
  157. }
  158. announcement.title = req.title
  159. announcement.content = req.content
  160. announcement.priority = req.priority
  161. announcement.is_active = 1 if req.is_active else 0
  162. if req.publish_time:
  163. try:
  164. announcement.publish_time = datetime.fromisoformat(req.publish_time.replace('Z', '+00:00'))
  165. except:
  166. pass
  167. announcement.updated_at = beijing_now()
  168. # 创建历史记录
  169. from app.utils.history import create_announcement_history, create_operation_log
  170. create_announcement_history(db, announcement, "UPDATE")
  171. # 记录操作日志
  172. client_ip = request.client.host if request.client else None
  173. create_operation_log(
  174. db=db,
  175. admin_id=current_admin.id,
  176. module="公告管理",
  177. action="更新公告",
  178. description=f"更新公告:{announcement.title}",
  179. old_data=old_data,
  180. new_data={
  181. "title": req.title,
  182. "content": req.content[:100] + "..." if len(req.content) > 100 else req.content,
  183. "priority": req.priority,
  184. "is_active": req.is_active
  185. },
  186. ip_address=client_ip
  187. )
  188. # 一次性提交所有更改
  189. db.commit()
  190. return {"code": 200, "msg": "更新成功"}
  191. @router.put("/{announcement_id}/status")
  192. async def toggle_announcement_status(
  193. announcement_id: int,
  194. request: Request,
  195. db: Session = Depends(get_db),
  196. current_admin = Depends(get_current_admin)
  197. ):
  198. """切换公告状态"""
  199. announcement = db.query(Announcement).filter(Announcement.id == announcement_id).first()
  200. if not announcement:
  201. return {"code": 404, "msg": "公告不存在"}
  202. old_active = announcement.is_active
  203. announcement.is_active = 0 if announcement.is_active else 1
  204. announcement.updated_at = beijing_now()
  205. # 创建历史记录
  206. from app.utils.history import create_announcement_history, create_operation_log
  207. create_announcement_history(db, announcement, "UPDATE")
  208. # 记录操作日志
  209. client_ip = request.client.host if request.client else None
  210. status_text = "启用" if announcement.is_active == 1 else "禁用"
  211. create_operation_log(
  212. db=db,
  213. admin_id=current_admin.id,
  214. module="公告管理",
  215. action="切换公告状态",
  216. description=f"{status_text}公告:{announcement.title}",
  217. old_data={"is_active": bool(old_active)},
  218. new_data={"is_active": bool(announcement.is_active)},
  219. ip_address=client_ip
  220. )
  221. # 一次性提交所有更改
  222. db.commit()
  223. return {"code": 200, "msg": "状态更新成功", "data": {"is_active": bool(announcement.is_active)}}
  224. @router.delete("/{announcement_id}")
  225. async def delete_announcement(
  226. announcement_id: int,
  227. request: Request,
  228. db: Session = Depends(get_db),
  229. current_admin = Depends(get_current_admin)
  230. ):
  231. """删除公告(软删除)"""
  232. announcement = db.query(Announcement).filter(Announcement.id == announcement_id, Announcement.is_deleted == 0).first()
  233. if not announcement:
  234. return {"code": 404, "msg": "公告不存在"}
  235. announcement.is_deleted = 1
  236. announcement.updated_at = beijing_now()
  237. # 记录操作日志(删除公告没有历史记录,因为它本身就是历史记录)
  238. from app.utils.history import create_operation_log
  239. client_ip = request.client.host if request.client else None
  240. create_operation_log(
  241. db=db,
  242. admin_id=current_admin.id,
  243. module="公告管理",
  244. action="删除公告",
  245. description=f"删除公告:{announcement.title}",
  246. old_data={
  247. "title": announcement.title,
  248. "content": announcement.content[:100] + "..." if len(announcement.content) > 100 else announcement.content,
  249. "priority": announcement.priority,
  250. "is_active": bool(announcement.is_active)
  251. },
  252. ip_address=client_ip
  253. )
  254. # 一次性提交所有更改
  255. db.commit()
  256. return {"code": 200, "msg": "删除成功"}