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
|
||||
|
||||
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
|
||||
|
||||
|
@ -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),
|
||||
]
|
||||
|
@ -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
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):
|
||||
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 = "简历列表"
|
||||
|
@ -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')
|
@ -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 = "网站列表"
|
||||
|
Loading…
x
Reference in New Issue
Block a user