diff --git a/accounts/api/authorize.py b/accounts/api/authorize.py index b278812..e5ac0e8 100644 --- a/accounts/api/authorize.py +++ b/accounts/api/authorize.py @@ -1,21 +1,45 @@ +# ✅ 授权管理接口:manager 给 user 授权网站 + 普通用户申请接口 from ninja import Router, Schema, Query from pydantic import Field -from typing import List +from typing import List, Optional from django.shortcuts import get_object_or_404 +from django.db import models from accounts.models import User from websites.models import Website from utils.auth import jwt_auth -from utils.permissions import manager_required +from utils.permissions import manager_required, login_required router = Router(tags=["授权管理"]) +# ========================= +# 模型(授权申请) +# ========================= +class WebsiteAccessRequest(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + website = models.ForeignKey(Website, on_delete=models.CASCADE) + status = models.CharField( + max_length=20, + choices=[("pending", "待审批"), ("approved", "已通过"), ("rejected", "已拒绝")], + default="pending" + ) + reason = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) +# ========================= +# 请求结构 +# ========================= class AuthorizeIn(Schema): user_id: int = Field(..., description="被授权的用户ID") website_ids: List[int] = Field(..., description="要授权的网站ID列表") +class AccessRequestIn(Schema): + website_id: int = Field(...) + reason: Optional[str] = Field(None, description="申请原因") +# ========================= +# 授权接口(POST) +# ========================= @router.post("/authorize", auth=jwt_auth) @manager_required def authorize_user(request, data: AuthorizeIn): @@ -24,6 +48,7 @@ def authorize_user(request, data: AuthorizeIn): if target_user.role != "user": return {"success": False, "message": "只能授权给普通用户"} + managed_ids = set(manager.managed_websites.values_list("id", flat=True)) for wid in data.website_ids: if wid not in managed_ids: @@ -31,44 +56,71 @@ def authorize_user(request, data: AuthorizeIn): target_user.authorized_websites.add(*data.website_ids) + # 如果用户曾申请过,设置为已批准 + WebsiteAccessRequest.objects.filter(user=target_user, website_id__in=data.website_ids).update(status="approved") + return { "success": True, "message": f"已授权 {target_user.username} 访问 {len(data.website_ids)} 个网站", } -@router.get("/authorized-sites", auth=jwt_auth) +# ========================= +# 用户发起申请(POST) +# ========================= +@router.post("/apply", auth=jwt_auth) +@login_required +def request_access(request, data: AccessRequestIn): + user = request.user + site = get_object_or_404(Website, id=data.website_id) + + # 不允许重复申请 + if WebsiteAccessRequest.objects.filter(user=user, website=site, status="pending").exists(): + return {"success": False, "message": "您已申请,正在等待审批"} + + WebsiteAccessRequest.objects.create(user=user, website=site, reason=data.reason or "") + + return {"success": True, "message": "申请已提交,等待分管理审批"} + +# ========================= +# 分管理查看待审批列表 +# ========================= +@router.get("/pending", auth=jwt_auth) @manager_required -def get_user_authorized_sites(request, user_id: int = Query(...)): - target_user = get_object_or_404(User, id=user_id) +def list_pending_requests(request): + manager = request.user + managed_ids = manager.managed_websites.values_list("id", flat=True) - if target_user.role != "user": - return {"success": False, "message": "只能查看普通用户的授权信息"} - - sites = target_user.authorized_websites.all().values("id", "name", "db_alias") + requests = WebsiteAccessRequest.objects.filter(website_id__in=managed_ids, status="pending") return { "success": True, - "user": target_user.username, - "authorized_websites": list(sites) + "items": [ + { + "id": r.id, + "user": r.user.username, + "website": r.website.name, + "reason": r.reason, + "created_at": r.created_at, + } + for r in requests + ] } -@router.post("/revoke", auth=jwt_auth) +# ========================= +# 分管理审批接口 +# ========================= +@router.post("/approve", auth=jwt_auth) @manager_required -def revoke_authorization(request, data: AuthorizeIn): - manager = request.user - target_user = get_object_or_404(User, id=data.user_id) +def approve_request(request, request_id: int = Query(...), approve: bool = Query(True)): + r = get_object_or_404(WebsiteAccessRequest, id=request_id) - if target_user.role != "user": - return {"success": False, "message": "只能撤销普通用户的授权"} + if r.website not in request.user.managed_websites.all(): + return {"success": False, "message": "无权审批此申请"} - managed_ids = set(manager.managed_websites.values_list("id", flat=True)) - for wid in data.website_ids: - if wid not in managed_ids: - return {"success": False, "message": f"无权撤销网站ID:{wid}"} + r.status = "approved" if approve else "rejected" + r.save() - target_user.authorized_websites.remove(*data.website_ids) + if approve: + r.user.authorized_websites.add(r.website) - return { - "success": True, - "message": f"已撤销 {target_user.username} 的 {len(data.website_ids)} 个授权网站" - } \ No newline at end of file + return {"success": True, "message": f"已{'通过' if approve else '拒绝'} {r.user.username} 的访问申请"} diff --git a/resumes/api/detail.py b/resumes/api/detail.py new file mode 100644 index 0000000..2f63417 --- /dev/null +++ b/resumes/api/detail.py @@ -0,0 +1,42 @@ +from ninja import Router +from ninja.errors import HttpError +from django.shortcuts import get_object_or_404 + +from resumes.models import ResumeBasic, ResumeDetail +from utils.auth import jwt_auth +from utils.permissions import login_required + +router = Router(tags=["简历详情"]) + +@router.get("/{resume_id}", auth=jwt_auth) +@login_required +def get_resume_detail(request, resume_id: int): + user = request.user + + resume = get_object_or_404(ResumeBasic, id=resume_id) + + # ✅ 权限校验 + if user.role == "admin": + pass + elif user.role == "manager": + if resume.source_id not in user.managed_websites.values_list("id", flat=True): + raise HttpError(403, "无权查看该简历") + elif user.role == "user": + if resume.source_id not in user.authorized_websites.values_list("id", flat=True): + raise HttpError(403, "无权查看该简历") + + # ✅ 获取详情模型(可选) + detail = ResumeDetail.objects.filter(resume_id=resume.id).first() + + return { + "id": resume.id, + "name": resume.name, + "age": resume.age, + "job_status": resume.job_status, + "source_id": resume.source_id, + "phone": detail.phone if detail else None, + "extra": { + "教育经历": getattr(detail, "education", None), + "项目经历": getattr(detail, "projects", None), + } + } diff --git a/resumes/models.py b/resumes/models.py index b0a2fe4..d02a187 100644 --- a/resumes/models.py +++ b/resumes/models.py @@ -81,3 +81,19 @@ class ResumeBasic(models.Model): class Meta: verbose_name = "简历" verbose_name_plural = "简历列表" + +class ResumeDetail(models.Model): + resume = models.OneToOneField( + ResumeBasic, + on_delete=models.CASCADE, + primary_key=True, + related_name="detail", + verbose_name="简历" + ) + phone = models.CharField(max_length=20, verbose_name="联系方式", blank=True) + education = models.TextField(verbose_name="教育经历", blank=True) + projects = models.TextField(verbose_name="项目经历", blank=True) + + class Meta: + verbose_name = "简历详情" + verbose_name_plural = "简历详情" \ No newline at end of file