From 286cbe907b89ba8d2339c48a2767b4cd277b2b25 Mon Sep 17 00:00:00 2001 From: Franklin-F Date: Tue, 15 Apr 2025 10:17:47 +0800 Subject: [PATCH] =?UTF-8?q?ninjia=20api=20=E7=AE=80=E5=8E=86(=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E4=BF=A1=E6=81=AF)=E6=8E=A5=E5=8F=A3=20=E5=92=8C?= =?UTF-8?q?=E6=96=87=E6=A1=A3=20=E5=AF=BC=E5=85=A5=20Excle=20=E7=9B=AE?= =?UTF-8?q?=E5=89=8D=E6=9C=AA=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api.py | 5 + core/settings.py | 6 +- core/urls.py | 3 +- resumes/admin.py | 6 + resumes/api/__init__.py | 0 resumes/api/schemas.py | 59 ++++++++++ resumes/api/views.py | 29 +++++ resumes/management/__init__.py | 0 resumes/management/commands/__init__.py | 0 .../commands/import_accounting_resumes.py | 100 ++++++++++++++++ resumes/models.py | 107 +++++++++++------- websites/admin.py | 6 +- websites/models.py | 13 ++- 13 files changed, 282 insertions(+), 52 deletions(-) create mode 100644 api.py create mode 100644 resumes/api/__init__.py create mode 100644 resumes/api/schemas.py create mode 100644 resumes/api/views.py create mode 100644 resumes/management/__init__.py create mode 100644 resumes/management/commands/__init__.py create mode 100644 resumes/management/commands/import_accounting_resumes.py diff --git a/api.py b/api.py new file mode 100644 index 0000000..57b5a1b --- /dev/null +++ b/api.py @@ -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) diff --git a/core/settings.py b/core/settings.py index d66f251..60cd9fd 100644 --- a/core/settings.py +++ b/core/settings.py @@ -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 diff --git a/core/urls.py b/core/urls.py index 1471c0a..dfe6276 100644 --- a/core/urls.py +++ b/core/urls.py @@ -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), ] diff --git a/resumes/admin.py b/resumes/admin.py index 8c38f3f..c1d4f26 100644 --- a/resumes/admin.py +++ b/resumes/admin.py @@ -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'] \ No newline at end of file diff --git a/resumes/api/__init__.py b/resumes/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resumes/api/schemas.py b/resumes/api/schemas.py new file mode 100644 index 0000000..72c01a2 --- /dev/null +++ b/resumes/api/schemas.py @@ -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] diff --git a/resumes/api/views.py b/resumes/api/views.py new file mode 100644 index 0000000..828fcb8 --- /dev/null +++ b/resumes/api/views.py @@ -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)} diff --git a/resumes/management/__init__.py b/resumes/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resumes/management/commands/__init__.py b/resumes/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/resumes/management/commands/import_accounting_resumes.py b/resumes/management/commands/import_accounting_resumes.py new file mode 100644 index 0000000..5f810dd --- /dev/null +++ b/resumes/management/commands/import_accounting_resumes.py @@ -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})" + )) diff --git a/resumes/models.py b/resumes/models.py index 40e94af..ee7bbe3 100644 --- a/resumes/models.py +++ b/resumes/models.py @@ -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 = "简历列表" diff --git a/websites/admin.py b/websites/admin.py index 8c38f3f..988c5f3 100644 --- a/websites/admin.py +++ b/websites/admin.py @@ -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') \ No newline at end of file diff --git a/websites/models.py b/websites/models.py index c638dae..e738646 100644 --- a/websites/models.py +++ b/websites/models.py @@ -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 = "网站列表"