ninjia api 简历(基础信息)接口 和文档 导入 Excle 目前未完善
This commit is contained in:
parent
1366de574b
commit
286cbe907b
5
api.py
Normal file
5
api.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from ninja import NinjaAPI
|
||||||
|
from resumes.api.views import router as resume_router
|
||||||
|
|
||||||
|
api = NinjaAPI(title="简历管理 API")
|
||||||
|
api.add_router("/resumes/", resume_router)
|
@ -14,11 +14,11 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
env = environ.Env()
|
env = environ.Env()
|
||||||
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
|
||||||
|
|
||||||
# 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/
|
||||||
@ -122,9 +122,9 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
# Internationalization
|
# Internationalization
|
||||||
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
# https://docs.djangoproject.com/en/5.0/topics/i18n/
|
||||||
|
|
||||||
LANGUAGE_CODE = 'en-us'
|
LANGUAGE_CODE = 'zh-hans'
|
||||||
|
|
||||||
TIME_ZONE = 'UTC'
|
TIME_ZONE = 'Asia/Shanghai'
|
||||||
|
|
||||||
USE_I18N = True
|
USE_I18N = True
|
||||||
|
|
||||||
|
@ -16,7 +16,8 @@ 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,3 +1,9 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import *
|
||||||
|
|
||||||
|
from .models import *
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
|
@admin.register(ResumeBasic)
|
||||||
|
class ResumeBasicAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ['resume_id', 'name', 'job_region', 'birthday', 'expected_position']
|
0
resumes/api/__init__.py
Normal file
0
resumes/api/__init__.py
Normal file
59
resumes/api/schemas.py
Normal file
59
resumes/api/schemas.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
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[datetime]
|
||||||
|
work_1_description: Optional[str]
|
||||||
|
|
||||||
|
work_2_experience: Optional[str]
|
||||||
|
work_2_time: Optional[datetime]
|
||||||
|
work_2_description: Optional[str]
|
||||||
|
|
||||||
|
work_3_experience: Optional[str]
|
||||||
|
work_3_time: Optional[datetime]
|
||||||
|
work_3_description: Optional[str]
|
||||||
|
|
||||||
|
work_4_experience: Optional[str]
|
||||||
|
work_4_time: Optional[datetime]
|
||||||
|
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]
|
29
resumes/api/views.py
Normal file
29
resumes/api/views.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
from ninja import Router, Query
|
||||||
|
from resumes.models import ResumeBasic
|
||||||
|
from resumes.api.schemas import ResumeBasicOut, PaginatedResumes
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
router = Router(tags=["简历"])
|
||||||
|
|
||||||
|
@router.get("/", response=PaginatedResumes)
|
||||||
|
def list_resumes(
|
||||||
|
request,
|
||||||
|
job_status: Optional[str] = Query(None),
|
||||||
|
age: Optional[int] = Query(None),
|
||||||
|
name: Optional[str] = Query(None),
|
||||||
|
limit: int = 10,
|
||||||
|
offset: int = 0
|
||||||
|
):
|
||||||
|
qs = ResumeBasic.objects.all()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
total = qs.count()
|
||||||
|
results = qs[offset:offset + limit]
|
||||||
|
|
||||||
|
return {"count": total, "items": list(results)}
|
0
resumes/management/__init__.py
Normal file
0
resumes/management/__init__.py
Normal file
0
resumes/management/commands/__init__.py
Normal file
0
resumes/management/commands/__init__.py
Normal file
100
resumes/management/commands/import_accounting_resumes.py
Normal file
100
resumes/management/commands/import_accounting_resumes.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
import pandas as pd
|
||||||
|
from resumes.models import ResumeBasic
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
'简历ID': 'resume_id', '姓名': 'name', '性别': 'gender', '年龄': 'age',
|
||||||
|
'手机': 'phone', '婚姻状况': 'marital_status', '身高': 'height', '体重': 'weight',
|
||||||
|
'学历': 'education', '毕业学校': 'school', '专业': 'major', '工作经验': 'work_years',
|
||||||
|
'现居住地': 'current_location', '期望职位': 'expected_position', '期望月薪': 'expected_salary',
|
||||||
|
'工作地点': 'job_location', '到岗时间': 'available_time', '更新时间': 'update_time'
|
||||||
|
}
|
||||||
|
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 None
|
||||||
|
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:
|
||||||
|
return pd.to_datetime(val)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if 'update_time' in df.columns:
|
||||||
|
df['update_time'] = df['update_time'].apply(parse_update_time)
|
||||||
|
|
||||||
|
# 清洗身高/体重(复合字段提取)
|
||||||
|
def extract_height_weight(text):
|
||||||
|
text = str(text) if text and not pd.isna(text) else ''
|
||||||
|
h = re.search(r'(\d{2,3})\s*cm', text)
|
||||||
|
w = re.search(r'(\d{2,3})\s*kg', text)
|
||||||
|
return {
|
||||||
|
'height': int(h.group(1)) if h else None,
|
||||||
|
'weight': int(w.group(1)) if w else None
|
||||||
|
}
|
||||||
|
|
||||||
|
for idx, row in df.iterrows():
|
||||||
|
text = ' '.join([str(v) for k, v in row.items() if k not in ['height', 'weight']])
|
||||||
|
parsed = extract_height_weight(text)
|
||||||
|
for key in ['height', 'weight']:
|
||||||
|
val = row.get(key)
|
||||||
|
try:
|
||||||
|
if pd.isna(val) or str(val).strip().lower() in ['nan', 'none', 'null', '']:
|
||||||
|
df.at[idx, key] = parsed[key]
|
||||||
|
except:
|
||||||
|
df.at[idx, key] = parsed[key]
|
||||||
|
|
||||||
|
if 'age' in df.columns:
|
||||||
|
df['age'] = df['age'].apply(lambda x: int(re.search(r'\d+', str(x)).group()) if pd.notna(x) and re.search(r'\d+', str(x)) else None)
|
||||||
|
|
||||||
|
valid_fields = [f.name for f in ResumeBasic._meta.fields]
|
||||||
|
df = df[[col for col in df.columns if col in valid_fields]]
|
||||||
|
|
||||||
|
# 清除所有 NaN -> None
|
||||||
|
for col in df.columns:
|
||||||
|
df[col] = df[col].apply(lambda x: None if pd.isna(x) or str(x).strip().lower() in ['nan', 'none', 'null', ''] else x)
|
||||||
|
|
||||||
|
records = df.to_dict(orient='records')
|
||||||
|
existing_ids = set(ResumeBasic.objects.filter(
|
||||||
|
resume_id__in=[r["resume_id"] for r in records if "resume_id" in r]
|
||||||
|
).values_list("resume_id", flat=True))
|
||||||
|
|
||||||
|
new_records = [r for r in records if r.get("resume_id") not in existing_ids]
|
||||||
|
|
||||||
|
ResumeBasic.objects.bulk_create([ResumeBasic(**r) for r in new_records])
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f"✅ 成功导入 {len(new_records)} 条简历记录(关键词:{default_keyword},来源:{default_source})"
|
||||||
|
))
|
@ -7,52 +7,77 @@ from websites.models import Website
|
|||||||
|
|
||||||
|
|
||||||
class ResumeBasic(models.Model):
|
class ResumeBasic(models.Model):
|
||||||
resume_id = models.IntegerField(max_length=64, unique=True, db_index=True, help_text='resume_id')
|
resume_id = models.IntegerField(unique=True, db_index=True, verbose_name="简历ID", help_text="resume_id")
|
||||||
name = models.CharField(max_length=255, null=True, blank=True, help_text='姓名')
|
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, help_text='求职区域')
|
job_region = models.CharField(max_length=255, null=True, blank=True, verbose_name="求职区域",
|
||||||
birthday = models.CharField(max_length=255, null=True, blank=True, help_text='生日')
|
help_text="求职区域")
|
||||||
education = models.CharField(max_length=255, null=True, blank=True, help_text='学历')
|
birthday = models.CharField(max_length=255, null=True, blank=True, verbose_name="生日", help_text="生日")
|
||||||
school = models.CharField(max_length=255, null=True, blank=True, help_text='学校')
|
education = models.CharField(max_length=255, null=True, blank=True, verbose_name="学历", help_text="学历")
|
||||||
expected_position = models.CharField(max_length=255, null=True, blank=True, help_text='期望职务')
|
school = 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, help_text='最后活跃时间')
|
expected_position = models.CharField(max_length=255, null=True, blank=True, verbose_name="期望职务",
|
||||||
marital_status = models.CharField(max_length=255, null=True, blank=True, help_text='婚姻')
|
help_text="期望职务")
|
||||||
current_location = models.CharField(max_length=255, null=True, blank=True, help_text='现居地')
|
last_active_time = models.CharField(max_length=255, null=True, blank=True, verbose_name="最后活跃时间",
|
||||||
age = models.IntegerField(null=True, blank=True, help_text='年龄')
|
help_text="最后活跃时间")
|
||||||
phone = models.CharField(max_length=255, null=True, blank=True, help_text='电话')
|
marital_status = models.CharField(max_length=255, null=True, blank=True, verbose_name="婚姻", help_text="婚姻")
|
||||||
gender = models.CharField(max_length=255, null=True, blank=True, help_text='性别')
|
current_location = models.CharField(max_length=255, null=True, blank=True, verbose_name="现居地",
|
||||||
job_type = models.CharField(max_length=255, null=True, blank=True, help_text='求职类型')
|
help_text="现居地")
|
||||||
job_status = models.CharField(max_length=255, null=True, blank=True, 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, help_text='工作1经历')
|
work_1_experience = models.TextField(null=True, blank=True, verbose_name="工作1经历", help_text="工作1经历")
|
||||||
work_1_time = models.DateTimeField(null=True, blank=True, help_text='工作1时间')
|
work_1_time = models.DateTimeField(null=True, blank=True, verbose_name="工作1时间", help_text="工作1时间")
|
||||||
work_1_description = models.TextField(null=True, blank=True, 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, help_text='工作2经历')
|
work_2_experience = models.TextField(null=True, blank=True, verbose_name="工作2经历", help_text="工作2经历")
|
||||||
work_2_time = models.DateTimeField(null=True, blank=True, help_text='工作2时间')
|
work_2_time = models.DateTimeField(null=True, blank=True, verbose_name="工作2时间", help_text="工作2时间")
|
||||||
work_2_description = models.TextField(null=True, blank=True, 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, help_text='工作3经历')
|
work_3_experience = models.TextField(null=True, blank=True, verbose_name="工作3经历", help_text="工作3经历")
|
||||||
work_3_time = models.DateTimeField(null=True, blank=True, help_text='工作3时间')
|
work_3_time = models.DateTimeField(null=True, blank=True, verbose_name="工作3时间", help_text="工作3时间")
|
||||||
work_3_description = models.TextField(null=True, blank=True, 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, help_text='工作4经历')
|
work_4_experience = models.TextField(null=True, blank=True, verbose_name="工作4经历", help_text="工作4经历")
|
||||||
work_4_time = models.DateTimeField(null=True, blank=True, help_text='工作4时间')
|
work_4_time = models.DateTimeField(null=True, blank=True, verbose_name="工作4时间", help_text="工作4时间")
|
||||||
work_4_description = models.TextField(null=True, blank=True, 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, help_text='身高')
|
height = models.IntegerField(null=True, blank=True, verbose_name="身高", help_text="身高")
|
||||||
weight = models.IntegerField(null=True, blank=True, help_text='体重')
|
weight = models.IntegerField(null=True, blank=True, verbose_name="体重", help_text="体重")
|
||||||
work_years = models.IntegerField(null=True, blank=True, 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, help_text='最高学历')
|
highest_education = models.CharField(max_length=255, null=True, blank=True, verbose_name="最高学历",
|
||||||
ethnicity = models.CharField(max_length=255, null=True, blank=True, help_text='民族')
|
help_text="最高学历")
|
||||||
update_time = models.DateTimeField(null=True, blank=True, help_text='更新时间')
|
ethnicity = models.CharField(max_length=255, null=True, blank=True, verbose_name="民族", help_text="民族")
|
||||||
job_function = models.CharField(max_length=255, null=True, blank=True, help_text='工作职能')
|
update_time = models.DateTimeField(null=True, blank=True, verbose_name="更新时间", help_text="更新时间")
|
||||||
intended_position = models.CharField(max_length=255, null=True, blank=True, help_text='意向岗位')
|
job_function = models.CharField(max_length=255, null=True, blank=True, verbose_name="工作职能",
|
||||||
industry = models.CharField(max_length=255, null=True, blank=True, help_text='从事行业')
|
help_text="工作职能")
|
||||||
expected_salary = models.CharField(max_length=255, null=True, blank=True, help_text='期望薪资')
|
intended_position = models.CharField(max_length=255, null=True, blank=True, verbose_name="意向岗位",
|
||||||
available_time = models.DateTimeField(null=True, blank=True, help_text='到岗时间')
|
help_text="意向岗位")
|
||||||
job_property = models.CharField(max_length=255, null=True, blank=True, help_text='工作性质')
|
industry = models.CharField(max_length=255, null=True, blank=True, verbose_name="从事行业",
|
||||||
job_location = models.CharField(max_length=255, null=True, blank=True, help_text='工作地点')
|
help_text="从事行业")
|
||||||
source = models.ForeignKey(Website, null=True, blank=True, on_delete=models.SET_NULL, 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):
|
def __str__(self):
|
||||||
return f"{self.name} - {self.resume_id} - {self.name}"
|
return f"{self.name} - {self.resume_id} - {self.name}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "简历"
|
||||||
|
verbose_name_plural = "简历列表"
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from .models import *
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
|
|
||||||
|
@admin.register(Website)
|
||||||
|
class WebsiteAdmin(admin.ModelAdmin):
|
||||||
|
list_display = ('name', 'db_alias', 'db_alias')
|
@ -1,11 +1,12 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
# Create your models here.
|
# Create your models here.
|
||||||
class Website(models.Model):
|
class Website(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100, verbose_name="网站名称")
|
||||||
db_alias = models.CharField(max_length=50, unique=True)
|
db_alias = models.CharField(max_length=50, unique=True, verbose_name="数据库别名")
|
||||||
description = models.TextField(blank=True)
|
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