Compare commits
10 Commits
5235fe1e77
...
1123189a4a
Author | SHA1 | Date | |
---|---|---|---|
1123189a4a | |||
c54372a7fa | |||
c0962dd877 | |||
9e0f36e77e | |||
1257aeff5d | |||
d4714d3f43 | |||
273b9713e0 | |||
8a95eb6a7d | |||
e18d7a20dc | |||
5474141c26 |
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AccessControlConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'access_control'
|
@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
@ -5,8 +5,21 @@ from .models import User
|
||||
|
||||
@admin.register(User)
|
||||
class UserAdmin(DefaultUserAdmin):
|
||||
list_display = ("username", "email", "role", "is_active", "is_staff", "last_login")
|
||||
list_display = ("username", "email", "role", "source_manager", "is_active", "is_staff", "last_login")
|
||||
list_filter = ("role", "is_active", "is_staff", "is_superuser")
|
||||
fieldsets = DefaultUserAdmin.fieldsets + (
|
||||
("角色权限", {"fields": ("role",)}),
|
||||
)
|
||||
search_fields = ("username", "email")
|
||||
filter_horizontal = ("groups", "user_permissions", "managed_websites", "authorized_websites")
|
||||
|
||||
def get_fieldsets(self, request, obj=None):
|
||||
base = list(super().get_fieldsets(request, obj))
|
||||
role_fields = ["role"]
|
||||
if obj and obj.role == "user":
|
||||
role_fields.append("source_manager")
|
||||
base.append(("角色和权限", {"fields": role_fields}))
|
||||
if obj:
|
||||
if obj.role == "manager":
|
||||
base.append(("管理权限", {"fields": ("managed_websites",)}))
|
||||
elif obj.role == "user":
|
||||
base.append(("访问权限", {"fields": ("authorized_websites",)}))
|
||||
|
||||
return base
|
@ -15,25 +15,30 @@ def register(
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
email: str = Form(...),
|
||||
code: str = Form(None)
|
||||
code: str = Form(...)
|
||||
):
|
||||
if User.objects.filter(username=username).exists():
|
||||
return {"success": False, "message": "用户名已存在"}
|
||||
|
||||
user = User(username=username, email=email, role="user")
|
||||
try:
|
||||
reg = RegistrationCode.objects.get(code=code)
|
||||
if not reg.is_available():
|
||||
return {"success": False, "message": "注册码已达使用上限"}
|
||||
except RegistrationCode.DoesNotExist:
|
||||
return {"success": False, "message": "注册码无效"}
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
role="user",
|
||||
source_manager=reg.manager
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
if code:
|
||||
try:
|
||||
reg = RegistrationCode.objects.get(code=code)
|
||||
if not reg.is_available():
|
||||
return {"success": False, "message": "注册码已达使用上限"}
|
||||
user.authorized_websites.set(reg.manager.managed_websites.all())
|
||||
reg.used_count += 1
|
||||
reg.save()
|
||||
except RegistrationCode.DoesNotExist:
|
||||
return {"success": False, "message": "注册码无效"}
|
||||
reg.used_count += 1
|
||||
reg.save()
|
||||
RegistrationCode.objects.create(code=reg, user=user)
|
||||
|
||||
refresh = RefreshToken.for_user(user)
|
||||
|
||||
@ -51,6 +56,7 @@ def register(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@auth_router.post("/login")
|
||||
def login(
|
||||
request,
|
||||
|
@ -1,129 +0,0 @@
|
||||
# ✅ 授权管理接口:manager 给 user 授权网站 + 普通用户申请接口
|
||||
from ninja import Router, Schema, Query
|
||||
from pydantic import Field
|
||||
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, 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="申请原因")
|
||||
|
||||
|
||||
@router.post("/authorize", auth=jwt_auth)
|
||||
@manager_required
|
||||
def authorize_user(request, data: AuthorizeIn):
|
||||
manager = request.user
|
||||
target_user = get_object_or_404(User, id=data.user_id)
|
||||
|
||||
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:
|
||||
return {"success": False, "message": f"无权授权网站ID:{wid}"}
|
||||
|
||||
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.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 list_pending_requests(request):
|
||||
manager = request.user
|
||||
managed_ids = manager.managed_websites.values_list("id", flat=True)
|
||||
|
||||
requests = WebsiteAccessRequest.objects.filter(website_id__in=managed_ids, status="pending")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"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("/approve", auth=jwt_auth)
|
||||
@manager_required
|
||||
def approve_request(request, request_id: int = Query(...), approve: bool = Query(True)):
|
||||
r = get_object_or_404(WebsiteAccessRequest, id=request_id)
|
||||
|
||||
if r.website not in request.user.managed_websites.all():
|
||||
return {"success": False, "message": "无权审批此申请"}
|
||||
|
||||
r.status = "approved" if approve else "rejected"
|
||||
r.save()
|
||||
|
||||
if approve:
|
||||
r.user.authorized_websites.add(r.website)
|
||||
|
||||
return {"success": True, "message": f"已{'通过' if approve else '拒绝'} {r.user.username} 的访问申请"}
|
||||
|
||||
|
||||
@router.get("/my-sites", auth=jwt_auth)
|
||||
@login_required
|
||||
def list_my_authorized_websites(request):
|
||||
user = request.user
|
||||
sites = user.authorized_websites.all().values("id", "name", "db_alias")
|
||||
return {"success": True, "websites": list(sites)}
|
||||
|
||||
|
||||
@router.get("/public-sites")
|
||||
def list_public_websites(request):
|
||||
websites = Website.objects.all().values("id", "name", "db_alias", "description")
|
||||
return {"success": True, "websites": list(websites)}
|
@ -21,6 +21,15 @@ class User(AbstractUser):
|
||||
related_name="authorized_users",
|
||||
help_text="普通用户被授权可访问的网站"
|
||||
)
|
||||
source_manager = models.ForeignKey(
|
||||
"self",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="brought_users",
|
||||
limit_choices_to={"role": "manager"},
|
||||
verbose_name="所属分管理"
|
||||
)
|
||||
def is_admin(self):
|
||||
return self.role == 'admin'
|
||||
|
||||
|
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
@ -1,3 +0,0 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
8
api.py
8
api.py
@ -2,10 +2,12 @@ from ninja import NinjaAPI
|
||||
from resumes.api.views import router as resume_router
|
||||
from accounts.api.auth import auth_router
|
||||
from accounts.api.user import user_router
|
||||
from accounts.api.authorize import router
|
||||
from authorize.api.website_authorize import website_authorize_router
|
||||
from authorize.api.resume_authorize import resume_authorize_router
|
||||
|
||||
api = NinjaAPI(title="简历管理 API")
|
||||
api.add_router("/resumes/", resume_router)
|
||||
api.add_router("/resumes", resume_router)
|
||||
api.add_router("/auth", auth_router)
|
||||
api.add_router("/users", user_router)
|
||||
api.add_router("/authorize", router)
|
||||
api.add_router("/website", website_authorize_router)
|
||||
api.add_router("/resume", resume_authorize_router)
|
18
authorize/admin.py
Normal file
18
authorize/admin.py
Normal file
@ -0,0 +1,18 @@
|
||||
from django.contrib import admin
|
||||
from authorize.models import WebsiteAccessRequest, ResumeDetailAccessRequest
|
||||
|
||||
|
||||
# Register your models here.
|
||||
@admin.register(WebsiteAccessRequest)
|
||||
class WebsiteAccessRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'website', 'status', 'reason', 'created_at', 'updated_at')
|
||||
list_filter = ('status', 'website', 'created_at')
|
||||
search_fields = ('user__username', 'website__name', 'reason')
|
||||
ordering = ('-created_at',)
|
||||
readonly_fields = ('created_at', 'updated_at')
|
||||
|
||||
@admin.register(ResumeDetailAccessRequest)
|
||||
class ResumeDetailAccessRequestAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'resume', 'reason', 'status', 'created_at')
|
||||
list_filter = ('status', 'created_at')
|
||||
search_fields = ('user__username', 'resume__id')
|
167
authorize/api/resume_authorize.py
Normal file
167
authorize/api/resume_authorize.py
Normal file
@ -0,0 +1,167 @@
|
||||
from ninja import Router, Query
|
||||
from django.shortcuts import get_object_or_404
|
||||
from accounts.models import User
|
||||
from authorize.models import ResumeDetailAccessRequest
|
||||
from authorize.schemas import ResumeAccessRequestIn
|
||||
from resumes.models import ResumeDetail
|
||||
from utils.auth import jwt_auth
|
||||
from utils.permissions import login_required, manager_required
|
||||
from logs.models import LogEntry
|
||||
|
||||
resume_authorize_router = Router(tags=["简历(详情信息)授权管理"])
|
||||
|
||||
|
||||
@resume_authorize_router.post("/apply", auth=jwt_auth, summary="申请简历详情[普]",
|
||||
description="普通用户申请查看某一份简历详情")
|
||||
@login_required
|
||||
def apply_resume_access(request, data: ResumeAccessRequestIn):
|
||||
user = request.user
|
||||
|
||||
if not user.is_user():
|
||||
return {"success": False, "message": "仅普通用户可申请查看简历"}
|
||||
|
||||
resume = get_object_or_404(ResumeDetail, id=data.resume_id)
|
||||
|
||||
exists = ResumeDetailAccessRequest.objects.filter(
|
||||
user=user, resume=resume, status="pending"
|
||||
).exists()
|
||||
|
||||
if exists:
|
||||
return {"success": False, "message": "您已申请过该简历,正在等待审批"}
|
||||
|
||||
ResumeDetailAccessRequest.objects.create(
|
||||
user=user,
|
||||
resume=resume,
|
||||
reason=data.reason or ""
|
||||
)
|
||||
LogEntry.objects.create(
|
||||
user=user,
|
||||
action="apply_resume",
|
||||
target_type="resume",
|
||||
target_id=resume.id,
|
||||
message="申请查看简历"
|
||||
)
|
||||
return {"success": True, "message": "申请已提交,等待审批"}
|
||||
|
||||
|
||||
@resume_authorize_router.get("/pending", auth=jwt_auth, summary="待审批简历[分]",
|
||||
description="分管理查看自己网站下的待审批简历详情申请")
|
||||
@manager_required
|
||||
def list_pending_resume_requests(request):
|
||||
manager = request.user
|
||||
manageable_ids = manager.managed_websites.values_list("id", flat=True)
|
||||
|
||||
requests = ResumeDetailAccessRequest.objects.filter(
|
||||
resume__source_id__in=manageable_ids,
|
||||
status="pending"
|
||||
)
|
||||
|
||||
data = [
|
||||
{
|
||||
"id": r.id,
|
||||
"user": r.user.username,
|
||||
"resume_id": r.resume.id,
|
||||
"reason": r.reason,
|
||||
"created_at": r.created_at,
|
||||
}
|
||||
for r in requests
|
||||
]
|
||||
|
||||
return {"success": True, "items": data}
|
||||
|
||||
|
||||
@resume_authorize_router.post("/approve", auth=jwt_auth, summary="审批简历详情[分]",
|
||||
description="分管理审批某个用户的简历查看申请")
|
||||
@manager_required
|
||||
def approve_resume_request(request, request_id: int = Query(...), approve: bool = Query(...)):
|
||||
req = get_object_or_404(ResumeDetailAccessRequest, id=request_id)
|
||||
|
||||
if req.resume.source not in request.user.managed_websites.all():
|
||||
return {"success": False, "message": "无权审批该申请"}
|
||||
|
||||
req.status = "approved" if approve else "rejected"
|
||||
req.save()
|
||||
|
||||
LogEntry.objects.create(
|
||||
user=request.user,
|
||||
action="approve_resume",
|
||||
target_type="resume",
|
||||
target_id=req.resume.id,
|
||||
message=f"审批简历:{req.user.username} -> {req.status}"
|
||||
)
|
||||
|
||||
return {"success": True, "message": f"已{'通过' if approve else '拒绝'}对简历 {req.resume.id} 的访问申请"}
|
||||
|
||||
|
||||
@resume_authorize_router.get("/history", auth=jwt_auth, summary="我的简历申请记录[普]",
|
||||
description="普通用户查看自己申请的简历详情访问记录")
|
||||
@login_required
|
||||
def my_resume_request_history(request):
|
||||
user = request.user
|
||||
if not user.is_user():
|
||||
return {"success": False, "message": "仅普通用户可查看"}
|
||||
|
||||
records = ResumeDetailAccessRequest.objects.filter(user=user).order_by("-created_at")
|
||||
|
||||
data = [
|
||||
{
|
||||
"resume_id": r.resume.id,
|
||||
"reason": r.reason,
|
||||
"status": r.status,
|
||||
"created_at": r.created_at,
|
||||
}
|
||||
for r in records
|
||||
]
|
||||
|
||||
return {"success": True, "items": data}
|
||||
|
||||
|
||||
@resume_authorize_router.post("/manual-authorize", auth=jwt_auth, summary="手动授权简历详情[分]",
|
||||
description="分管理跳过申请流程,直接授权某用户查看指定简历")
|
||||
@manager_required
|
||||
def manually_authorize_resume(request, user_id: int = Query(...), resume_id: int = Query(...)):
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
resume = get_object_or_404(ResumeDetail, id=resume_id)
|
||||
|
||||
if not user.is_user():
|
||||
return {"success": False, "message": "仅能授权给普通用户"}
|
||||
|
||||
if resume.source not in request.user.managed_websites.all():
|
||||
return {"success": False, "message": "无权授权该简历"}
|
||||
|
||||
record, created = ResumeDetailAccessRequest.objects.get_or_create(
|
||||
user=user,
|
||||
resume=resume,
|
||||
defaults={"status": "approved", "reason": "由分管理手动授权"}
|
||||
)
|
||||
|
||||
if not created:
|
||||
record.status = "approved"
|
||||
record.save()
|
||||
|
||||
LogEntry.objects.create(
|
||||
user=request.user,
|
||||
action="manual_grant_resume",
|
||||
target_type="resume",
|
||||
target_id=resume.id,
|
||||
message=f"手动授权 {user.username} 查看简历"
|
||||
)
|
||||
|
||||
|
||||
return {"success": True, "message": f"已手动授权 {user.username} 访问简历 {resume.id}"}
|
||||
|
||||
|
||||
@resume_authorize_router.get("/granted", auth=jwt_auth, summary="我已获授权的简历ID[普]",
|
||||
description="普通用户查看当前已被授权访问的简历ID列表")
|
||||
@login_required
|
||||
def list_granted_resume_ids(request):
|
||||
user = request.user
|
||||
if not user.is_user():
|
||||
return {"success": False, "message": "仅普通用户可访问"}
|
||||
|
||||
ids = ResumeDetailAccessRequest.objects.filter(
|
||||
user=user,
|
||||
status="approved"
|
||||
).values_list("resume_id", flat=True)
|
||||
|
||||
return {"success": True, "resume_ids": list(ids)}
|
147
authorize/api/website_authorize.py
Normal file
147
authorize/api/website_authorize.py
Normal file
@ -0,0 +1,147 @@
|
||||
from ninja import Router, Query
|
||||
from django.shortcuts import get_object_or_404
|
||||
from accounts.models import User
|
||||
from authorize.models import WebsiteAccessRequest, ResumeDetailAccessRequest
|
||||
from authorize.schemas import ResumeAccessRequestIn, AccessRequestIn, AuthorizeIn
|
||||
from resumes.models import ResumeDetail
|
||||
from websites.models import Website
|
||||
from utils.auth import jwt_auth
|
||||
from utils.permissions import manager_required, login_required
|
||||
from logs.models import LogEntry
|
||||
|
||||
website_authorize_router = Router(tags=["网站(简历一般信息)授权管理"])
|
||||
|
||||
|
||||
@website_authorize_router.post("/authorize", auth=jwt_auth, summary="分管手动授权网站[分管]",
|
||||
description="分管理授权普通用户访问指定网站")
|
||||
@manager_required
|
||||
def authorize_user(request, data: AuthorizeIn):
|
||||
manager = request.user
|
||||
target_user = get_object_or_404(User, id=data.user_id)
|
||||
|
||||
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:
|
||||
return {"success": False, "message": f"无权授权网站ID:{wid}"}
|
||||
|
||||
target_user.authorized_websites.add(*data.website_ids)
|
||||
|
||||
WebsiteAccessRequest.objects.filter(user=target_user, website_id__in=data.website_ids).update(status="approved")
|
||||
|
||||
for wid in data.website_ids:
|
||||
LogEntry.objects.create(
|
||||
user=manager,
|
||||
action="manual_grant_website",
|
||||
target_type="website",
|
||||
target_id=wid,
|
||||
message=f"手动授权 {target_user.username} 访问网站"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"已授权 {target_user.username} 访问 {len(data.website_ids)} 个网站",
|
||||
}
|
||||
|
||||
|
||||
@website_authorize_router.post("/apply", auth=jwt_auth, summary="申请网站授权[普]",
|
||||
description="普通用户发起网站访问申请")
|
||||
@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 "")
|
||||
|
||||
LogEntry.objects.create(
|
||||
user=user,
|
||||
action="apply_website",
|
||||
target_type="website",
|
||||
target_id=site.id,
|
||||
message="申请访问网站"
|
||||
)
|
||||
|
||||
return {"success": True, "message": "申请已提交,等待分管理审批"}
|
||||
|
||||
|
||||
@website_authorize_router.get("/pending", auth=jwt_auth, summary="待审批列表[分管]",
|
||||
description="分管理查看自己负责的网站的待审批访问申请")
|
||||
@manager_required
|
||||
def list_pending_requests(request):
|
||||
manager = request.user
|
||||
managed_ids = manager.managed_websites.values_list("id", flat=True)
|
||||
|
||||
requests = WebsiteAccessRequest.objects.filter(website_id__in=managed_ids, status="pending")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"items": [
|
||||
{
|
||||
"id": r.id,
|
||||
"user": r.user.username,
|
||||
"website": r.website.name,
|
||||
"reason": r.reason,
|
||||
"created_at": r.created_at,
|
||||
}
|
||||
for r in requests
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@website_authorize_router.post("/approve", auth=jwt_auth, summary="审批网站授权[分管]",
|
||||
description="分管理审批网站访问申请(通过或拒绝)")
|
||||
@manager_required
|
||||
def approve_request(request, request_id: int = Query(...), approve: bool = Query(True)):
|
||||
r = get_object_or_404(WebsiteAccessRequest, id=request_id)
|
||||
|
||||
if r.website not in request.user.managed_websites.all():
|
||||
return {"success": False, "message": "无权审批此申请"}
|
||||
|
||||
r.status = "approved" if approve else "rejected"
|
||||
r.save()
|
||||
|
||||
if approve:
|
||||
r.user.authorized_websites.add(r.website)
|
||||
|
||||
LogEntry.objects.create(
|
||||
user=request.user,
|
||||
action="approve_website",
|
||||
target_type="website",
|
||||
target_id=r.website.id,
|
||||
message=f"审批网站:{r.user.username} -> {r.status}"
|
||||
)
|
||||
|
||||
return {"success": True, "message": f"已{'通过' if approve else '拒绝'} {r.user.username} 的访问申请"}
|
||||
|
||||
|
||||
@website_authorize_router.get("/my-sites", auth=jwt_auth, summary="我的网站列表[普]",
|
||||
description="展示当前用户可申请与已授权的网站列表,并标记授权状态")
|
||||
@login_required
|
||||
def list_user_sites_with_status(request):
|
||||
user = request.user
|
||||
|
||||
if not user.is_user():
|
||||
return {"success": False, "message": "仅普通用户可访问"}
|
||||
|
||||
if not user.source_manager:
|
||||
return {"success": False, "message": "您尚未绑定所属分管理,无法申请网站"}
|
||||
|
||||
# 可申请的网站(所属分管理可管理)
|
||||
manageable_sites = user.source_manager.managed_websites.all()
|
||||
authorized_site_ids = set(user.authorized_websites.values_list("id", flat=True))
|
||||
|
||||
websites = []
|
||||
for site in manageable_sites:
|
||||
websites.append({
|
||||
"id": site.id,
|
||||
"name": site.name,
|
||||
"db_alias": site.db_alias,
|
||||
"authorized": site.id in authorized_site_ids
|
||||
})
|
||||
|
||||
return {"success": True, "websites": websites}
|
@ -1,6 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AdminPanelConfig(AppConfig):
|
||||
class AuthorizeConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'admin_panel'
|
||||
name = 'authorize'
|
47
authorize/models.py
Normal file
47
authorize/models.py
Normal file
@ -0,0 +1,47 @@
|
||||
from django.db import models
|
||||
from accounts.models import User
|
||||
from websites.models import Website
|
||||
|
||||
from resumes.models import ResumeDetail
|
||||
|
||||
|
||||
# Create your models here.
|
||||
class WebsiteAccessRequest(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
website = models.ForeignKey(Website, on_delete=models.CASCADE)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
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 Meta:
|
||||
verbose_name = "网站访问申请"
|
||||
verbose_name_plural = "网站访问申请"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} 申请网站 {self.website.name} ({self.status})"
|
||||
|
||||
|
||||
class ResumeDetailAccessRequest(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="申请用户")
|
||||
resume = models.ForeignKey(ResumeDetail, on_delete=models.CASCADE, verbose_name="目标简历")
|
||||
reason = models.TextField(blank=True, verbose_name="申请理由")
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=[("pending", "待审批"), ("approved", "已通过"), ("rejected", "已拒绝")],
|
||||
default="pending",
|
||||
verbose_name="审批状态"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="申请时间")
|
||||
|
||||
class Meta:
|
||||
unique_together = ("user", "resume")
|
||||
verbose_name = "简历详情访问申请"
|
||||
verbose_name_plural = "简历详情访问申请"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} 申请查看简历 {self.resume.id} ({self.status})"
|
18
authorize/schemas.py
Normal file
18
authorize/schemas.py
Normal file
@ -0,0 +1,18 @@
|
||||
from ninja import Schema, Query
|
||||
from pydantic import Field
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
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="申请原因")
|
||||
|
||||
|
||||
class ResumeAccessRequestIn(Schema):
|
||||
resume_id: int = Field(..., description="简历ID")
|
||||
reason: Optional[str] = Field(None, description="申请理由")
|
@ -47,10 +47,9 @@ INSTALLED_APPS = [
|
||||
'accounts',
|
||||
'websites',
|
||||
'resumes',
|
||||
'access_control',
|
||||
'admin_panel',
|
||||
'logs',
|
||||
'invites'
|
||||
'invites',
|
||||
'authorize'
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
|
@ -14,6 +14,10 @@ class RegistrationCode(models.Model):
|
||||
used_count = models.IntegerField(default=0, verbose_name="已使用次数")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "注册码申请"
|
||||
verbose_name_plural = "注册码申请"
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.code} ({self.used_count}/{self.usage_limit})"
|
||||
|
||||
|
@ -1,3 +1,11 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
from logs.models import LogEntry
|
||||
|
||||
@admin.register(LogEntry)
|
||||
class LogEntryAdmin(admin.ModelAdmin):
|
||||
list_display = ("user", "action", "target_type", "target_id", "message", "created_at")
|
||||
list_filter = ("action", "target_type", "created_at")
|
||||
search_fields = ("user__username", "message", "target_type")
|
||||
readonly_fields = ("user", "action", "target_type", "target_id", "message", "created_at")
|
@ -1,3 +1,31 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
# Create your models here.
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class LogEntry(models.Model):
|
||||
ACTION_CHOICES = [
|
||||
("apply_resume", "申请查看简历"),
|
||||
("approve_resume", "审批简历申请"),
|
||||
("manual_grant_resume", "手动授权简历"),
|
||||
("apply_website", "申请访问网站"),
|
||||
("approve_website", "审批网站申请"),
|
||||
("manual_grant_website", "手动授权网站"),
|
||||
]
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name="operation_logs",
|
||||
verbose_name="操作者")
|
||||
action = models.CharField(max_length=50, choices=ACTION_CHOICES, verbose_name="操作类型")
|
||||
target_type = models.CharField(max_length=50, verbose_name="目标类型", help_text="如 resume, website")
|
||||
target_id = models.IntegerField(verbose_name="目标ID")
|
||||
message = models.TextField(blank=True, verbose_name="说明")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="操作时间")
|
||||
|
||||
class Meta:
|
||||
verbose_name = "操作日志"
|
||||
verbose_name_plural = "操作日志"
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"[{self.get_action_display()}] {self.user} -> {self.target_type}#{self.target_id}"
|
||||
|
@ -9,3 +9,18 @@ class ResumeDetailInline(admin.StackedInline):
|
||||
class ResumeDetailAdmin(admin.ModelAdmin):
|
||||
list_display = ("resume", "phone", "updated_at")
|
||||
search_fields = ("resume__name", "phone")
|
||||
|
||||
@admin.register(ResumeBasic)
|
||||
class ResumeBasicAdmin(admin.ModelAdmin):
|
||||
list_display = (
|
||||
'resume_id', 'name', 'age', 'gender', 'job_status', 'education',
|
||||
'expected_position', 'last_active_time', 'update_time'
|
||||
)
|
||||
search_fields = ('name', 'phone', 'resume_id')
|
||||
list_filter = ('job_status', 'gender', 'education', 'highest_education', 'source')
|
||||
ordering = ('-update_time',)
|
||||
|
||||
@admin.display(description='数据来源')
|
||||
def source_name(self, obj):
|
||||
print(obj.source.name)
|
||||
return obj.source.name if obj.source else "-"
|
62
resumes/management/commands/import_resume_details.py
Normal file
62
resumes/management/commands/import_resume_details.py
Normal file
@ -0,0 +1,62 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
import pandas as pd
|
||||
from resumes.models import ResumeBasic, ResumeDetail
|
||||
from django.db import transaction
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "导入护理类简历详情数据到 ResumeDetail 模型"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument("filepath", type=str, help="Excel 文件路径")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
filepath = options["filepath"]
|
||||
df = pd.read_excel(filepath)
|
||||
|
||||
success, skipped, errors = 0, 0, []
|
||||
|
||||
for _, row in df.iterrows():
|
||||
raw_resume_id = row.get("resume_id")
|
||||
phone = row.get("phone")
|
||||
email = row.get("email")
|
||||
|
||||
if pd.isna(raw_resume_id):
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
resume_id = None
|
||||
try:
|
||||
resume_id = int(str(raw_resume_id).strip().split(".")[0]) # 去掉小数点尾巴等干扰
|
||||
except (ValueError, TypeError):
|
||||
errors.append(f"resume_id 非法格式: {raw_resume_id}")
|
||||
continue
|
||||
|
||||
try:
|
||||
basic = ResumeBasic.objects.get(resume_id=resume_id)
|
||||
ResumeDetail.objects.update_or_create(
|
||||
resume=basic,
|
||||
defaults={
|
||||
"unlinked_resume_id": None,
|
||||
"phone": str(phone).strip() if not pd.isna(phone) else "",
|
||||
"email": str(email).strip() if not pd.isna(email) else ""
|
||||
}
|
||||
)
|
||||
success += 1
|
||||
except ResumeBasic.DoesNotExist:
|
||||
ResumeDetail.objects.update_or_create(
|
||||
unlinked_resume_id=resume_id,
|
||||
defaults={
|
||||
"resume": None,
|
||||
"phone": str(phone).strip() if not pd.isna(phone) else "",
|
||||
"email": str(email).strip() if not pd.isna(email) else ""
|
||||
}
|
||||
)
|
||||
success += 1
|
||||
errors.append(f"resume_id={resume_id} 无对应 ResumeBasic,已记录至 unlinked_resume_id")
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"成功导入 {success} 条,跳过 {skipped} 条"))
|
||||
if errors:
|
||||
self.stdout.write(self.style.WARNING("以下数据未关联基础简历:"))
|
||||
for msg in errors:
|
||||
self.stdout.write(f" - {msg}")
|
@ -7,7 +7,7 @@ from websites.models import Website
|
||||
|
||||
|
||||
class ResumeBasic(models.Model):
|
||||
resume_id = models.IntegerField(unique=True, db_index=True, verbose_name="简历ID", help_text="resume_id")
|
||||
resume_id = models.IntegerField(db_index=True, verbose_name="简历ID", help_text="resume_id")
|
||||
name = models.CharField(max_length=255, null=True, blank=True, verbose_name="姓名", help_text="姓名")
|
||||
job_region = models.CharField(max_length=255, null=True, blank=True, verbose_name="求职区域",
|
||||
help_text="求职区域")
|
||||
@ -30,19 +30,23 @@ class ResumeBasic(models.Model):
|
||||
help_text="求职状态")
|
||||
|
||||
work_1_experience = models.TextField(null=True, blank=True, verbose_name="工作1经历", help_text="工作1经历")
|
||||
work_1_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作1时间", help_text="工作1时间")
|
||||
work_1_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作1时间",
|
||||
help_text="工作1时间")
|
||||
work_1_description = models.TextField(null=True, blank=True, verbose_name="工作1内容", help_text="工作1内容")
|
||||
|
||||
work_2_experience = models.TextField(null=True, blank=True, verbose_name="工作2经历", help_text="工作2经历")
|
||||
work_2_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作2时间", help_text="工作2时间")
|
||||
work_2_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作2时间",
|
||||
help_text="工作2时间")
|
||||
work_2_description = models.TextField(null=True, blank=True, verbose_name="工作2内容", help_text="工作2内容")
|
||||
|
||||
work_3_experience = models.TextField(null=True, blank=True, verbose_name="工作3经历", help_text="工作3经历")
|
||||
work_3_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作3时间", help_text="工作3时间")
|
||||
work_3_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作3时间",
|
||||
help_text="工作3时间")
|
||||
work_3_description = models.TextField(null=True, blank=True, verbose_name="工作3内容", help_text="工作3内容")
|
||||
|
||||
work_4_experience = models.TextField(null=True, blank=True, verbose_name="工作4经历", help_text="工作4经历")
|
||||
work_4_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作4时间", help_text="工作4时间")
|
||||
work_4_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作4时间",
|
||||
help_text="工作4时间")
|
||||
work_4_description = models.TextField(null=True, blank=True, verbose_name="工作4内容", help_text="工作4内容")
|
||||
|
||||
height = models.IntegerField(null=True, blank=True, verbose_name="身高", help_text="身高")
|
||||
@ -60,7 +64,8 @@ class ResumeBasic(models.Model):
|
||||
help_text="从事行业")
|
||||
expected_salary = models.CharField(max_length=255, null=True, blank=True, verbose_name="期望薪资",
|
||||
help_text="期望薪资")
|
||||
available_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="到岗时间", help_text="到岗时间")
|
||||
available_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="到岗时间",
|
||||
help_text="到岗时间")
|
||||
job_property = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作性质",
|
||||
help_text="工作性质")
|
||||
job_location = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作地点",
|
||||
@ -81,17 +86,22 @@ class ResumeBasic(models.Model):
|
||||
class Meta:
|
||||
verbose_name = "简历"
|
||||
verbose_name_plural = "简历列表"
|
||||
unique_together = ('source', 'resume_id')
|
||||
|
||||
|
||||
class ResumeDetail(models.Model):
|
||||
resume = models.OneToOneField(
|
||||
ResumeBasic,
|
||||
on_delete=models.CASCADE,
|
||||
primary_key=True,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
unique=True,
|
||||
related_name="detail",
|
||||
verbose_name="简历"
|
||||
)
|
||||
unlinked_resume_id = models.IntegerField(null=True, blank=True, verbose_name="无法关联的简历ID")
|
||||
phone = models.CharField(max_length=20, verbose_name="联系方式", blank=True)
|
||||
|
||||
email = models.EmailField(verbose_name="邮箱", blank=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
|
@ -7,6 +7,9 @@ class Website(models.Model):
|
||||
db_alias = models.CharField(max_length=50, unique=True, verbose_name="数据库别名")
|
||||
description = models.TextField(blank=True, verbose_name="描述")
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = "网站"
|
||||
verbose_name_plural = "网站列表"
|
||||
|
Loading…
x
Reference in New Issue
Block a user