Compare commits

..

No commits in common. "dev" and "main" have entirely different histories.
dev ... main

75 changed files with 27 additions and 1941 deletions

56
.gitignore vendored
View File

@ -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
View File

@ -1,8 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

View File

@ -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
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

View File

@ -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

View File

@ -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),
}
}

View File

@ -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,
}

View File

@ -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

View File

@ -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()),
],
),
]

View File

@ -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()})"

View File

@ -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)

View File

@ -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
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

13
api.py
View File

@ -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)

View File

@ -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')

View File

@ -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)}

View File

@ -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}

View File

@ -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')},
},
),
]

View File

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

View File

@ -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="申请理由")

View File

@ -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': '企业列表',
},
),
]

View File

@ -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='公司规模'),
),
]

View File

@ -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"),)

View File

@ -1,2 +0,0 @@
import pymysql
pymysql.install_as_MySQLdb()

View File

@ -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()

View File

@ -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/

View File

@ -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),
] ]

View File

View File

@ -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")

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class InvitesConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "invites"

View File

@ -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': '注册码申请',
},
),
]

View File

@ -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

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

View File

@ -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")

View File

@ -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}"

View File

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class PositionsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'positions'

View File

@ -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': '职位详情列表',
},
),
]

View File

@ -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']

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,3 +0,0 @@
from django.shortcuts import render
# Create your views here.

Binary file not shown.

View File

@ -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 "-"

View File

@ -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),
}
}

View File

@ -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]

View File

@ -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)}

View File

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

View File

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

View File

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

View File

@ -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': '简历详情',
},
),
]

View File

@ -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 = "简历详情"

View File

View File

@ -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()

View File

@ -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

View File

@ -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')

View File

@ -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': '网站列表',
},
),
]

View File

@ -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 = "网站列表"