Compare commits
No commits in common. "dev" and "main" have entirely different histories.
56
.gitignore
vendored
56
.gitignore
vendored
@ -1,56 +0,0 @@
|
|||||||
# === Python 缓存 ===
|
|
||||||
__pycache__/
|
|
||||||
*.py[cod]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# === 环境变量文件 ===
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
|
|
||||||
# === 虚拟环境目录 ===
|
|
||||||
venv/
|
|
||||||
.venv/
|
|
||||||
env/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# === 安装构建缓存 ===
|
|
||||||
*.egg
|
|
||||||
*.egg-info/
|
|
||||||
.eggs/
|
|
||||||
dist/
|
|
||||||
build/
|
|
||||||
pip-log.txt
|
|
||||||
|
|
||||||
# === 测试相关缓存文件 ===
|
|
||||||
.coverage
|
|
||||||
.tox/
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py,cover
|
|
||||||
|
|
||||||
# === 数据库相关 ===
|
|
||||||
*.sqlite3
|
|
||||||
db.sqlite3
|
|
||||||
|
|
||||||
# === 日志文件 ===
|
|
||||||
*.log
|
|
||||||
logs/
|
|
||||||
|
|
||||||
# === 静态与媒体文件(Django) ===
|
|
||||||
media/
|
|
||||||
static/
|
|
||||||
staticfiles/
|
|
||||||
|
|
||||||
# === IDE 配置 ===
|
|
||||||
.idea/ # PyCharm
|
|
||||||
*.iml
|
|
||||||
*.ipr
|
|
||||||
*.iws
|
|
||||||
.vscode/ # VS Code
|
|
||||||
|
|
||||||
# === 系统自动生成文件 ===
|
|
||||||
.DS_Store # macOS
|
|
||||||
Thumbs.db # Windows
|
|
8
.idea/.gitignore
generated
vendored
8
.idea/.gitignore
generated
vendored
@ -1,8 +0,0 @@
|
|||||||
# 默认忽略的文件
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# 基于编辑器的 HTTP 客户端请求
|
|
||||||
/httpRequests/
|
|
||||||
# Datasource local storage ignored files
|
|
||||||
/dataSources/
|
|
||||||
/dataSources.local.xml
|
|
@ -1,6 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class CompaniesConfig(AppConfig):
|
class AccessControlConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'companies'
|
name = 'access_control'
|
3
access_control/models.py
Normal file
3
access_control/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
@ -1,25 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.contrib.auth.admin import UserAdmin as DefaultUserAdmin
|
|
||||||
from .models import User
|
|
||||||
|
|
||||||
|
# Register your models here.
|
||||||
@admin.register(User)
|
|
||||||
class UserAdmin(DefaultUserAdmin):
|
|
||||||
list_display = ("username", "email", "role", "source_manager", "is_active", "is_staff", "last_login")
|
|
||||||
list_filter = ("role", "is_active", "is_staff", "is_superuser")
|
|
||||||
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
|
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
from ninja import Router, Form
|
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
from rest_framework_simplejwt.tokens import RefreshToken
|
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from invites.models import RegistrationCode
|
|
||||||
|
|
||||||
auth_router = Router(tags=["认证"])
|
|
||||||
User = get_user_model()
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.post("/register")
|
|
||||||
def register(
|
|
||||||
request,
|
|
||||||
username: str = Form(...),
|
|
||||||
password: str = Form(...),
|
|
||||||
email: str = Form(...),
|
|
||||||
code: str = Form(...)
|
|
||||||
):
|
|
||||||
if User.objects.filter(username=username).exists():
|
|
||||||
return {"success": False, "message": "用户名已存在"}
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
reg.used_count += 1
|
|
||||||
reg.save()
|
|
||||||
RegistrationCode.objects.create(code=reg, user=user)
|
|
||||||
|
|
||||||
refresh = RefreshToken.for_user(user)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "注册成功",
|
|
||||||
"user": {
|
|
||||||
"id": user.id,
|
|
||||||
"username": user.username,
|
|
||||||
"role": user.role,
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"access": str(refresh.access_token),
|
|
||||||
"refresh": str(refresh),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@auth_router.post("/login")
|
|
||||||
def login(
|
|
||||||
request,
|
|
||||||
username: str = Form(...),
|
|
||||||
password: str = Form(...),
|
|
||||||
):
|
|
||||||
user = User.objects.filter(Q(username=username) | Q(email=username)).first()
|
|
||||||
|
|
||||||
if not user or not user.check_password(password):
|
|
||||||
return {"success": False, "message": "用户名或密码错误"}
|
|
||||||
|
|
||||||
if not user.is_active:
|
|
||||||
return {"success": False, "message": "账号未激活"}
|
|
||||||
|
|
||||||
refresh = RefreshToken.for_user(user)
|
|
||||||
return {
|
|
||||||
"success": True,
|
|
||||||
"message": "登录成功",
|
|
||||||
"user": {
|
|
||||||
"id": user.id,
|
|
||||||
"username": user.username,
|
|
||||||
"role": user.role,
|
|
||||||
},
|
|
||||||
"token": {
|
|
||||||
"access": str(refresh.access_token),
|
|
||||||
"refresh": str(refresh),
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
from ninja import Router
|
|
||||||
from utils.permissions import login_required
|
|
||||||
from utils.auth import jwt_auth
|
|
||||||
|
|
||||||
user_router = Router(tags=["用户信息"])
|
|
||||||
|
|
||||||
@user_router.get("/me", auth=jwt_auth)
|
|
||||||
@login_required
|
|
||||||
def get_user_info(request):
|
|
||||||
user = request.user
|
|
||||||
return {
|
|
||||||
"id": user.id,
|
|
||||||
"username": user.username,
|
|
||||||
"email": user.email,
|
|
||||||
"role": user.role,
|
|
||||||
"is_active": user.is_active,
|
|
||||||
"is_staff": user.is_staff,
|
|
||||||
}
|
|
@ -4,6 +4,3 @@ from django.apps import AppConfig
|
|||||||
class AccountsConfig(AppConfig):
|
class AccountsConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'accounts'
|
name = 'accounts'
|
||||||
|
|
||||||
def ready(self):
|
|
||||||
import accounts.signals
|
|
@ -1,51 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 05:54
|
|
||||||
|
|
||||||
import django.contrib.auth.models
|
|
||||||
import django.contrib.auth.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
|
||||||
('websites', '__first__'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='User',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
||||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
||||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
||||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
||||||
('role', models.CharField(choices=[('admin', '管理员'), ('manager', '分管理'), ('user', '普通用户')], default='user', help_text='用户角色', max_length=20)),
|
|
||||||
('authorized_websites', models.ManyToManyField(blank=True, help_text='普通用户被授权可访问的网站', related_name='authorized_users', to='websites.website')),
|
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
|
|
||||||
('managed_websites', models.ManyToManyField(blank=True, help_text='分管理可管理的网站', related_name='managers', to='websites.website')),
|
|
||||||
('source_manager', models.ForeignKey(blank=True, limit_choices_to={'role': 'manager'}, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='brought_users', to=settings.AUTH_USER_MODEL, verbose_name='所属分管理')),
|
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'user',
|
|
||||||
'verbose_name_plural': 'users',
|
|
||||||
'abstract': False,
|
|
||||||
},
|
|
||||||
managers=[
|
|
||||||
('objects', django.contrib.auth.models.UserManager()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,43 +1,3 @@
|
|||||||
from django.contrib.auth.models import AbstractUser
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from websites.models import Website
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
# Create your models here.
|
||||||
ROLE_CHOICES = [
|
|
||||||
('admin', '管理员'),
|
|
||||||
('manager', '分管理'),
|
|
||||||
('user', '普通用户'),
|
|
||||||
]
|
|
||||||
role = models.CharField(max_length=20, choices=ROLE_CHOICES, default='user', help_text="用户角色")
|
|
||||||
managed_websites = models.ManyToManyField(
|
|
||||||
Website,
|
|
||||||
blank=True,
|
|
||||||
related_name="managers",
|
|
||||||
help_text="分管理可管理的网站"
|
|
||||||
)
|
|
||||||
authorized_websites = models.ManyToManyField(
|
|
||||||
Website,
|
|
||||||
blank=True,
|
|
||||||
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'
|
|
||||||
|
|
||||||
def is_manager(self):
|
|
||||||
return self.role == 'manager'
|
|
||||||
|
|
||||||
def is_user(self):
|
|
||||||
return self.role == 'user'
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.username} ({self.get_role_display()})"
|
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
from django.db.models.signals import post_save
|
|
||||||
from django.dispatch import receiver
|
|
||||||
from accounts.models import User
|
|
||||||
from invites.models import RegistrationCode
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save, sender=User)
|
|
||||||
def create_registration_code_for_manager(sender, instance, created, **kwargs):
|
|
||||||
if instance.role == "manager":
|
|
||||||
if created or not RegistrationCode.objects.filter(manager=instance).exists():
|
|
||||||
RegistrationCode.objects.create(
|
|
||||||
code=str(uuid.uuid4()).replace("-", "")[:12],
|
|
||||||
manager=instance,
|
|
||||||
description=f"{instance.username} 的默认邀请码",
|
|
||||||
usage_limit=10
|
|
||||||
)
|
|
||||||
elif instance.role == "user":
|
|
||||||
RegistrationCode.objects.filter(manager=instance).update(usage_limit=0)
|
|
@ -1,6 +1,6 @@
|
|||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
|
||||||
class AuthorizeConfig(AppConfig):
|
class AdminPanelConfig(AppConfig):
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
default_auto_field = 'django.db.models.BigAutoField'
|
||||||
name = 'authorize'
|
name = 'admin_panel'
|
3
admin_panel/models.py
Normal file
3
admin_panel/models.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
13
api.py
13
api.py
@ -1,13 +0,0 @@
|
|||||||
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 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("/auth", auth_router)
|
|
||||||
api.add_router("/users", user_router)
|
|
||||||
api.add_router("/website", website_authorize_router)
|
|
||||||
api.add_router("/resume", resume_authorize_router)
|
|
@ -1,18 +0,0 @@
|
|||||||
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')
|
|
@ -1,167 +0,0 @@
|
|||||||
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)}
|
|
@ -1,147 +0,0 @@
|
|||||||
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,51 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 05:55
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('resumes', '0001_initial'),
|
|
||||||
('websites', '0001_initial'),
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='WebsiteAccessRequest',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('status', models.CharField(choices=[('pending', '待审批'), ('approved', '已通过'), ('rejected', '已拒绝')], default='pending', max_length=20)),
|
|
||||||
('reason', models.TextField(blank=True)),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
|
||||||
('website', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='websites.website')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '网站访问申请',
|
|
||||||
'verbose_name_plural': '网站访问申请',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ResumeDetailAccessRequest',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('reason', models.TextField(blank=True, verbose_name='申请理由')),
|
|
||||||
('status', models.CharField(choices=[('pending', '待审批'), ('approved', '已通过'), ('rejected', '已拒绝')], default='pending', max_length=20, verbose_name='审批状态')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='申请时间')),
|
|
||||||
('resume', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='resumes.resumedetail', verbose_name='目标简历')),
|
|
||||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='申请用户')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '简历详情访问申请',
|
|
||||||
'verbose_name_plural': '简历详情访问申请',
|
|
||||||
'unique_together': {('user', 'resume')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,47 +0,0 @@
|
|||||||
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})"
|
|
@ -1,18 +0,0 @@
|
|||||||
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="申请理由")
|
|
@ -1,33 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 05:55
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('websites', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Company',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=200, verbose_name='企业名称')),
|
|
||||||
('category', models.CharField(max_length=100, verbose_name='公司类别')),
|
|
||||||
('size', models.CharField(max_length=50, verbose_name='公司规模')),
|
|
||||||
('introduction', models.TextField(verbose_name='企业介绍')),
|
|
||||||
('address', models.CharField(max_length=300, verbose_name='企业地址')),
|
|
||||||
('benefits', models.TextField(blank=True, verbose_name='企业福利')),
|
|
||||||
('website', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='companies', to='websites.website', verbose_name='隶属网站')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '企业',
|
|
||||||
'verbose_name_plural': '企业列表',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,48 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 08:17
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('companies', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='company',
|
|
||||||
name='company_type',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='企业类型:私企民营:国企外企等'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='company',
|
|
||||||
name='created_at',
|
|
||||||
field=models.DateTimeField(auto_now_add=True, null=True, verbose_name='入库时间'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='company',
|
|
||||||
name='founded_date',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='创办时间'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='company',
|
|
||||||
name='updated_at',
|
|
||||||
field=models.DateTimeField(auto_now=True, null=True, verbose_name='更新时间'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='company',
|
|
||||||
name='benefits',
|
|
||||||
field=models.TextField(blank=True, null=True, verbose_name='企业福利'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='company',
|
|
||||||
name='category',
|
|
||||||
field=models.CharField(blank=True, max_length=100, null=True, verbose_name='公司类别:这家企业是干啥的'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='company',
|
|
||||||
name='size',
|
|
||||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='公司规模'),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,29 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from websites.models import Website
|
|
||||||
|
|
||||||
|
|
||||||
class Company(models.Model):
|
|
||||||
name = models.CharField(max_length=200, verbose_name="企业名称")
|
|
||||||
category = models.CharField(max_length=100, verbose_name="公司类别:这家企业是干啥的", null=True, blank=True)
|
|
||||||
size = models.CharField(max_length=50, verbose_name="公司规模", null=True, blank=True)
|
|
||||||
introduction = models.TextField(verbose_name="企业介绍")
|
|
||||||
address = models.CharField(max_length=300, verbose_name="企业地址")
|
|
||||||
benefits = models.TextField(blank=True, verbose_name="企业福利", null=True)
|
|
||||||
website = models.ForeignKey(
|
|
||||||
Website,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
related_name="companies",
|
|
||||||
verbose_name="隶属网站"
|
|
||||||
)
|
|
||||||
company_type = models.CharField(max_length=100, verbose_name="企业类型:私企民营:国企外企等", null=True, blank=True)
|
|
||||||
founded_date = models.CharField(max_length=100, verbose_name="创办时间", null=True, blank=True)
|
|
||||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="入库时间", null=True)
|
|
||||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间", null=True)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.name
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "企业"
|
|
||||||
verbose_name_plural = "企业列表"
|
|
||||||
unique_together = (("name", "website"),)
|
|
@ -1,2 +0,0 @@
|
|||||||
import pymysql
|
|
||||||
pymysql.install_as_MySQLdb()
|
|
@ -11,6 +11,6 @@ import os
|
|||||||
|
|
||||||
from django.core.asgi import get_asgi_application
|
from django.core.asgi import get_asgi_application
|
||||||
|
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ts_reshub.settings')
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
|
@ -9,16 +9,12 @@ https://docs.djangoproject.com/en/5.0/topics/settings/
|
|||||||
For the full list of settings and their values, see
|
For the full list of settings and their values, see
|
||||||
https://docs.djangoproject.com/en/5.0/ref/settings/
|
https://docs.djangoproject.com/en/5.0/ref/settings/
|
||||||
"""
|
"""
|
||||||
import environ
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
env = environ.Env()
|
from pathlib import Path
|
||||||
|
|
||||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
# Quick-start development settings - unsuitable for production
|
||||||
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
|
||||||
@ -41,17 +37,6 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'ninja',
|
|
||||||
|
|
||||||
# 你自己的 apps 👇
|
|
||||||
'accounts', # 用户系统
|
|
||||||
'websites', # 网站系统
|
|
||||||
'resumes', # 简历系统
|
|
||||||
'logs', # 日志系统
|
|
||||||
'invites', # 邀请系统
|
|
||||||
'authorize', # 授权系统
|
|
||||||
'companies', # 公司信息
|
|
||||||
'positions' # 职位信息
|
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
@ -64,7 +49,7 @@ MIDDLEWARE = [
|
|||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = 'core.urls'
|
ROOT_URLCONF = 'ts_reshub.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
@ -82,7 +67,7 @@ TEMPLATES = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
WSGI_APPLICATION = 'core.wsgi.application'
|
WSGI_APPLICATION = 'ts_reshub.wsgi.application'
|
||||||
|
|
||||||
|
|
||||||
# Database
|
# Database
|
||||||
@ -90,18 +75,12 @@ WSGI_APPLICATION = 'core.wsgi.application'
|
|||||||
|
|
||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
'ENGINE': 'django.db.backends.sqlite3',
|
||||||
'NAME': env('DB_NAME'),
|
'NAME': BASE_DIR / 'db.sqlite3',
|
||||||
'USER': env('DB_USER'),
|
|
||||||
'PASSWORD': env('DB_PASSWORD'),
|
|
||||||
'HOST': env('DB_HOST'),
|
|
||||||
'PORT': env('DB_PORT'),
|
|
||||||
'OPTIONS': {
|
|
||||||
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
|
||||||
|
|
||||||
@ -120,23 +99,18 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
AUTH_USER_MODEL = 'accounts.User'
|
|
||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'zh-hans'
|
LANGUAGE_CODE = 'en-us'
|
||||||
|
|
||||||
TIME_ZONE = 'Asia/Shanghai'
|
TIME_ZONE = 'UTC'
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
USE_TZ = True
|
USE_TZ = True
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
|
||||||
"rest_framework_simplejwt.authentication.JWTAuthentication",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
# Static files (CSS, JavaScript, Images)
|
# Static files (CSS, JavaScript, Images)
|
||||||
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
# https://docs.djangoproject.com/en/5.0/howto/static-files/
|
||||||
|
@ -16,8 +16,7 @@ Including another URLconf
|
|||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from api import api
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
path('api/', api.urls),
|
|
||||||
]
|
]
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
from django.contrib import admin
|
|
||||||
from invites.models import RegistrationCode
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(RegistrationCode)
|
|
||||||
class RegistrationCodeAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ("code", "manager", "usage_limit", "used_count", "created_at")
|
|
||||||
list_filter = ("manager",)
|
|
||||||
search_fields = ("code", "manager__username")
|
|
||||||
readonly_fields = ("used_count", "created_at")
|
|
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class InvitesConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "invites"
|
|
@ -1,33 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 05:55
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='RegistrationCode',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('code', models.CharField(max_length=32, unique=True, verbose_name='注册码')),
|
|
||||||
('description', models.CharField(blank=True, max_length=100, verbose_name='说明')),
|
|
||||||
('usage_limit', models.IntegerField(default=1, verbose_name='最多使用次数')),
|
|
||||||
('used_count', models.IntegerField(default=0, verbose_name='已使用次数')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
|
||||||
('manager', models.ForeignKey(limit_choices_to={'role': 'manager'}, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='对应分管理')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '注册码申请',
|
|
||||||
'verbose_name_plural': '注册码申请',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,25 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from accounts.models import User
|
|
||||||
|
|
||||||
class RegistrationCode(models.Model):
|
|
||||||
code = models.CharField(max_length=32, unique=True, verbose_name="注册码")
|
|
||||||
manager = models.ForeignKey(
|
|
||||||
User,
|
|
||||||
on_delete=models.CASCADE,
|
|
||||||
limit_choices_to={"role": "manager"},
|
|
||||||
verbose_name="对应分管理"
|
|
||||||
)
|
|
||||||
description = models.CharField(max_length=100, blank=True, verbose_name="说明")
|
|
||||||
usage_limit = models.IntegerField(default=1, verbose_name="最多使用次数")
|
|
||||||
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})"
|
|
||||||
|
|
||||||
def is_available(self):
|
|
||||||
return self.used_count < self.usage_limit
|
|
@ -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.
|
|
@ -1,11 +1,3 @@
|
|||||||
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")
|
|
@ -1,31 +1,3 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.contrib.auth import get_user_model
|
|
||||||
|
|
||||||
User = get_user_model()
|
# Create your models here.
|
||||||
|
|
||||||
|
|
||||||
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}"
|
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class PositionsConfig(AppConfig):
|
|
||||||
default_auto_field = 'django.db.models.BigAutoField'
|
|
||||||
name = 'positions'
|
|
@ -1,50 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 05:55
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('companies', '0001_initial'),
|
|
||||||
('websites', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Position',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('title', models.CharField(max_length=200, verbose_name='职位名称')),
|
|
||||||
('nature', models.CharField(max_length=50, verbose_name='职位性质')),
|
|
||||||
('category', models.CharField(max_length=100, verbose_name='职位类别')),
|
|
||||||
('region', models.CharField(max_length=100, verbose_name='职位区域')),
|
|
||||||
('experience', models.CharField(max_length=100, verbose_name='工作经历要求')),
|
|
||||||
('education', models.CharField(max_length=100, verbose_name='学历要求')),
|
|
||||||
('salary', models.CharField(max_length=100, verbose_name='职位薪资')),
|
|
||||||
('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='positions', to='companies.company', verbose_name='所属企业')),
|
|
||||||
('website', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='positions', to='websites.website', verbose_name='所属网站')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '职位',
|
|
||||||
'verbose_name_plural': '职位列表',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='PositionDetail',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('description', models.TextField(verbose_name='职位描述(详情)')),
|
|
||||||
('contact_name', models.CharField(max_length=100, verbose_name='联系人姓名')),
|
|
||||||
('contact_info', models.CharField(max_length=200, verbose_name='联系方式')),
|
|
||||||
('position', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='detail', to='positions.position', verbose_name='所属职位')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '职位详情',
|
|
||||||
'verbose_name_plural': '职位详情列表',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,38 +0,0 @@
|
|||||||
from django.db import models
|
|
||||||
from websites.models import Website
|
|
||||||
from companies.models import Company
|
|
||||||
|
|
||||||
|
|
||||||
class Position(models.Model):
|
|
||||||
POSITION_STATUS_CHOICES = [
|
|
||||||
(0, '已下架'),
|
|
||||||
(1, '正在招聘'),
|
|
||||||
(2, '已结束'),
|
|
||||||
(3, '不确定'),
|
|
||||||
]
|
|
||||||
|
|
||||||
title = models.CharField(max_length=200, verbose_name="职位名称")
|
|
||||||
nature = models.CharField(max_length=50, verbose_name="职位性质", blank=True, null=True, )
|
|
||||||
category = models.CharField(max_length=100, verbose_name="职位类别", blank=True, null=True, )
|
|
||||||
region = models.CharField(max_length=100, verbose_name="职位区域", blank=True, null=True, )
|
|
||||||
experience = models.CharField(max_length=100, verbose_name="工作经历要求", blank=True, null=True, )
|
|
||||||
education = models.CharField(max_length=100, verbose_name="学历要求", blank=True, null=True, )
|
|
||||||
salary = models.CharField(max_length=100, verbose_name="职位薪资", blank=True, null=True, )
|
|
||||||
position_status = models.IntegerField(choices=POSITION_STATUS_CHOICES, default=0, verbose_name='职位状态',
|
|
||||||
blank=True, null=True, )
|
|
||||||
description = models.TextField(verbose_name="职位描述(详情)", blank=True, null=True, )
|
|
||||||
contact_name = models.CharField(max_length=100, verbose_name="联系人姓名", blank=True, null=True, )
|
|
||||||
contact_info = models.CharField(max_length=200, verbose_name="联系方式", blank=True, null=True, )
|
|
||||||
benefits = models.TextField(verbose_name="职位福利", blank=True, null=True, help_text="可用换行或逗号分隔多条福利")
|
|
||||||
openings = models.PositiveIntegerField(verbose_name="招聘人数", default=1, blank=True, null=True,
|
|
||||||
help_text="默认为 1")
|
|
||||||
website = models.ForeignKey(Website, on_delete=models.CASCADE, related_name="positions", verbose_name="所属网站")
|
|
||||||
company = models.ForeignKey(Company, on_delete=models.CASCADE, related_name="positions", verbose_name="所属企业")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.company.name} - {self.title}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "职位"
|
|
||||||
verbose_name_plural = "职位列表"
|
|
||||||
unique_together = ['title', 'website', 'company']
|
|
@ -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.
|
|
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@ -1,26 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import *
|
|
||||||
|
|
||||||
class ResumeDetailInline(admin.StackedInline):
|
# Register your models here.
|
||||||
model = ResumeDetail
|
|
||||||
extra = 0
|
|
||||||
|
|
||||||
@admin.register(ResumeDetail)
|
|
||||||
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 "-"
|
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
from ninja import Schema
|
|
||||||
from typing import Optional, List
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
# 简历单条记录输出结构
|
|
||||||
class ResumeBasicOut(Schema):
|
|
||||||
id: int
|
|
||||||
resume_id: int
|
|
||||||
name: Optional[str]
|
|
||||||
job_region: Optional[str]
|
|
||||||
birthday: Optional[str]
|
|
||||||
education: Optional[str]
|
|
||||||
school: Optional[str]
|
|
||||||
expected_position: Optional[str]
|
|
||||||
last_active_time: Optional[str]
|
|
||||||
marital_status: Optional[str]
|
|
||||||
current_location: Optional[str]
|
|
||||||
age: Optional[int]
|
|
||||||
phone: Optional[str]
|
|
||||||
gender: Optional[str]
|
|
||||||
job_type: Optional[str]
|
|
||||||
job_status: Optional[str]
|
|
||||||
|
|
||||||
work_1_experience: Optional[str]
|
|
||||||
work_1_time: Optional[str]
|
|
||||||
work_1_description: Optional[str]
|
|
||||||
|
|
||||||
work_2_experience: Optional[str]
|
|
||||||
work_2_time: Optional[str]
|
|
||||||
work_2_description: Optional[str]
|
|
||||||
|
|
||||||
work_3_experience: Optional[str]
|
|
||||||
work_3_time: Optional[str]
|
|
||||||
work_3_description: Optional[str]
|
|
||||||
|
|
||||||
work_4_experience: Optional[str]
|
|
||||||
work_4_time: Optional[str]
|
|
||||||
work_4_description: Optional[str]
|
|
||||||
|
|
||||||
height: Optional[int]
|
|
||||||
weight: Optional[int]
|
|
||||||
work_years: Optional[str]
|
|
||||||
highest_education: Optional[str]
|
|
||||||
ethnicity: Optional[str]
|
|
||||||
update_time: Optional[datetime]
|
|
||||||
job_function: Optional[str]
|
|
||||||
intended_position: Optional[str]
|
|
||||||
industry: Optional[str]
|
|
||||||
expected_salary: Optional[str]
|
|
||||||
available_time: Optional[str]
|
|
||||||
job_property: Optional[str]
|
|
||||||
job_location: Optional[str]
|
|
||||||
crawl_keywords: Optional[str]
|
|
||||||
source_id: Optional[int]
|
|
||||||
|
|
||||||
# 分页响应结构
|
|
||||||
class PaginatedResumes(Schema):
|
|
||||||
count: int
|
|
||||||
items: List[ResumeBasicOut]
|
|
@ -1,48 +0,0 @@
|
|||||||
from ninja import Router, Query
|
|
||||||
|
|
||||||
from accounts.models import User
|
|
||||||
from resumes.models import ResumeBasic
|
|
||||||
from resumes.api.schemas import ResumeBasicOut, PaginatedResumes
|
|
||||||
from typing import Optional
|
|
||||||
from utils.auth import jwt_auth
|
|
||||||
from utils.permissions import login_required
|
|
||||||
|
|
||||||
router = Router(tags=["简历"])
|
|
||||||
|
|
||||||
@router.get("/", response=PaginatedResumes, auth=jwt_auth)
|
|
||||||
@login_required
|
|
||||||
def list_resumes(
|
|
||||||
request,
|
|
||||||
job_status: Optional[str] = Query(None),
|
|
||||||
age: Optional[int] = Query(None),
|
|
||||||
name: Optional[str] = Query(None),
|
|
||||||
source_id: Optional[int] = Query(None),
|
|
||||||
keyword: Optional[str] = Query(None),
|
|
||||||
limit: int = 10,
|
|
||||||
offset: int = 0
|
|
||||||
):
|
|
||||||
user = request.user
|
|
||||||
qs = ResumeBasic.objects.all()
|
|
||||||
|
|
||||||
if user.is_admin():
|
|
||||||
pass # 管理员访问全部
|
|
||||||
elif user.is_manager():
|
|
||||||
qs = qs.filter(source_id__in=user.managed_websites.values_list("id", flat=True))
|
|
||||||
elif user.is_user():
|
|
||||||
qs = qs.filter(source_id__in=user.authorized_websites.values_list("id", flat=True))
|
|
||||||
|
|
||||||
if job_status:
|
|
||||||
qs = qs.filter(job_status=job_status)
|
|
||||||
if age:
|
|
||||||
qs = qs.filter(age=age)
|
|
||||||
if name:
|
|
||||||
qs = qs.filter(name__icontains=name)
|
|
||||||
if source_id:
|
|
||||||
qs = qs.filter(source_id=source_id)
|
|
||||||
if keyword:
|
|
||||||
qs = qs.filter(crawl_keywords__icontains=keyword)
|
|
||||||
|
|
||||||
total = qs.count()
|
|
||||||
results = qs[offset:offset + limit]
|
|
||||||
|
|
||||||
return {"count": total, "items": list(results)}
|
|
@ -1,139 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from pandas._libs.tslibs.timestamps import Timestamp
|
|
||||||
import pandas as pd
|
|
||||||
from resumes.models import ResumeBasic
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "导入会计类简历(支持 --keyword 和 --source 参数)"
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument('--file', required=True, help='Excel 文件路径')
|
|
||||||
parser.add_argument('--keyword', default='', help='crawl_keywords 值')
|
|
||||||
parser.add_argument('--source', type=int, default=2, help='source_id 值')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
filepath = options['file']
|
|
||||||
default_keyword = options['keyword']
|
|
||||||
default_source = options['source']
|
|
||||||
df = pd.read_excel(filepath)
|
|
||||||
|
|
||||||
rename_map = {
|
|
||||||
'姓名': 'name', '性别': 'gender', '年龄': 'age',
|
|
||||||
'手机': 'phone', '婚姻状况': 'marital_status', '身高': 'height', '体重': 'weight',
|
|
||||||
'学历': 'education', '毕业学校': 'school', '工作经验': 'work_years',
|
|
||||||
'现居住地': 'current_location', '工作地点': 'job_location', '到岗时间': 'available_time',
|
|
||||||
'更新时间': 'update_time', '最高学历': 'education', '婚姻状态': 'marital_status',
|
|
||||||
'民族': 'ethnicity', '工作职能': 'job_function', '意向岗位': 'intended_position',
|
|
||||||
'从事行业': 'industry', '期望薪资': 'expected_salary', '工作性质': 'job_property',
|
|
||||||
'求职状态': 'job_status',
|
|
||||||
}
|
|
||||||
df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns}, inplace=True)
|
|
||||||
|
|
||||||
df['source_id'] = default_source
|
|
||||||
df['crawl_keywords'] = default_keyword
|
|
||||||
|
|
||||||
def parse_update_time(val):
|
|
||||||
if pd.isna(val):
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
val = str(val)
|
|
||||||
now = datetime.now()
|
|
||||||
if "刚刚" in val:
|
|
||||||
return now
|
|
||||||
if "小时前" in val:
|
|
||||||
hours = int(re.search(r'\d+', val).group())
|
|
||||||
return now - timedelta(hours=hours)
|
|
||||||
if "天前" in val:
|
|
||||||
days = int(re.search(r'\d+', val).group())
|
|
||||||
return now - timedelta(days=days)
|
|
||||||
try:
|
|
||||||
dt = pd.to_datetime(val)
|
|
||||||
return dt.to_pydatetime()
|
|
||||||
except Exception:
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
|
|
||||||
df['update_time'] = df['update_time'].apply(parse_update_time)
|
|
||||||
|
|
||||||
def val(v, field=None):
|
|
||||||
if v is None or pd.isna(v):
|
|
||||||
if field == 'update_time':
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
return None
|
|
||||||
if field == 'update_time':
|
|
||||||
if isinstance(v, Timestamp):
|
|
||||||
return v.to_pydatetime()
|
|
||||||
if isinstance(v, str):
|
|
||||||
try:
|
|
||||||
return pd.to_datetime(v).to_pydatetime()
|
|
||||||
except Exception:
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
if isinstance(v, datetime):
|
|
||||||
return v
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
if isinstance(v, Timestamp):
|
|
||||||
return v.to_pydatetime()
|
|
||||||
return v
|
|
||||||
|
|
||||||
success_count = 0
|
|
||||||
fail_count = 0
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for i, row in df.iterrows():
|
|
||||||
try:
|
|
||||||
resume_id = val(row.get('resume_id'))
|
|
||||||
defaults = {
|
|
||||||
'name': val(row.get('name')),
|
|
||||||
'gender': val(row.get('gender')),
|
|
||||||
'age': val(row.get('age')),
|
|
||||||
'phone': val(row.get('phone')),
|
|
||||||
'marital_status': val(row.get('marital_status')),
|
|
||||||
'height': val(row.get('height')),
|
|
||||||
'weight': val(row.get('weight')),
|
|
||||||
'education': val(row.get('education')),
|
|
||||||
'school': val(row.get('school')),
|
|
||||||
'work_years': val(row.get('work_years')),
|
|
||||||
'current_location': val(row.get('current_location')),
|
|
||||||
'job_location': val(row.get('job_location')),
|
|
||||||
'available_time': val(row.get('available_time')),
|
|
||||||
'update_time': val(row['update_time'], field='update_time'),
|
|
||||||
'ethnicity': val(row.get('ethnicity')),
|
|
||||||
'job_function': val(row.get('job_function')),
|
|
||||||
'intended_position': val(row.get('intended_position')),
|
|
||||||
'industry': val(row.get('industry')),
|
|
||||||
'expected_salary': val(row.get('expected_salary')),
|
|
||||||
'job_property': val(row.get('job_property')),
|
|
||||||
'job_status': val(row.get('job_status')),
|
|
||||||
'source_id': val(row.get('source_id')),
|
|
||||||
'crawl_keywords': val(row.get('crawl_keywords')),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 安全方式:get_or_create + 逐字段 set
|
|
||||||
obj, _ = ResumeBasic.objects.get_or_create(resume_id=resume_id)
|
|
||||||
|
|
||||||
for k, v in defaults.items():
|
|
||||||
try:
|
|
||||||
setattr(obj, k, v)
|
|
||||||
except Exception as field_error:
|
|
||||||
print(f"[字段设置错误] {k} = {v!r} ({type(v)}) → {field_error}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
obj.save()
|
|
||||||
success_count += 1
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
fail_count += 1
|
|
||||||
errors.append((i + 2, str(e)))
|
|
||||||
print(f"\n❌ 第 {i + 2} 行出错:{e}")
|
|
||||||
print(f"resume_id: {repr(resume_id)} ({type(resume_id).__name__})")
|
|
||||||
for k, v in defaults.items():
|
|
||||||
print(f"{k:<20} | {repr(v):<30} | {type(v).__name__}")
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"导入完成!总数:{len(df)},成功:{success_count},失败:{fail_count}"))
|
|
||||||
if errors:
|
|
||||||
self.stdout.write(self.style.WARNING("失败记录如下:"))
|
|
||||||
for line_no, msg in errors:
|
|
||||||
self.stdout.write(f" 第 {line_no} 行出错:{msg}")
|
|
@ -1,147 +0,0 @@
|
|||||||
from datetime import datetime, timedelta
|
|
||||||
from django.core.management.base import BaseCommand
|
|
||||||
from pandas._libs.tslibs.timestamps import Timestamp
|
|
||||||
import pandas as pd
|
|
||||||
from resumes.models import ResumeBasic
|
|
||||||
import re
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "导入会计类简历(支持 --keyword 和 --source 参数)"
|
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
|
||||||
parser.add_argument('--file', required=True, help='Excel 文件路径')
|
|
||||||
parser.add_argument('--keyword', default='', help='crawl_keywords 值')
|
|
||||||
parser.add_argument('--source', type=int, default=1, help='source_id 值')
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
filepath = options['file']
|
|
||||||
default_keyword = options['keyword']
|
|
||||||
default_source = options['source']
|
|
||||||
df = pd.read_excel(filepath)
|
|
||||||
|
|
||||||
rename_map = {
|
|
||||||
'姓名': 'name', '性别': 'gender', '年龄': 'age', '求职区域': 'job_location', '生日': 'birthday',
|
|
||||||
'学校': 'school', '期望职务': 'expected_position',
|
|
||||||
'手机': 'phone', '婚姻': 'marital_status', '身高': 'height', '体重': 'weight', '电话': 'phone',
|
|
||||||
'学历': 'education', '毕业学校': 'school', '工作经验': 'work_years',
|
|
||||||
'现居住地': 'current_location', '工作地点': 'job_location', '到岗时间': 'available_time',
|
|
||||||
'最后活跃时间': 'update_time', '最高学历': 'education', '婚姻状态': 'marital_status',
|
|
||||||
'民族': 'ethnicity', '工作职能': 'job_function', '意向岗位': 'intended_position',
|
|
||||||
'从事行业': 'industry', '期望薪资': 'expected_salary', '求职类型': 'job_property',
|
|
||||||
'现居地': 'current_location',
|
|
||||||
'求职状态': 'job_status', '工作1经历': 'work_1_experience', '工作1时间': 'work_1_time',
|
|
||||||
'工作1内容': 'work_1_description',
|
|
||||||
'工作2经历': 'work_2_experience', '工作2时间': 'work_2_time', '工作2内容': 'work_2_description',
|
|
||||||
'工作3经历': 'work_3_experience', '工作3时间': 'work_3_time', '工作3内容': 'work_3_description',
|
|
||||||
'工作4经历': 'work_4_experience', '工作4时间': 'work_4_time', '工作4内容': 'work_4_description',
|
|
||||||
}
|
|
||||||
df.rename(columns={k: v for k, v in rename_map.items() if k in df.columns}, inplace=True)
|
|
||||||
|
|
||||||
df['source_id'] = default_source
|
|
||||||
df['crawl_keywords'] = default_keyword
|
|
||||||
|
|
||||||
def val(v, field=None):
|
|
||||||
if v is None or pd.isna(v):
|
|
||||||
if field == 'update_time':
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
return None
|
|
||||||
if field == 'update_time':
|
|
||||||
if isinstance(v, Timestamp):
|
|
||||||
return v.to_pydatetime()
|
|
||||||
if isinstance(v, str):
|
|
||||||
try:
|
|
||||||
return pd.to_datetime(v).to_pydatetime()
|
|
||||||
except Exception:
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
if isinstance(v, datetime):
|
|
||||||
return v
|
|
||||||
return datetime(2019, 12, 12)
|
|
||||||
if isinstance(v, Timestamp):
|
|
||||||
return v.to_pydatetime()
|
|
||||||
return v
|
|
||||||
|
|
||||||
success_count = 0
|
|
||||||
fail_count = 0
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for i, row in df.iterrows():
|
|
||||||
try:
|
|
||||||
resume_id = val(row.get('resume_id'))
|
|
||||||
|
|
||||||
# 自动从年龄字段中提取整数(如 "38岁" → 38)
|
|
||||||
raw_age = row.get('age')
|
|
||||||
try:
|
|
||||||
extracted_age = int(re.search(r'\d+', str(raw_age)).group()) if raw_age else None
|
|
||||||
except:
|
|
||||||
extracted_age = None
|
|
||||||
|
|
||||||
defaults = {
|
|
||||||
'name': val(row.get('name')),
|
|
||||||
'gender': val(row.get('gender')),
|
|
||||||
'age': extracted_age,
|
|
||||||
'phone': val(row.get('phone')),
|
|
||||||
'marital_status': val(row.get('marital_status')),
|
|
||||||
'height': val(row.get('height')),
|
|
||||||
'weight': val(row.get('weight')),
|
|
||||||
'education': val(row.get('education')),
|
|
||||||
'school': val(row.get('school')),
|
|
||||||
'work_years': val(row.get('work_years')),
|
|
||||||
'current_location': val(row.get('current_location')),
|
|
||||||
'job_location': val(row.get('job_location')),
|
|
||||||
'available_time': val(row.get('available_time')),
|
|
||||||
'update_time': val(row['update_time'], field='update_time'),
|
|
||||||
'ethnicity': val(row.get('ethnicity')),
|
|
||||||
'job_function': val(row.get('job_function')),
|
|
||||||
'intended_position': val(row.get('intended_position')),
|
|
||||||
'industry': val(row.get('industry')),
|
|
||||||
'expected_salary': val(row.get('expected_salary')),
|
|
||||||
'job_property': val(row.get('job_property')),
|
|
||||||
'job_status': val(row.get('job_status')),
|
|
||||||
'source_id': val(row.get('source_id')),
|
|
||||||
'crawl_keywords': val(row.get('crawl_keywords')),
|
|
||||||
'birthday': val(row.get('birthday')),
|
|
||||||
'expected_position': val(row.get('expected_position')),
|
|
||||||
'work_1_experience': val(row.get('work_1_experience')),
|
|
||||||
'work_1_time': val(row.get('work_1_time')),
|
|
||||||
'work_1_description': val(row.get('work_1_description')),
|
|
||||||
'work_2_experience': val(row.get('work_2_experience')),
|
|
||||||
'work_2_time': val(row.get('work_2_time')),
|
|
||||||
'work_2_description': val(row.get('work_2_description')),
|
|
||||||
'work_3_experience': val(row.get('work_3_experience')),
|
|
||||||
'work_3_time': val(row.get('work_3_time')),
|
|
||||||
'work_3_description': val(row.get('work_3_description')),
|
|
||||||
'work_4_experience': val(row.get('work_4_experience')),
|
|
||||||
'work_4_time': val(row.get('work_4_time')),
|
|
||||||
'work_4_description': val(row.get('work_4_description')),
|
|
||||||
}
|
|
||||||
|
|
||||||
# 安全方式:get_or_create + 逐字段 set
|
|
||||||
obj, _ = ResumeBasic.objects.get_or_create(resume_id=resume_id)
|
|
||||||
|
|
||||||
for k, v in defaults.items():
|
|
||||||
try:
|
|
||||||
setattr(obj, k, v)
|
|
||||||
except Exception as field_error:
|
|
||||||
print(f"[字段设置错误] {k} = {v!r} ({type(v)}) → {field_error}")
|
|
||||||
raise
|
|
||||||
|
|
||||||
obj.save()
|
|
||||||
success_count += 1
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
fail_count += 1
|
|
||||||
errors.append((i + 2, str(e)))
|
|
||||||
print(f"\n❌ 第 {i + 2} 行出错:{e}")
|
|
||||||
print(f"resume_id: {repr(resume_id)} ({type(resume_id).__name__})")
|
|
||||||
for k, v in defaults.items():
|
|
||||||
print(f"{k:<20} | {repr(v):<30} | {type(v).__name__}")
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"导入完成!总数:{len(df)},成功:{success_count},失败:{fail_count}"))
|
|
||||||
if errors:
|
|
||||||
self.stdout.write(self.style.WARNING("失败记录如下:"))
|
|
||||||
for line_no, msg in errors:
|
|
||||||
self.stdout.write(f" 第 {line_no} 行出错:{msg}")
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
|||||||
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}")
|
|
@ -1,84 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 05:55
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('websites', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ResumeBasic',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('resume_id', models.IntegerField(db_index=True, help_text='resume_id', verbose_name='简历ID')),
|
|
||||||
('name', models.CharField(blank=True, help_text='姓名', max_length=255, null=True, verbose_name='姓名')),
|
|
||||||
('job_region', models.CharField(blank=True, help_text='求职区域', max_length=255, null=True, verbose_name='求职区域')),
|
|
||||||
('birthday', models.CharField(blank=True, help_text='生日', max_length=255, null=True, verbose_name='生日')),
|
|
||||||
('education', models.CharField(blank=True, help_text='学历', max_length=255, null=True, verbose_name='学历')),
|
|
||||||
('school', models.CharField(blank=True, help_text='学校', max_length=255, null=True, verbose_name='学校')),
|
|
||||||
('expected_position', models.CharField(blank=True, help_text='期望职务', max_length=255, null=True, verbose_name='期望职务')),
|
|
||||||
('last_active_time', models.CharField(blank=True, help_text='最后活跃时间', max_length=255, null=True, verbose_name='最后活跃时间')),
|
|
||||||
('marital_status', models.CharField(blank=True, help_text='婚姻', max_length=255, null=True, verbose_name='婚姻')),
|
|
||||||
('current_location', models.CharField(blank=True, help_text='现居地', max_length=255, null=True, verbose_name='现居地')),
|
|
||||||
('age', models.IntegerField(blank=True, help_text='年龄', null=True, verbose_name='年龄')),
|
|
||||||
('phone', models.CharField(blank=True, help_text='电话', max_length=255, null=True, verbose_name='电话')),
|
|
||||||
('gender', models.CharField(blank=True, help_text='性别', max_length=255, null=True, verbose_name='性别')),
|
|
||||||
('job_type', models.CharField(blank=True, help_text='求职类型', max_length=255, null=True, verbose_name='求职类型')),
|
|
||||||
('job_status', models.CharField(blank=True, help_text='求职状态', max_length=255, null=True, verbose_name='求职状态')),
|
|
||||||
('work_1_experience', models.TextField(blank=True, help_text='工作1经历', null=True, verbose_name='工作1经历')),
|
|
||||||
('work_1_time', models.CharField(blank=True, help_text='工作1时间', max_length=255, null=True, verbose_name='工作1时间')),
|
|
||||||
('work_1_description', models.TextField(blank=True, help_text='工作1内容', null=True, verbose_name='工作1内容')),
|
|
||||||
('work_2_experience', models.TextField(blank=True, help_text='工作2经历', null=True, verbose_name='工作2经历')),
|
|
||||||
('work_2_time', models.CharField(blank=True, help_text='工作2时间', max_length=255, null=True, verbose_name='工作2时间')),
|
|
||||||
('work_2_description', models.TextField(blank=True, help_text='工作2内容', null=True, verbose_name='工作2内容')),
|
|
||||||
('work_3_experience', models.TextField(blank=True, help_text='工作3经历', null=True, verbose_name='工作3经历')),
|
|
||||||
('work_3_time', models.CharField(blank=True, help_text='工作3时间', max_length=255, null=True, verbose_name='工作3时间')),
|
|
||||||
('work_3_description', models.TextField(blank=True, help_text='工作3内容', null=True, verbose_name='工作3内容')),
|
|
||||||
('work_4_experience', models.TextField(blank=True, help_text='工作4经历', null=True, verbose_name='工作4经历')),
|
|
||||||
('work_4_time', models.CharField(blank=True, help_text='工作4时间', max_length=255, null=True, verbose_name='工作4时间')),
|
|
||||||
('work_4_description', models.TextField(blank=True, help_text='工作4内容', null=True, verbose_name='工作4内容')),
|
|
||||||
('height', models.IntegerField(blank=True, help_text='身高', null=True, verbose_name='身高')),
|
|
||||||
('weight', models.IntegerField(blank=True, help_text='体重', null=True, verbose_name='体重')),
|
|
||||||
('work_years', models.CharField(blank=True, help_text='工作经验', max_length=255, null=True, verbose_name='工作经验')),
|
|
||||||
('highest_education', models.CharField(blank=True, help_text='最高学历', max_length=255, null=True, verbose_name='最高学历')),
|
|
||||||
('ethnicity', models.CharField(blank=True, help_text='民族', max_length=255, null=True, verbose_name='民族')),
|
|
||||||
('update_time', models.DateTimeField(blank=True, help_text='更新时间', null=True, verbose_name='更新时间')),
|
|
||||||
('job_function', models.CharField(blank=True, help_text='工作职能', max_length=255, null=True, verbose_name='工作职能')),
|
|
||||||
('intended_position', models.CharField(blank=True, help_text='意向岗位', max_length=255, null=True, verbose_name='意向岗位')),
|
|
||||||
('industry', models.CharField(blank=True, help_text='从事行业', max_length=255, null=True, verbose_name='从事行业')),
|
|
||||||
('expected_salary', models.CharField(blank=True, help_text='期望薪资', max_length=255, null=True, verbose_name='期望薪资')),
|
|
||||||
('available_time', models.CharField(blank=True, help_text='到岗时间', max_length=255, null=True, verbose_name='到岗时间')),
|
|
||||||
('job_property', models.CharField(blank=True, help_text='工作性质', max_length=255, null=True, verbose_name='工作性质')),
|
|
||||||
('job_location', models.CharField(blank=True, help_text='工作地点', max_length=255, null=True, verbose_name='工作地点')),
|
|
||||||
('crawl_keywords', models.CharField(blank=True, help_text='关键字', max_length=255, null=True, verbose_name='关键字')),
|
|
||||||
('source', models.ForeignKey(blank=True, help_text='数据来源网站', null=True, on_delete=django.db.models.deletion.SET_NULL, to='websites.website', verbose_name='数据来源')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '简历',
|
|
||||||
'verbose_name_plural': '简历列表',
|
|
||||||
'unique_together': {('source', 'resume_id')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ResumeDetail',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('unlinked_resume_id', models.IntegerField(blank=True, null=True, verbose_name='无法关联的简历ID')),
|
|
||||||
('phone', models.CharField(blank=True, max_length=20, verbose_name='联系方式')),
|
|
||||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='邮箱')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True)),
|
|
||||||
('resume', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='detail', to='resumes.resumebasic', verbose_name='简历')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '简历详情',
|
|
||||||
'verbose_name_plural': '简历详情',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,109 +1,3 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
from websites.models import Website
|
|
||||||
|
|
||||||
|
|
||||||
class ResumeBasic(models.Model):
|
|
||||||
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="求职区域")
|
|
||||||
birthday = models.CharField(max_length=255, null=True, blank=True, verbose_name="生日", help_text="生日")
|
|
||||||
education = models.CharField(max_length=255, null=True, blank=True, verbose_name="学历", help_text="学历")
|
|
||||||
school = models.CharField(max_length=255, null=True, blank=True, verbose_name="学校", help_text="学校")
|
|
||||||
expected_position = models.CharField(max_length=255, null=True, blank=True, verbose_name="期望职务",
|
|
||||||
help_text="期望职务")
|
|
||||||
last_active_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="最后活跃时间",
|
|
||||||
help_text="最后活跃时间")
|
|
||||||
marital_status = models.CharField(max_length=255, null=True, blank=True, verbose_name="婚姻", help_text="婚姻")
|
|
||||||
current_location = models.CharField(max_length=255, null=True, blank=True, verbose_name="现居地",
|
|
||||||
help_text="现居地")
|
|
||||||
age = models.IntegerField(null=True, blank=True, verbose_name="年龄", help_text="年龄")
|
|
||||||
phone = models.CharField(max_length=255, null=True, blank=True, verbose_name="电话", help_text="电话")
|
|
||||||
gender = models.CharField(max_length=255, null=True, blank=True, verbose_name="性别", help_text="性别")
|
|
||||||
job_type = models.CharField(max_length=255, null=True, blank=True, verbose_name="求职类型",
|
|
||||||
help_text="求职类型")
|
|
||||||
job_status = models.CharField(max_length=255, null=True, blank=True, verbose_name="求职状态",
|
|
||||||
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_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_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_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_description = models.TextField(null=True, blank=True, verbose_name="工作4内容", help_text="工作4内容")
|
|
||||||
|
|
||||||
height = models.IntegerField(null=True, blank=True, verbose_name="身高", help_text="身高")
|
|
||||||
weight = models.IntegerField(null=True, blank=True, verbose_name="体重", help_text="体重")
|
|
||||||
work_years = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作经验", help_text="工作经验")
|
|
||||||
highest_education = models.CharField(max_length=255, null=True, blank=True, verbose_name="最高学历",
|
|
||||||
help_text="最高学历")
|
|
||||||
ethnicity = models.CharField(max_length=255, null=True, blank=True, verbose_name="民族", help_text="民族")
|
|
||||||
update_time = models.DateTimeField(null=True, blank=True, verbose_name="更新时间", help_text="更新时间")
|
|
||||||
job_function = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作职能",
|
|
||||||
help_text="工作职能")
|
|
||||||
intended_position = models.CharField(max_length=255, null=True, blank=True, verbose_name="意向岗位",
|
|
||||||
help_text="意向岗位")
|
|
||||||
industry = models.CharField(max_length=255, null=True, blank=True, verbose_name="从事行业",
|
|
||||||
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="到岗时间")
|
|
||||||
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="工作地点",
|
|
||||||
help_text="工作地点")
|
|
||||||
source = models.ForeignKey(
|
|
||||||
Website,
|
|
||||||
null=True,
|
|
||||||
blank=True,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
verbose_name="数据来源",
|
|
||||||
help_text="数据来源网站"
|
|
||||||
)
|
|
||||||
crawl_keywords = models.CharField(max_length=255, null=True, blank=True, verbose_name="关键字", help_text="关键字")
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{self.name} - {self.resume_id} - {self.name}"
|
|
||||||
|
|
||||||
class Meta:
|
|
||||||
verbose_name = "简历"
|
|
||||||
verbose_name_plural = "简历列表"
|
|
||||||
unique_together = ('source', 'resume_id')
|
|
||||||
|
|
||||||
|
|
||||||
class ResumeDetail(models.Model):
|
|
||||||
resume = models.OneToOneField(
|
|
||||||
ResumeBasic,
|
|
||||||
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:
|
|
||||||
verbose_name = "简历详情"
|
|
||||||
verbose_name_plural = "简历详情"
|
|
@ -1,16 +0,0 @@
|
|||||||
from ninja.security import HttpBearer
|
|
||||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
|
||||||
|
|
||||||
|
|
||||||
class JWTAuth(HttpBearer):
|
|
||||||
def authenticate(self, request, token):
|
|
||||||
jwt_auth = JWTAuthentication()
|
|
||||||
try:
|
|
||||||
validated_token = jwt_auth.get_validated_token(token)
|
|
||||||
user = jwt_auth.get_user(validated_token)
|
|
||||||
return user
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
jwt_auth = JWTAuth()
|
|
@ -1,32 +0,0 @@
|
|||||||
from functools import wraps
|
|
||||||
from ninja.errors import HttpError
|
|
||||||
|
|
||||||
def login_required(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(request, *args, **kwargs):
|
|
||||||
user = getattr(request, 'auth', None)
|
|
||||||
if not user or not user.is_authenticated:
|
|
||||||
raise HttpError(401, "请先登录")
|
|
||||||
request.user = user
|
|
||||||
return func(request, *args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def manager_required(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(request, *args, **kwargs):
|
|
||||||
user = getattr(request, 'auth', None)
|
|
||||||
if not user or not user.is_authenticated or user.role not in ['admin', 'manager']:
|
|
||||||
raise HttpError(403, "仅分管理或管理员可访问")
|
|
||||||
request.user = user
|
|
||||||
return func(request, *args, **kwargs)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
def admin_required(func):
|
|
||||||
@wraps(func)
|
|
||||||
def wrapper(request, *args, **kwargs):
|
|
||||||
user = getattr(request, 'auth', None)
|
|
||||||
if not user or not user.is_authenticated or user.role != 'admin':
|
|
||||||
raise HttpError(403, "仅管理员可访问")
|
|
||||||
request.user = user
|
|
||||||
return func(request, *args, **kwargs)
|
|
||||||
return wrapper
|
|
@ -1,7 +1,3 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from .models import *
|
|
||||||
# Register your models here.
|
|
||||||
|
|
||||||
@admin.register(Website)
|
# Register your models here.
|
||||||
class WebsiteAdmin(admin.ModelAdmin):
|
|
||||||
list_display = ('name', 'db_alias', 'db_alias')
|
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
# Generated by Django 5.2 on 2025-05-24 05:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='Website',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(max_length=100, verbose_name='网站名称')),
|
|
||||||
('db_alias', models.CharField(max_length=50, unique=True, verbose_name='数据库别名')),
|
|
||||||
('description', models.TextField(blank=True, verbose_name='描述')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': '网站',
|
|
||||||
'verbose_name_plural': '网站列表',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,14 +1,3 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
# Create your models here.
|
||||||
class Website(models.Model):
|
|
||||||
name = models.CharField(max_length=100, verbose_name="网站名称")
|
|
||||||
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