📢 阅读提示
感谢你阅读本章。本文所有代码和注释均来自真实项目,为了能让更多认真学习的读者持续获得优质内容,本文将在7天后转为“粉丝可见”。
如果你觉得本文有价值,欢迎收藏并关注我,本系列博客将继续保持同样详细的注释风格。
感谢理解与支持!
一、业务场景
上一章我们完成了员工信息模型(60+字段)。现在需要支持批量导入:企业一次可能录入数百名员工,手工逐条录入不现实。导入功能要满足:
- 提供标准 Excel 模板(带颜色标注、填写说明)。
- 数据验证:区分不合格(必填字段为空、日期错误、部门无权限)和警告(可选字段为空、手机号不规范等)。
- 不合格的行不能导入,警告行可导入。
- 根据员工编码判断是更新还是新建(存在则更新,不存在则新建)。
- 更新时,Excel 中为空的字段不覆盖数据库中已有的值(防止清空已有数据)。
- 每行数据独立事务,一行失败不影响其他行。
- 与导出格式完全兼容:导出的 Excel 文件稍作修改(例如调整某列内容),即可原样导回。
二、导入功能整体流程
用户点击“数据导入”菜单 ↓ 【第一步】下载模板(或直接上传Excel) ↓ 【第二步】上传 Excel → 验证数据(validators.py) ↓ 展示验证结果(合格/警告/不合格) 【第三步】确认导入(只有无不合格行时允许) ↓ 【第四步】执行导入(core.py,逐行事务) ↓ 【第五步】显示导入结果(成功/失败统计)Admin 入口通过 DataImport 代理模型注册,重写 changelist_view 实现自定义界面。
三、模板生成器(template.py)
模板生成器负责生成带格式的 Excel 模板,包含两个 Sheet:
· 数据导入:33列,第1行表头,第2行示例数据,第3行起由用户填写。
· 填写说明:说明必填字段、日期格式、导入规则等。
3.1 关键代码逐行注释
# employee_info/template.pyimportpandasaspdfromioimportBytesIOfromdjango.httpimportHttpResponsefromurllib.parseimportquoteimportopenpyxlfromopenpyxl.stylesimportFont,PatternFill,Alignment,Border,Sidefromopenpyxl.utilsimportget_column_letterdefdownload_enhanced_template()->HttpResponse:"""生成并下载增强版Excel模板(精简版)"""# ---------- 1. 定义模板数据(33列)----------template_data={# 核心必填字段(3个)- 红色标注'员工编码':['EMP001','※必填※ 员工唯一编码'],'员工姓名':['张三','※必填※ 员工姓名'],'单位ID':['1','※必填※ 部门ID(整数)'],# 个人基本信息(8个)'身份证号码':['110101199001011234','15位或18位身份证号码'],'民族':['汉族','可选:汉族/回族/蒙古族/...'],'性别':['男','可选:男/女/其他'],'出生日期':['1990-05-23','※格式需正确※ 支持:yyyy-MM-dd, yyyy/MM/dd, yyyyMMdd'],'籍贯':['北京','籍贯'],'婚姻状况':['未婚','可选:未婚/已婚/离异/丧偶/其他'],'联系电话':['13800138000','手机号或座机号'],'常住地址':['北京市朝阳区','常住地址'],# ... 其他字段(共33列,此处省略)}# 创建 DataFrame,两行数据(表头 + 示例)df=pd.DataFrame(template_data)# df 只有一行示例数据# ---------- 2. 写入 Excel 并设置样式 ----------output=BytesIO()withpd.ExcelWriter(output,engine='openpyxl')aswriter:# 写入数据到 Sheet1(数据导入)df.to_excel(writer,sheet_name='数据导入',index=False)workbook=writer.book worksheet=writer.sheets['数据导入']# 设置列宽(略)# 设置表头样式:深蓝色背景、白色加粗字体、居中header_fill=PatternFill(start_color='366092',end_color='366092',fill_type='solid')header_font=Font(color='FFFFFF',bold=True)forcellinworksheet[1]:cell.fill=header_fill cell.font=header_font cell.alignment=Alignment(horizontal='center',vertical='center')# 设置示例行(第2行)样式:浅绿色背景、斜体example_fill=PatternFill(start_color='E2EFDA',end_color='E2EFDA',fill_type='solid')forcellinworksheet[2]:cell.fill=example_fill cell.font=Font(italic=True)# 必填字段(A、B、C列)字体标红required_columns=['A','B','C']forcolinrequired_columns:worksheet[f'{col}1'].font=Font(color='FF0000',bold=True)worksheet[f'{col}2'].font=Font(color='FF0000',bold=True,italic=True)# 日期字段列标蓝date_columns=['H','L','M','S']# 出生日期、毕业时间、参工时间、加入组织时间forcolindate_columns:worksheet[f'{col}1'].font=Font(color='0070C0',bold=True)worksheet[f'{col}2'].font=Font(color='0070C0',italic=True)# 添加边框thin_border=Border(left=Side(style='thin'),right=Side(style='thin'),top=Side(style='thin'),bottom=Side(style='thin'))forrowinworksheet.iter_rows(min_row=1,max_row=worksheet.max_row):forcellinrow:cell.border=thin_border# ---------- 3. 创建第二个 Sheet(填写说明)----------field_sheet=workbook.create_sheet(title='填写说明')instructions=[['员工数据导入模板 - 填写说明'],[''],['一、必填字段(3个,必须填写)'],[' 1. 员工编码:员工唯一编码,不可重复'],[' 2. 员工姓名:员工姓名'],[' 3. 单位ID:部门ID,必须是有效的整数'],[''],['二、日期字段(如果填写,格式必须正确)'],[' 支持格式:yyyy-MM-dd、yyyy/MM/dd、yyyyMMdd、yyyy年MM月dd日'],[''],['三、导入规则'],[' 1. 员工编码存在 → 更新数据(空值不覆盖现有值)'],[' 2. 员工编码不存在 → 新建员工'],[' 3. 有不合格数据 → 必须修正后重新上传'],[' 4. 只有警告数据 → 可以继续导入'],[''],['四、与导出的闭环',' 导出的Excel文件稍作修改后,可直接导入系统,实现数据批量更新。']]# 写入说明内容并设置样式...# ---------- 4. 返回 HTTP 响应(下载)----------output.seek(0)response=HttpResponse(output.getvalue(),content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')filename="企业员工基础信息导入模板.xlsx"response['Content-Disposition']=f"attachment; filename*=UTF-8''{quote(filename)}"returnresponse亮点:必填字段红色标注、日期字段蓝色标注、示例行浅绿背景、自带填写说明 Sheet。用户拿到模板即可直接填写。
四、数据验证器(validators.py)
验证器负责:读取上传的 Excel,逐行验证,区分不合格(error)、警告(warning)、合格(success)。
4.1 验证结果类 ValidationResult
classValidationResult:def__init__(self,row_num,employee_number=''):self.row_num=row_num# Excel 行号(从2开始)self.employee_number=employee_number self.full_name=''# 员工姓名(用于显示)self.department_id=''# 单位IDself.status='pending'# success / warning / errorself.critical_errors=[]# 不合格错误(必须修正)self.warnings=[]# 警告(可忽略)4.2 核心验证逻辑 validate_dataframe
defvalidate_dataframe(self,df):""" 验证 DataFrame,返回 (can_import, validation_results, clean_df) """# 1. 检查必填列是否存在forcolin['员工编码','员工姓名','单位ID']:ifcolnotindf.columns:returnFalse,[错误结果],pd.DataFrame()# 2. 清理数据:空值处理、日期规范化df_clean=self._clean_dataframe(df)# 3. 逐行验证foridx,rowindf_clean.iterrows():result=self._validate_single_row(idx+2,row)# +2 转为 Excel 行号# 验证部门权限(使用第8章的权限函数)ifresult.department_id:dept_id=int(result.department_id)ifself.viewable_dept_idsanddept_idnotinself.viewable_dept_ids:result.critical_errors.append(f"单位ID{dept_id}不在您的部门权限范围内")# 根据 critical_errors 和 warnings 决定 statusifresult.critical_errors:result.status='error'elifresult.warnings:result.status='warning'else:result.status='success'self.validation_results.append(result)can_import=all(r.status!='error'forrinself.validation_results)returncan_import,self.validation_results,df_clean4.3 日期规范化(支持多种格式)
def_normalize_date_string(self,date_value):"""将各种日期格式统一转为 yyyy-MM-dd"""ifpd.isna(date_value):return''date_str=str(date_value).strip()forfmtin['%Y-%m-%d','%Y/%m/%d','%Y%m%d','%Y年%m月%d日']:try:returndatetime.strptime(date_str,fmt).strftime('%Y-%m-%d')except:continuereturndate_str# 无法解析则原样返回(后续会报错)4.4 部门权限校验:只能导入自己部门及下级的数据(安全亮点)
导入时,系统会严格检查 Excel 中填写的“单位ID”是否属于当前用户有权管理的部门范围。用户只能导入自己所在部门及其所有下级部门的员工数据,不能导入同级部门、上级部门或其他无关部门的员工。
实现原理:
· 验证器初始化时,通过 get_viewable_departments(request.user) 获取当前用户有权查看的部门ID列表(该函数返回用户自己部门及所有子孙部门的ID)。
· 对每一行数据,在 _validate_single_row 中检查 department_id 是否在此列表中。如果不在,则记录为“不合格”错误(critical_errors),该行数据将无法导入。
安全价值:
· 防止用户越权导入其他部门的敏感数据,确保数据归属正确。
· 符合企业数据隔离要求:每个管理员只能管理自己管辖范围内的员工。
代码位置:见 validate_dataframe 方法中的部门权限校验段落。
五、导入器(core.py)
5.1 整体结构
classEnhancedImporter:def__init__(self,request):self.request=request self.user=request.user self.results={'total':0,'created':0,'updated':0,'skipped':0,'errors':[]}defimport_data(self,validated_data):# 遍历每行,每行独立事务forrow_datainvalidated_data:withtransaction.atomic():self._import_single_row(row_data)returnself.results5.2 单行导入:更新或新建
def_import_single_row(self,row_data):employee_number=row_data.get('employee_number')department_id=row_data.get('department_id')# 部门验证department=Department.objects.get(id=department_id,is_active=True)# 根据员工编码查找try:employee=EmployeeBasicInfo.objects.get(employee_number=employee_number)# 更新模式:空值不覆盖returnself._update_employee(employee,row_data,department)exceptEmployeeBasicInfo.DoesNotExist:returnself._create_employee(row_data,department)5.3 更新逻辑(空值不覆盖)
def_update_employee(self,employee,row_data,department):"""更新员工信息:只有上传的字段非空时才更新"""has_changes=Falseupdate_fields=[]# 字段映射字典(略)forexcel_field,(model_field,converter)infield_mapping.items():new_value=row_data.get(excel_field)ifnew_valuein(None,'','NaN'):# 空值跳过continueold_value=getattr(employee,model_field)converted=converter(new_value)ifold_value!=converted:setattr(employee,model_field,converted)update_fields.append(model_field)has_changes=True# 更新部门(如果部门变化)ifdepartmentandemployee.department_id!=department.id:employee.department=department employee.department_name=department.name update_fields.extend(['department','department_name'])has_changes=Trueifhas_changes:employee.updated_by=self.user employee.save(update_fields=update_fields)return'updated'else:return'skipped'关键:if new_value in (None, ‘’, ‘NaN’): continue 确保空值不会覆盖现有数据。这避免了用户误清空字段。
六、Admin 集成(DataImportAdmin)
通过代理模型 DataImport 注册,重写 changelist_view 实现自定义界面。
@admin.register(DataImport)classDataImportAdmin(admin.ModelAdmin):defchangelist_view(self,request,extra_context=None):# 权限检查ifnotrequest.user.has_perm('employee_info.add_employeebasicinfo'):returnHttpResponseForbidden("没有导入权限")context={**self.admin_site.each_context(request),'title':'员工数据导入','opts':self.model._meta,}# 从 session 中取出验证结果(如果有)if'enhanced_validation_results'inrequest.session:context.update(validation_data)returnrender(request,'admin/employee_info/enhanced_import.html',context)defget_urls(self):urls=super().get_urls()custom_urls=[path('download-template/',self.download_template_view,name='download_enhanced_template'),path('validate/',self.validate_view,name='validate_enhanced_data'),path('confirm/',self.confirm_view,name='enhanced_import_confirm'),path('execute/',self.execute_view,name='execute_enhanced_import'),]returncustom_urls+urls七、与导出的闭环-设计特色
在导出模块(第16章)中,generator.py 的 FIELD_DEFINITIONS 与 template.py 的列顺序完全一致。这意味着:
- 用户导出员工数据(33列 Excel)。
- 用户在该 Excel 中修改部分字段(比如调整岗位层级、联系方式)。
- 用户将修改后的 Excel 直接上传到导入功能。
- 系统根据员工编码自动更新对应记录,且空值不覆盖(如果某个单元格被无意清空,不会删除数据库中的现有值)。
闭环规则总结:
· 必须保留的列:前3列(员工编码、员工姓名、单位ID)。如果删除这3列中的任何一列,验证器会报错“缺少必要列”。
· 其他30列均可删除(例如导出时不需要“头像”列,删掉不影响导入),因为验证器只检查必填列和字段映射,缺失的列会被视为空值,而空值不会覆盖现有数据。
· 列名必须与模板完全一致(大小写、空格敏感)。
八、注意事项(踩坑经验)
| 序号 | 注意事项 | 踩坑总结 |
|---|---|---|
| 1 | 空值不覆盖 | _update_employee 中检查 new_value in (None, ‘’, ‘NaN’) 跳过更新,这是防止误清空的关键 |
| 2 | 部门权限 | 导入前验证 viewable_dept_ids,避免用户导入无权部门的员工 |
| 3 | 日期解析 | 支持多种格式,但建议统一 yyyy-MM-dd,避免歧义 |
| 4 | 事务处理 | 每行独立事务,一行错误不会导致整个文件回滚 |
| 5 | 大文件性能 | 目前逐行保存,上千行数据可能较慢,可考虑批量更新优化,但鉴于企业员工数量通常不超过数千,可接受 |
| 6 | 必填列检查 | 验证器会检查列是否存在,若不存在直接返回错误,不给导入机会 |
| 7 | 特殊标记字段 | 布尔字段支持“是/否、1/0、true/false”等多种表示,转换函数 _convert_boolean 已做兼容 |
九、小结
本章完成了员工数据导入的全流程:
· 模板生成器(带样式、填写说明)
· 三步流程(上传→验证→确认)
· 验证器(区分不合格/警告/合格,含部门权限)
· 导入器(逐行事务,更新时空值不覆盖)
· Admin 入口集成
· 导出→导入完美闭环(列对齐、空值安全、唯一标识为员工编码)
下一章:员工数据导出(13个筛选条件、33列 Excel 生成、预览流程)。
互动问题:在批量导入中,你是否遇到过“意外清空已有字段”的问题?我们采用的“空值不覆盖”策略是否解决了你的痛点?欢迎评论区交流。