ninjia api 简历(基础信息)接口 和文档 导入 Excle 目前未完善

This commit is contained in:
晓丰 2025-04-15 10:17:47 +08:00
parent 1366de574b
commit 286cbe907b
13 changed files with 282 additions and 52 deletions

5
api.py Normal file
View 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)

View File

@ -14,11 +14,11 @@ import os
from pathlib import Path
env = environ.Env()
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
@ -122,9 +122,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# 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

View File

@ -16,7 +16,8 @@ Including another URLconf
"""
from django.contrib import admin
from django.urls import path
from api import api
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', api.urls),
]

View File

@ -1,3 +1,9 @@
from django.contrib import admin
from .models import *
from .models import *
# 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
View File

59
resumes/api/schemas.py Normal file
View 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
View 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)}

View File

View File

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

View File

@ -7,52 +7,77 @@ from websites.models import Website
class ResumeBasic(models.Model):
resume_id = models.IntegerField(max_length=64, unique=True, db_index=True, help_text='resume_id')
name = models.CharField(max_length=255, null=True, blank=True, help_text='姓名')
job_region = models.CharField(max_length=255, null=True, blank=True, help_text='求职区域')
birthday = models.CharField(max_length=255, null=True, blank=True, help_text='生日')
education = models.CharField(max_length=255, null=True, blank=True, help_text='学历')
school = models.CharField(max_length=255, null=True, blank=True, help_text='学校')
expected_position = models.CharField(max_length=255, null=True, blank=True, help_text='期望职务')
last_active_time = models.CharField(max_length=255, null=True, blank=True, help_text='最后活跃时间')
marital_status = models.CharField(max_length=255, null=True, blank=True, help_text='婚姻')
current_location = models.CharField(max_length=255, null=True, blank=True, help_text='现居地')
age = models.IntegerField(null=True, blank=True, help_text='年龄')
phone = models.CharField(max_length=255, null=True, blank=True, help_text='电话')
gender = models.CharField(max_length=255, null=True, blank=True, help_text='性别')
job_type = models.CharField(max_length=255, null=True, blank=True, help_text='求职类型')
job_status = models.CharField(max_length=255, null=True, blank=True, help_text='求职状态')
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, 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, help_text='工作1经历')
work_1_time = models.DateTimeField(null=True, blank=True, help_text='工作1时间')
work_1_description = 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, 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, help_text='工作2经历')
work_2_time = models.DateTimeField(null=True, blank=True, help_text='工作2时间')
work_2_description = 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, 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, help_text='工作3经历')
work_3_time = models.DateTimeField(null=True, blank=True, help_text='工作3时间')
work_3_description = 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, 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, help_text='工作4经历')
work_4_time = models.DateTimeField(null=True, blank=True, help_text='工作4时间')
work_4_description = 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, 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, help_text='身高')
weight = models.IntegerField(null=True, blank=True, help_text='体重')
work_years = models.IntegerField(null=True, blank=True, help_text='工作经验')
highest_education = models.CharField(max_length=255, null=True, blank=True, help_text='最高学历')
ethnicity = models.CharField(max_length=255, null=True, blank=True, help_text='民族')
update_time = models.DateTimeField(null=True, blank=True, help_text='更新时间')
job_function = models.CharField(max_length=255, null=True, blank=True, help_text='工作职能')
intended_position = models.CharField(max_length=255, null=True, blank=True, help_text='意向岗位')
industry = models.CharField(max_length=255, null=True, blank=True, help_text='从事行业')
expected_salary = models.CharField(max_length=255, null=True, blank=True, help_text='期望薪资')
available_time = models.DateTimeField(null=True, blank=True, help_text='到岗时间')
job_property = models.CharField(max_length=255, null=True, blank=True, help_text='工作性质')
job_location = models.CharField(max_length=255, null=True, blank=True, help_text='工作地点')
source = models.ForeignKey(Website, null=True, blank=True, on_delete=models.SET_NULL, help_text="数据来源网站")
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 = "简历列表"

View File

@ -1,3 +1,7 @@
from django.contrib import admin
from .models import *
# Register your models here.
@admin.register(Website)
class WebsiteAdmin(admin.ModelAdmin):
list_display = ('name', 'db_alias', 'db_alias')

View File

@ -1,11 +1,12 @@
from django.db import models
# Create your models here.
class Website(models.Model):
name = models.CharField(max_length=100)
db_alias = models.CharField(max_length=50, unique=True)
description = models.TextField(blank=True)
def __str__(self):
return self.name
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="描述")
class Meta:
verbose_name = "网站"
verbose_name_plural = "网站列表"