admin_business.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. from datetime import datetime
  2. from typing import Annotated, Any
  3. from fastapi import APIRouter, Depends, Request
  4. from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
  5. from pydantic import BaseModel, Field
  6. from app.api.responses import success_response
  7. from app.core.errors import AppError
  8. from app.domains.attribution.service import AttributionService
  9. from app.domains.catalog.service import ProductCatalogService
  10. from app.domains.enrollment.service import EnrollmentService
  11. from app.domains.identity.models import AdminUser
  12. from app.domains.identity.service import AdminAuthService
  13. admin_business_bearer = HTTPBearer(auto_error=False)
  14. class RoleUpdateRequest(BaseModel):
  15. permissions: list[str] = Field(default_factory=list, max_length=50)
  16. data_scope: str = Field(min_length=2, max_length=32)
  17. class AdminUserCreateRequest(BaseModel):
  18. username: str = Field(min_length=3, max_length=32)
  19. password: str = Field(min_length=8, max_length=128)
  20. display_name: str = Field(min_length=1, max_length=64)
  21. role_code: str = Field(min_length=2, max_length=64)
  22. class AdminUserUpdateRequest(BaseModel):
  23. display_name: str = Field(min_length=1, max_length=64)
  24. status: str = Field(min_length=2, max_length=32)
  25. role_code: str = Field(min_length=2, max_length=64)
  26. class PasswordResetRequest(BaseModel):
  27. new_password: str = Field(min_length=8, max_length=128)
  28. class UserStatusRequest(BaseModel):
  29. status: str = Field(min_length=2, max_length=32)
  30. class ProductStatusRequest(BaseModel):
  31. status: str = Field(pattern="^(ACTIVE|INACTIVE)$")
  32. class ProductUpdateRequest(BaseModel):
  33. name: str = Field(min_length=1, max_length=128)
  34. category: str = Field(pattern="^(MEDICAL|ACCIDENT)$")
  35. summary: str = Field(min_length=1, max_length=500)
  36. class ProductVersionCreateRequest(BaseModel):
  37. version_no: str = Field(min_length=1, max_length=32)
  38. effective_from: datetime
  39. source_version_id: str | None = Field(default=None, max_length=26)
  40. class ProductPlanRequest(BaseModel):
  41. id: str | None = Field(default=None, max_length=26)
  42. code: str = Field(min_length=1, max_length=32)
  43. name: str = Field(min_length=1, max_length=128)
  44. summary: str = Field(default="", max_length=500)
  45. status: str = Field(pattern="^(ACTIVE|INACTIVE)$")
  46. premium_cents: int = Field(ge=0, le=100_000_000)
  47. coverage_amount_cents: int = Field(ge=0, le=100_000_000_000)
  48. min_age: int = Field(ge=0, le=120)
  49. max_age: int = Field(ge=0, le=120)
  50. class ProductCreateRequest(BaseModel):
  51. product_code: str = Field(min_length=3, max_length=32)
  52. name: str = Field(min_length=1, max_length=128)
  53. category: str = Field(pattern="^(MEDICAL|ACCIDENT)$")
  54. summary: str = Field(min_length=1, max_length=500)
  55. version_no: str = Field(default="1.0.0", min_length=1, max_length=32)
  56. effective_from: datetime
  57. rule_version: str = Field(default="eligibility-v1", min_length=1, max_length=32)
  58. rate_version: str = Field(default="rate-v1", min_length=1, max_length=32)
  59. terms_summary: str = Field(default="", max_length=5000)
  60. plans: list[ProductPlanRequest] = Field(min_length=1, max_length=12)
  61. class ProductVersionUpdateRequest(BaseModel):
  62. effective_from: datetime
  63. effective_to: datetime | None = None
  64. rule_version: str = Field(min_length=1, max_length=32)
  65. rate_version: str = Field(min_length=1, max_length=32)
  66. terms_summary: str = Field(default="", max_length=5000)
  67. plans: list[ProductPlanRequest] = Field(min_length=1, max_length=12)
  68. def create_admin_business_router(
  69. service: EnrollmentService,
  70. auth_service: AdminAuthService,
  71. catalog_service: ProductCatalogService,
  72. attribution_service: AttributionService,
  73. ) -> APIRouter:
  74. router = APIRouter(tags=["admin-business"])
  75. def current_admin(
  76. credentials: Annotated[
  77. HTTPAuthorizationCredentials | None,
  78. Depends(admin_business_bearer),
  79. ],
  80. ) -> AdminUser:
  81. if credentials is None:
  82. raise AppError("AUTH_REQUIRED", "请先登录后台", 401)
  83. return auth_service.authenticate_access(credentials.credentials)
  84. def require_read(user: AdminUser, permission: str) -> None:
  85. if "*" not in user.permissions and permission not in user.permissions:
  86. raise AppError("PERMISSION_DENIED", "当前角色无权访问该数据", 403)
  87. def scoped_orders(user: AdminUser) -> dict[str, Any]:
  88. result = service.list_orders()
  89. if user.data_scope == "SELF":
  90. allowed = attribution_service.order_ids(user.id)
  91. items = [item for item in result["items"] if item["order_id"] in allowed]
  92. result = {"items": items, "total": len(items)}
  93. if user.data_scope != "ALL":
  94. result = {
  95. "items": [_mask_order(item) for item in result["items"]],
  96. "total": result["total"],
  97. }
  98. return result
  99. def scoped_policies(user: AdminUser) -> dict[str, Any]:
  100. result = service.list_all_policies()
  101. if user.data_scope == "SELF":
  102. allowed = attribution_service.order_ids(user.id)
  103. items = [item for item in result["items"] if item["order_id"] in allowed]
  104. result = {"items": items, "total": len(items)}
  105. if user.data_scope != "ALL":
  106. result = {
  107. "items": [_mask_policy(item) for item in result["items"]],
  108. "total": result["total"],
  109. }
  110. return result
  111. @router.get("/admin/dashboard")
  112. def dashboard(
  113. request: Request,
  114. user: Annotated[AdminUser, Depends(current_admin)],
  115. ) -> dict[str, Any]:
  116. require_read(user, "dashboard:read")
  117. orders = scoped_orders(user)
  118. policies = scoped_policies(user)
  119. active_policies = [item for item in policies["items"] if item["status"] == "ACTIVE"]
  120. return success_response(
  121. request,
  122. {
  123. "order_count": orders["total"],
  124. "policy_count": len(active_policies),
  125. "premium_cents": sum(int(item["premium_cents"]) for item in active_policies),
  126. },
  127. )
  128. @router.get("/admin/orders")
  129. def orders(
  130. request: Request,
  131. user: Annotated[AdminUser, Depends(current_admin)],
  132. ) -> dict[str, Any]:
  133. require_read(user, "order:read")
  134. return success_response(request, scoped_orders(user))
  135. @router.get("/admin/orders/{order_id}")
  136. def order_detail(
  137. order_id: str,
  138. request: Request,
  139. user: Annotated[AdminUser, Depends(current_admin)],
  140. ) -> dict[str, Any]:
  141. require_read(user, "order:read")
  142. item = next(
  143. (order for order in scoped_orders(user)["items"] if order["order_id"] == order_id),
  144. None,
  145. )
  146. if item is None:
  147. raise AppError("ORDER_NOT_FOUND", "未找到订单或无权查看", 404)
  148. return success_response(request, item)
  149. @router.get("/admin/policies")
  150. def policies(
  151. request: Request,
  152. user: Annotated[AdminUser, Depends(current_admin)],
  153. ) -> dict[str, Any]:
  154. require_read(user, "policy:read")
  155. return success_response(request, scoped_policies(user))
  156. @router.get("/admin/policies/{policy_id}")
  157. def policy_detail(
  158. policy_id: str,
  159. request: Request,
  160. user: Annotated[AdminUser, Depends(current_admin)],
  161. ) -> dict[str, Any]:
  162. require_read(user, "policy:read")
  163. item = next(
  164. (
  165. policy
  166. for policy in scoped_policies(user)["items"]
  167. if policy["policy_id"] == policy_id
  168. ),
  169. None,
  170. )
  171. if item is None:
  172. raise AppError("POLICY_NOT_FOUND", "未找到保单或无权查看", 404)
  173. return success_response(request, item)
  174. @router.get("/admin/products")
  175. def products(
  176. request: Request,
  177. user: Annotated[AdminUser, Depends(current_admin)],
  178. ) -> dict[str, Any]:
  179. require_read(user, "product:read")
  180. return success_response(request, catalog_service.list_managed())
  181. @router.post("/admin/products", status_code=201)
  182. def create_product(
  183. payload: ProductCreateRequest,
  184. request: Request,
  185. user: Annotated[AdminUser, Depends(current_admin)],
  186. ) -> dict[str, Any]:
  187. require_read(user, "product:write")
  188. return success_response(
  189. request,
  190. catalog_service.create_product(
  191. product_code=payload.product_code,
  192. name=payload.name,
  193. category=payload.category,
  194. summary=payload.summary,
  195. version_no=payload.version_no,
  196. effective_from=payload.effective_from,
  197. rule_version=payload.rule_version,
  198. rate_version=payload.rate_version,
  199. terms_summary=payload.terms_summary,
  200. plans=[plan.model_dump() for plan in payload.plans],
  201. actor_id=user.id,
  202. actor_name=user.display_name,
  203. ),
  204. )
  205. @router.put("/admin/products/{product_id}/status")
  206. def update_product_status(
  207. product_id: str,
  208. payload: ProductStatusRequest,
  209. request: Request,
  210. user: Annotated[AdminUser, Depends(current_admin)],
  211. ) -> dict[str, Any]:
  212. require_read(user, "product:write")
  213. return success_response(
  214. request,
  215. catalog_service.update_product_status(
  216. product_id,
  217. payload.status,
  218. actor_id=user.id,
  219. actor_name=user.display_name,
  220. ),
  221. )
  222. @router.put("/admin/products/{product_id}")
  223. def update_product(
  224. product_id: str,
  225. payload: ProductUpdateRequest,
  226. request: Request,
  227. user: Annotated[AdminUser, Depends(current_admin)],
  228. ) -> dict[str, Any]:
  229. require_read(user, "product:write")
  230. return success_response(
  231. request,
  232. catalog_service.update_product(
  233. product_id,
  234. name=payload.name,
  235. category=payload.category,
  236. summary=payload.summary,
  237. actor_id=user.id,
  238. actor_name=user.display_name,
  239. ),
  240. )
  241. @router.post("/admin/products/{product_id}/versions", status_code=201)
  242. def create_product_version(
  243. product_id: str,
  244. payload: ProductVersionCreateRequest,
  245. request: Request,
  246. user: Annotated[AdminUser, Depends(current_admin)],
  247. ) -> dict[str, Any]:
  248. require_read(user, "product:write")
  249. return success_response(
  250. request,
  251. catalog_service.create_draft_version(
  252. product_id,
  253. version_no=payload.version_no,
  254. effective_from=payload.effective_from,
  255. source_version_id=payload.source_version_id,
  256. actor_id=user.id,
  257. actor_name=user.display_name,
  258. ),
  259. )
  260. @router.put("/admin/products/{product_id}/versions/{version_id}")
  261. def update_product_version(
  262. product_id: str,
  263. version_id: str,
  264. payload: ProductVersionUpdateRequest,
  265. request: Request,
  266. user: Annotated[AdminUser, Depends(current_admin)],
  267. ) -> dict[str, Any]:
  268. require_read(user, "product:write")
  269. return success_response(
  270. request,
  271. catalog_service.update_draft_version(
  272. product_id,
  273. version_id,
  274. effective_from=payload.effective_from,
  275. effective_to=payload.effective_to,
  276. rule_version=payload.rule_version,
  277. rate_version=payload.rate_version,
  278. terms_summary=payload.terms_summary,
  279. plans=[plan.model_dump() for plan in payload.plans],
  280. actor_id=user.id,
  281. actor_name=user.display_name,
  282. ),
  283. )
  284. @router.post("/admin/products/{product_id}/versions/{version_id}/publish")
  285. def publish_product_version(
  286. product_id: str,
  287. version_id: str,
  288. request: Request,
  289. user: Annotated[AdminUser, Depends(current_admin)],
  290. ) -> dict[str, Any]:
  291. require_read(user, "product:write")
  292. return success_response(
  293. request,
  294. catalog_service.publish_version(
  295. product_id,
  296. version_id,
  297. actor_id=user.id,
  298. actor_name=user.display_name,
  299. ),
  300. )
  301. @router.post("/admin/products/{product_id}/versions/{version_id}/retire")
  302. def retire_product_version(
  303. product_id: str,
  304. version_id: str,
  305. request: Request,
  306. user: Annotated[AdminUser, Depends(current_admin)],
  307. ) -> dict[str, Any]:
  308. require_read(user, "product:write")
  309. return success_response(
  310. request,
  311. catalog_service.retire_version(
  312. product_id,
  313. version_id,
  314. actor_id=user.id,
  315. actor_name=user.display_name,
  316. ),
  317. )
  318. @router.delete("/admin/products/{product_id}/versions/{version_id}")
  319. def delete_product_version(
  320. product_id: str,
  321. version_id: str,
  322. request: Request,
  323. user: Annotated[AdminUser, Depends(current_admin)],
  324. ) -> dict[str, Any]:
  325. require_read(user, "product:write")
  326. return success_response(
  327. request,
  328. catalog_service.delete_draft_version(
  329. product_id,
  330. version_id,
  331. actor_id=user.id,
  332. actor_name=user.display_name,
  333. ),
  334. )
  335. @router.get("/admin/products/{product_id}/logs")
  336. def product_change_logs(
  337. product_id: str,
  338. request: Request,
  339. user: Annotated[AdminUser, Depends(current_admin)],
  340. ) -> dict[str, Any]:
  341. require_read(user, "product:read")
  342. return success_response(
  343. request,
  344. catalog_service.list_change_logs(product_id),
  345. )
  346. @router.get("/admin/attribution/overview")
  347. def attribution_overview(
  348. request: Request,
  349. user: Annotated[AdminUser, Depends(current_admin)],
  350. ) -> dict[str, Any]:
  351. require_read(user, "attribution:read")
  352. owner_id = user.id if user.data_scope == "SELF" else None
  353. return success_response(
  354. request,
  355. attribution_service.performance(owner_id),
  356. )
  357. @router.get("/admin/attribution/codes")
  358. def attribution_codes(
  359. request: Request,
  360. user: Annotated[AdminUser, Depends(current_admin)],
  361. ) -> dict[str, Any]:
  362. require_read(user, "attribution:read")
  363. owner_id = user.id if user.data_scope == "SELF" else None
  364. return success_response(
  365. request,
  366. attribution_service.list_codes(owner_id),
  367. )
  368. @router.get("/admin/roles")
  369. def roles(
  370. request: Request,
  371. user: Annotated[AdminUser, Depends(current_admin)],
  372. ) -> dict[str, Any]:
  373. require_read(user, "role:read")
  374. return success_response(request, auth_service.list_roles())
  375. @router.put("/admin/roles/{role_code}")
  376. def update_role(
  377. role_code: str,
  378. payload: RoleUpdateRequest,
  379. request: Request,
  380. user: Annotated[AdminUser, Depends(current_admin)],
  381. ) -> dict[str, Any]:
  382. require_read(user, "role:write")
  383. return success_response(
  384. request,
  385. auth_service.update_role(
  386. role_code,
  387. permissions=payload.permissions,
  388. data_scope=payload.data_scope,
  389. ),
  390. )
  391. @router.get("/admin/users")
  392. def managed_users(
  393. request: Request,
  394. user: Annotated[AdminUser, Depends(current_admin)],
  395. ) -> dict[str, Any]:
  396. require_read(user, "user:read")
  397. return success_response(request, auth_service.list_managed_users())
  398. @router.post("/admin/users/admins", status_code=201)
  399. def create_admin_user(
  400. payload: AdminUserCreateRequest,
  401. request: Request,
  402. user: Annotated[AdminUser, Depends(current_admin)],
  403. ) -> dict[str, Any]:
  404. require_read(user, "user:write")
  405. return success_response(
  406. request,
  407. auth_service.create_admin_user(
  408. username=payload.username,
  409. password=payload.password,
  410. display_name=payload.display_name,
  411. role_code=payload.role_code,
  412. ),
  413. )
  414. @router.put("/admin/users/admins/{user_id}")
  415. def update_admin_user(
  416. user_id: str,
  417. payload: AdminUserUpdateRequest,
  418. request: Request,
  419. user: Annotated[AdminUser, Depends(current_admin)],
  420. ) -> dict[str, Any]:
  421. require_read(user, "user:write")
  422. return success_response(
  423. request,
  424. auth_service.update_admin_user(
  425. user,
  426. user_id,
  427. display_name=payload.display_name,
  428. status=payload.status,
  429. role_code=payload.role_code,
  430. ),
  431. )
  432. @router.post("/admin/users/admins/{user_id}/password")
  433. def reset_admin_password(
  434. user_id: str,
  435. payload: PasswordResetRequest,
  436. request: Request,
  437. user: Annotated[AdminUser, Depends(current_admin)],
  438. ) -> dict[str, Any]:
  439. require_read(user, "user:write")
  440. return success_response(
  441. request,
  442. auth_service.reset_admin_password(
  443. user_id,
  444. new_password=payload.new_password,
  445. ),
  446. )
  447. @router.put("/admin/users/customers/{user_id}/status")
  448. def update_customer_status(
  449. user_id: str,
  450. payload: UserStatusRequest,
  451. request: Request,
  452. user: Annotated[AdminUser, Depends(current_admin)],
  453. ) -> dict[str, Any]:
  454. require_read(user, "user:write")
  455. return success_response(
  456. request,
  457. auth_service.update_customer_status(
  458. user_id,
  459. status=payload.status,
  460. ),
  461. )
  462. return router
  463. def _mask_secret(value: str | None) -> str:
  464. if not value:
  465. return ""
  466. if len(value) == 11:
  467. return f"{value[:3]}****{value[-4:]}"
  468. if len(value) >= 10:
  469. return f"{value[:6]}********{value[-4:]}"
  470. return value
  471. def _mask_person(person: dict[str, Any]) -> dict[str, Any]:
  472. masked = dict(person)
  473. if "id_no" in masked:
  474. masked["id_no"] = _mask_secret(str(masked["id_no"]))
  475. return masked
  476. def _mask_order(item: dict[str, Any]) -> dict[str, Any]:
  477. masked = dict(item)
  478. masked["applicant"] = _mask_person(dict(item.get("applicant", {})))
  479. masked["insured"] = _mask_person(dict(item.get("insured", {})))
  480. contact = dict(item.get("contact", {}))
  481. if "mobile" in contact:
  482. contact["mobile"] = _mask_secret(str(contact["mobile"]))
  483. masked["contact"] = contact
  484. return masked
  485. def _mask_policy(item: dict[str, Any]) -> dict[str, Any]:
  486. masked = dict(item)
  487. masked["applicant"] = _mask_person(dict(item.get("applicant", {})))
  488. masked["insured"] = _mask_person(dict(item.get("insured", {})))
  489. return masked