整理简历详细信息

加入导入脚本
This commit is contained in:
晓丰 2025-04-17 16:58:00 +08:00
parent c0962dd877
commit c54372a7fa
7 changed files with 180 additions and 17 deletions

View File

@ -6,11 +6,13 @@ from authorize.schemas import ResumeAccessRequestIn
from resumes.models import ResumeDetail from resumes.models import ResumeDetail
from utils.auth import jwt_auth from utils.auth import jwt_auth
from utils.permissions import login_required, manager_required from utils.permissions import login_required, manager_required
from logs.models import LogEntry
resume_authorize_router = Router(tags=["简历(详情信息)授权管理"]) resume_authorize_router = Router(tags=["简历(详情信息)授权管理"])
@resume_authorize_router.post("/apply", auth=jwt_auth, summary="申请简历详情[普]", description="普通用户申请查看某一份简历详情") @resume_authorize_router.post("/apply", auth=jwt_auth, summary="申请简历详情[普]",
description="普通用户申请查看某一份简历详情")
@login_required @login_required
def apply_resume_access(request, data: ResumeAccessRequestIn): def apply_resume_access(request, data: ResumeAccessRequestIn):
user = request.user user = request.user
@ -32,11 +34,18 @@ def apply_resume_access(request, data: ResumeAccessRequestIn):
resume=resume, resume=resume,
reason=data.reason or "" reason=data.reason or ""
) )
LogEntry.objects.create(
user=user,
action="apply_resume",
target_type="resume",
target_id=resume.id,
message="申请查看简历"
)
return {"success": True, "message": "申请已提交,等待审批"} return {"success": True, "message": "申请已提交,等待审批"}
@resume_authorize_router.get("/pending", auth=jwt_auth, summary="待审批简历[分]", description="分管理查看自己网站下的待审批简历详情申请") @resume_authorize_router.get("/pending", auth=jwt_auth, summary="待审批简历[分]",
description="分管理查看自己网站下的待审批简历详情申请")
@manager_required @manager_required
def list_pending_resume_requests(request): def list_pending_resume_requests(request):
manager = request.user manager = request.user
@ -61,7 +70,8 @@ def list_pending_resume_requests(request):
return {"success": True, "items": data} return {"success": True, "items": data}
@resume_authorize_router.post("/approve", auth=jwt_auth, summary="审批简历详情[分]", description="分管理审批某个用户的简历查看申请") @resume_authorize_router.post("/approve", auth=jwt_auth, summary="审批简历详情[分]",
description="分管理审批某个用户的简历查看申请")
@manager_required @manager_required
def approve_resume_request(request, request_id: int = Query(...), approve: bool = Query(...)): def approve_resume_request(request, request_id: int = Query(...), approve: bool = Query(...)):
req = get_object_or_404(ResumeDetailAccessRequest, id=request_id) req = get_object_or_404(ResumeDetailAccessRequest, id=request_id)
@ -72,10 +82,19 @@ def approve_resume_request(request, request_id: int = Query(...), approve: bool
req.status = "approved" if approve else "rejected" req.status = "approved" if approve else "rejected"
req.save() 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} 的访问申请"} return {"success": True, "message": f"{'通过' if approve else '拒绝'}对简历 {req.resume.id} 的访问申请"}
@resume_authorize_router.get("/history", auth=jwt_auth, summary="我的简历申请记录[普]", description="普通用户查看自己申请的简历详情访问记录") @resume_authorize_router.get("/history", auth=jwt_auth, summary="我的简历申请记录[普]",
description="普通用户查看自己申请的简历详情访问记录")
@login_required @login_required
def my_resume_request_history(request): def my_resume_request_history(request):
user = request.user user = request.user
@ -97,7 +116,8 @@ def my_resume_request_history(request):
return {"success": True, "items": data} return {"success": True, "items": data}
@resume_authorize_router.post("/manual-authorize", auth=jwt_auth, summary="手动授权简历详情[分]", description="分管理跳过申请流程,直接授权某用户查看指定简历") @resume_authorize_router.post("/manual-authorize", auth=jwt_auth, summary="手动授权简历详情[分]",
description="分管理跳过申请流程,直接授权某用户查看指定简历")
@manager_required @manager_required
def manually_authorize_resume(request, user_id: int = Query(...), resume_id: int = Query(...)): def manually_authorize_resume(request, user_id: int = Query(...), resume_id: int = Query(...)):
user = get_object_or_404(User, id=user_id) user = get_object_or_404(User, id=user_id)
@ -119,10 +139,20 @@ def manually_authorize_resume(request, user_id: int = Query(...), resume_id: int
record.status = "approved" record.status = "approved"
record.save() 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}"} return {"success": True, "message": f"已手动授权 {user.username} 访问简历 {resume.id}"}
@resume_authorize_router.get("/granted", auth=jwt_auth, summary="我已获授权的简历ID[普]", description="普通用户查看当前已被授权访问的简历ID列表") @resume_authorize_router.get("/granted", auth=jwt_auth, summary="我已获授权的简历ID[普]",
description="普通用户查看当前已被授权访问的简历ID列表")
@login_required @login_required
def list_granted_resume_ids(request): def list_granted_resume_ids(request):
user = request.user user = request.user
@ -134,4 +164,4 @@ def list_granted_resume_ids(request):
status="approved" status="approved"
).values_list("resume_id", flat=True) ).values_list("resume_id", flat=True)
return {"success": True, "resume_ids": list(ids)} return {"success": True, "resume_ids": list(ids)}

View File

@ -7,6 +7,7 @@ from resumes.models import ResumeDetail
from websites.models import Website from websites.models import Website
from utils.auth import jwt_auth from utils.auth import jwt_auth
from utils.permissions import manager_required, login_required from utils.permissions import manager_required, login_required
from logs.models import LogEntry
website_authorize_router = Router(tags=["网站(简历一般信息)授权管理"]) website_authorize_router = Router(tags=["网站(简历一般信息)授权管理"])
@ -30,6 +31,15 @@ def authorize_user(request, data: AuthorizeIn):
WebsiteAccessRequest.objects.filter(user=target_user, website_id__in=data.website_ids).update(status="approved") 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 { return {
"success": True, "success": True,
"message": f"已授权 {target_user.username} 访问 {len(data.website_ids)} 个网站", "message": f"已授权 {target_user.username} 访问 {len(data.website_ids)} 个网站",
@ -48,6 +58,14 @@ def request_access(request, data: AccessRequestIn):
WebsiteAccessRequest.objects.create(user=user, website=site, reason=data.reason or "") 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": "申请已提交,等待分管理审批"} return {"success": True, "message": "申请已提交,等待分管理审批"}
@ -90,6 +108,14 @@ def approve_request(request, request_id: int = Query(...), approve: bool = Query
if approve: if approve:
r.user.authorized_websites.add(r.website) 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} 的访问申请"} return {"success": True, "message": f"{'通过' if approve else '拒绝'} {r.user.username} 的访问申请"}

View File

@ -1,3 +1,11 @@
from django.contrib import admin from django.contrib import admin
# Register your models here. # 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")

View File

@ -1,3 +1,31 @@
from django.db import models 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}"

View 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}")

View File

@ -30,19 +30,23 @@ class ResumeBasic(models.Model):
help_text="求职状态") help_text="求职状态")
work_1_experience = models.TextField(null=True, blank=True, verbose_name="工作1经历", help_text="工作1经历") 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_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_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_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_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_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_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内容") 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="身高") height = models.IntegerField(null=True, blank=True, verbose_name="身高", help_text="身高")
@ -60,7 +64,8 @@ class ResumeBasic(models.Model):
help_text="从事行业") help_text="从事行业")
expected_salary = models.CharField(max_length=255, null=True, blank=True, verbose_name="期望薪资", expected_salary = models.CharField(max_length=255, null=True, blank=True, verbose_name="期望薪资",
help_text="期望薪资") 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="工作性质", job_property = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作性质",
help_text="工作性质") help_text="工作性质")
job_location = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作地点", job_location = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作地点",
@ -82,16 +87,20 @@ class ResumeBasic(models.Model):
verbose_name = "简历" verbose_name = "简历"
verbose_name_plural = "简历列表" verbose_name_plural = "简历列表"
class ResumeDetail(models.Model): class ResumeDetail(models.Model):
resume = models.OneToOneField( resume = models.OneToOneField(
ResumeBasic, ResumeBasic,
on_delete=models.CASCADE, on_delete=models.SET_NULL,
primary_key=True, null=True,
blank=True,
unique=True,
related_name="detail", related_name="detail",
verbose_name="简历" verbose_name="简历"
) )
unlinked_resume_id = models.IntegerField(null=True, blank=True, verbose_name="无法关联的简历ID")
phone = models.CharField(max_length=20, verbose_name="联系方式", blank=True) phone = models.CharField(max_length=20, verbose_name="联系方式", blank=True)
email = models.EmailField(verbose_name="邮箱", blank=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
class Meta: class Meta: