admin.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. """管理端 API:工作台、台账办理、配置、用户/角色、审计、附件上传。"""
  2. from typing import Annotated, Any, Dict, List, Optional
  3. from fastapi import APIRouter, Depends, File, Form, Query, UploadFile
  4. from sqlalchemy.orm import Session
  5. from app.core import storage
  6. from app.core.database import get_db
  7. from app.core.deps import get_current_user, require_menu
  8. from app.core.response import AppHTTPError, success_response
  9. from app.core.security import hash_password
  10. from app.core.storage import StorageError
  11. from app.crud import attachment as attachment_crud
  12. from app.crud import menu as menu_crud
  13. from app.crud import oper_log as oper_log_crud
  14. from app.crud import role as role_crud
  15. from app.crud import user as user_crud
  16. from app.models.system import SysUser
  17. from app.schemas.business import HotlineUpdateBody, QrcodeUpdateBody, StatusUpdateBody
  18. from app.schemas.system import (
  19. ResetPasswordBody,
  20. RoleMenusBody,
  21. UserCreateBody,
  22. UserUpdateBody,
  23. )
  24. from app.services import admin_ledger_service, auth_service, config_service, dashboard_service
  25. router = APIRouter(prefix="/web/admin", tags=["admin"])
  26. def _iso(dt) -> Optional[str]:
  27. if not dt:
  28. return None
  29. return dt.isoformat(sep=" ", timespec="seconds")
  30. def _user_to_dict(db: Session, user: SysUser) -> Dict[str, Any]:
  31. role = user_crud.get_role_by_id(db, user.role_id)
  32. return {
  33. "id": user.id,
  34. "username": user.username,
  35. "displayName": user.display_name,
  36. "roleId": user.role_id,
  37. "roleCode": role.role_code if role else "",
  38. "roleName": role.role_name if role else "",
  39. "status": user.status,
  40. "createTime": _iso(user.create_time),
  41. "updateTime": _iso(user.update_time),
  42. }
  43. def _role_to_dict(role) -> Dict[str, Any]:
  44. return {
  45. "id": role.id,
  46. "roleCode": role.role_code,
  47. "roleName": role.role_name,
  48. "remark": role.remark or "",
  49. }
  50. def _oper_log_to_dict(row) -> Dict[str, Any]:
  51. return {
  52. "id": row.id,
  53. "userId": row.user_id,
  54. "username": row.username,
  55. "module": row.module,
  56. "action": row.action,
  57. "detail": row.detail,
  58. "ip": row.ip,
  59. "createTime": _iso(row.create_time),
  60. }
  61. @router.get("/dashboard")
  62. def dashboard(
  63. db: Annotated[Session, Depends(get_db)],
  64. _user: Annotated[SysUser, Depends(get_current_user)],
  65. ):
  66. return success_response(dashboard_service.get_dashboard(db))
  67. @router.get("/feedbacks")
  68. def list_feedbacks(
  69. db: Annotated[Session, Depends(get_db)],
  70. _user: Annotated[SysUser, Depends(get_current_user)],
  71. status: Optional[str] = Query(None),
  72. fbType: Optional[str] = Query(None),
  73. keyword: Optional[str] = Query(None),
  74. page: int = Query(1, ge=1),
  75. pageSize: int = Query(10, ge=1, le=100),
  76. ):
  77. return success_response(
  78. admin_ledger_service.list_feedbacks_page(
  79. db,
  80. status=status,
  81. fb_type=fbType,
  82. keyword=keyword,
  83. page=page,
  84. page_size=pageSize,
  85. )
  86. )
  87. @router.get("/feedbacks/{feedback_id}")
  88. def get_feedback(
  89. feedback_id: str,
  90. db: Annotated[Session, Depends(get_db)],
  91. _user: Annotated[SysUser, Depends(get_current_user)],
  92. ):
  93. return success_response(
  94. admin_ledger_service.get_feedback_detail(db, feedback_id)
  95. )
  96. @router.put("/feedbacks/{feedback_id}/status")
  97. def update_feedback_status(
  98. feedback_id: str,
  99. body: StatusUpdateBody,
  100. db: Annotated[Session, Depends(get_db)],
  101. user: Annotated[SysUser, Depends(require_menu("feedback:handle"))],
  102. ):
  103. return success_response(
  104. admin_ledger_service.update_feedback_status(
  105. db,
  106. user,
  107. feedback_id,
  108. status=body.status,
  109. handle_remark=body.handle_remark,
  110. )
  111. )
  112. @router.get("/suggestions")
  113. def list_suggestions(
  114. db: Annotated[Session, Depends(get_db)],
  115. _user: Annotated[SysUser, Depends(get_current_user)],
  116. status: Optional[str] = Query(None),
  117. keyword: Optional[str] = Query(None),
  118. page: int = Query(1, ge=1),
  119. pageSize: int = Query(10, ge=1, le=100),
  120. ):
  121. return success_response(
  122. admin_ledger_service.list_suggestions_page(
  123. db,
  124. status=status,
  125. keyword=keyword,
  126. page=page,
  127. page_size=pageSize,
  128. )
  129. )
  130. @router.get("/suggestions/{suggestion_id}")
  131. def get_suggestion(
  132. suggestion_id: str,
  133. db: Annotated[Session, Depends(get_db)],
  134. _user: Annotated[SysUser, Depends(get_current_user)],
  135. ):
  136. return success_response(
  137. admin_ledger_service.get_suggestion_detail(db, suggestion_id)
  138. )
  139. @router.put("/suggestions/{suggestion_id}/status")
  140. def update_suggestion_status(
  141. suggestion_id: str,
  142. body: StatusUpdateBody,
  143. db: Annotated[Session, Depends(get_db)],
  144. user: Annotated[SysUser, Depends(require_menu("suggestion:handle"))],
  145. ):
  146. return success_response(
  147. admin_ledger_service.update_suggestion_status(
  148. db,
  149. user,
  150. suggestion_id,
  151. status=body.status,
  152. handle_remark=body.handle_remark,
  153. )
  154. )
  155. @router.get("/hotline")
  156. def get_hotline(
  157. db: Annotated[Session, Depends(get_db)],
  158. _user: Annotated[SysUser, Depends(get_current_user)],
  159. ):
  160. return success_response(config_service.get_hotline_payload(db))
  161. @router.put("/hotline")
  162. def put_hotline(
  163. body: HotlineUpdateBody,
  164. db: Annotated[Session, Depends(get_db)],
  165. user: Annotated[SysUser, Depends(require_menu("hotline:write"))],
  166. ):
  167. return success_response(
  168. config_service.update_hotline(
  169. db,
  170. user,
  171. phone=body.phone,
  172. email=body.email,
  173. address=body.address,
  174. work_time=body.work_time,
  175. )
  176. )
  177. @router.get("/qrcode")
  178. def get_qrcode(
  179. db: Annotated[Session, Depends(get_db)],
  180. _user: Annotated[SysUser, Depends(get_current_user)],
  181. ):
  182. return success_response(config_service.get_qrcode_payload(db))
  183. @router.put("/qrcode")
  184. def put_qrcode(
  185. body: QrcodeUpdateBody,
  186. db: Annotated[Session, Depends(get_db)],
  187. user: Annotated[SysUser, Depends(require_menu("qrcode:write"))],
  188. ):
  189. return success_response(
  190. config_service.update_qrcode(
  191. db,
  192. user,
  193. land_url=body.land_url,
  194. title=body.title,
  195. image_url=body.image_url,
  196. )
  197. )
  198. @router.post("/attachment/upload")
  199. async def admin_upload(
  200. db: Annotated[Session, Depends(get_db)],
  201. _user: Annotated[SysUser, Depends(get_current_user)],
  202. file: UploadFile = File(...),
  203. folder: Optional[str] = Form(""),
  204. ):
  205. if not file or not file.filename:
  206. raise AppHTTPError(400, "请选择文件")
  207. content = await file.read()
  208. try:
  209. stored = storage.upload_file(content, file.filename, folder or "")
  210. except StorageError as exc:
  211. raise AppHTTPError(400, exc.message) from exc
  212. row = attachment_crud.create_attachment(
  213. db,
  214. file_name=file.filename,
  215. object_key=stored["objectKey"],
  216. url=stored["url"],
  217. size=len(content),
  218. )
  219. return success_response(
  220. {
  221. "id": row.id,
  222. "url": row.url,
  223. "fileName": row.file_name,
  224. "size": row.size,
  225. }
  226. )
  227. @router.get("/users")
  228. def list_users(
  229. db: Annotated[Session, Depends(get_db)],
  230. _user: Annotated[SysUser, Depends(require_menu("user:manage"))],
  231. ):
  232. rows = user_crud.list_users(db)
  233. return success_response([_user_to_dict(db, u) for u in rows])
  234. @router.post("/users")
  235. def create_user(
  236. body: UserCreateBody,
  237. db: Annotated[Session, Depends(get_db)],
  238. _user: Annotated[SysUser, Depends(require_menu("user:manage"))],
  239. ):
  240. username = (body.username or "").strip()
  241. password = body.password or ""
  242. display_name = (body.display_name or "").strip()
  243. if not username or not password or not display_name:
  244. raise AppHTTPError(400, "用户名、密码、显示名不能为空")
  245. if user_crud.get_user_by_username(db, username):
  246. raise AppHTTPError(400, "用户名已存在")
  247. role = user_crud.get_role_by_id(db, body.role_id)
  248. if not role:
  249. raise AppHTTPError(400, "角色不存在")
  250. row = user_crud.create_user(
  251. db,
  252. username=username,
  253. password_hash=hash_password(password),
  254. display_name=display_name,
  255. role_id=role.id,
  256. )
  257. return success_response(_user_to_dict(db, row))
  258. @router.put("/users/{user_id}")
  259. def update_user(
  260. user_id: str,
  261. body: UserUpdateBody,
  262. db: Annotated[Session, Depends(get_db)],
  263. _user: Annotated[SysUser, Depends(require_menu("user:manage"))],
  264. ):
  265. row = user_crud.get_user_by_id(db, user_id)
  266. if not row:
  267. raise AppHTTPError(400, "用户不存在")
  268. role_id = body.role_id
  269. if role_id is not None:
  270. role = user_crud.get_role_by_id(db, role_id)
  271. if not role:
  272. raise AppHTTPError(400, "角色不存在")
  273. status = body.status
  274. if status is not None and status not in ("0", "1"):
  275. raise AppHTTPError(400, "状态无效")
  276. display_name = body.display_name
  277. if display_name is not None:
  278. display_name = display_name.strip()
  279. if not display_name:
  280. raise AppHTTPError(400, "显示名不能为空")
  281. updated = user_crud.update_user(
  282. db,
  283. row,
  284. display_name=display_name,
  285. role_id=role_id,
  286. status=status,
  287. )
  288. return success_response(_user_to_dict(db, updated))
  289. @router.put("/users/{user_id}/reset-password")
  290. def reset_user_password(
  291. user_id: str,
  292. body: ResetPasswordBody,
  293. db: Annotated[Session, Depends(get_db)],
  294. _user: Annotated[SysUser, Depends(require_menu("user:manage"))],
  295. ):
  296. row = user_crud.get_user_by_id(db, user_id)
  297. if not row:
  298. raise AppHTTPError(400, "用户不存在")
  299. password = body.password or ""
  300. if not password:
  301. raise AppHTTPError(400, "密码不能为空")
  302. user_crud.update_user(db, row, password_hash=hash_password(password))
  303. return success_response({"id": user_id})
  304. @router.get("/roles")
  305. def list_roles(
  306. db: Annotated[Session, Depends(get_db)],
  307. _user: Annotated[SysUser, Depends(get_current_user)],
  308. ):
  309. return success_response([_role_to_dict(r) for r in role_crud.list_roles(db)])
  310. @router.get("/menus/tree")
  311. def menus_tree(
  312. db: Annotated[Session, Depends(get_db)],
  313. _user: Annotated[SysUser, Depends(get_current_user)],
  314. ):
  315. menus = menu_crud.list_all_menus(db)
  316. return success_response(auth_service.build_menu_tree(menus))
  317. @router.get("/roles/{role_id}/menus")
  318. def get_role_menus(
  319. role_id: str,
  320. db: Annotated[Session, Depends(get_db)],
  321. _user: Annotated[SysUser, Depends(get_current_user)],
  322. ):
  323. role = role_crud.get_role_by_id(db, role_id)
  324. if not role:
  325. raise AppHTTPError(400, "角色不存在")
  326. return success_response({"menuIds": role_crud.list_menu_ids_for_role(db, role_id)})
  327. @router.put("/roles/{role_id}/menus")
  328. def put_role_menus(
  329. role_id: str,
  330. body: RoleMenusBody,
  331. db: Annotated[Session, Depends(get_db)],
  332. _user: Annotated[SysUser, Depends(require_menu("role:grant"))],
  333. ):
  334. role = role_crud.get_role_by_id(db, role_id)
  335. if not role:
  336. raise AppHTTPError(400, "角色不存在")
  337. menu_ids: List[str] = list(body.menu_ids or [])
  338. if menu_ids:
  339. valid_ids = {m.id for m in menu_crud.list_all_menus(db)}
  340. invalid = [mid for mid in menu_ids if mid not in valid_ids]
  341. if invalid:
  342. raise AppHTTPError(400, "存在无效菜单")
  343. role_crud.replace_role_menus(db, role_id, menu_ids)
  344. return success_response({"menuIds": menu_ids})
  345. @router.get("/oper-logs")
  346. def list_oper_logs(
  347. db: Annotated[Session, Depends(get_db)],
  348. _user: Annotated[SysUser, Depends(require_menu("audit"))],
  349. page: int = Query(1, ge=1),
  350. pageSize: int = Query(10, ge=1, le=100),
  351. ):
  352. items, total = oper_log_crud.list_oper_logs(db, page=page, page_size=pageSize)
  353. return success_response(
  354. {
  355. "items": [_oper_log_to_dict(r) for r in items],
  356. "total": total,
  357. "page": page,
  358. "pageSize": pageSize,
  359. }
  360. )