项目初次提交

This commit is contained in:
2025-04-11 08:54:28 +08:00
commit 9e14a3256f
220 changed files with 15673 additions and 0 deletions

7
utils/excel/__init__.py Normal file
View File

@ -0,0 +1,7 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/11/14 9:56
# @File : __init__.py.py
# @IDE : PyCharm
# @desc : 简要说明

245
utils/excel/excel_manage.py Normal file
View File

@ -0,0 +1,245 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/5/6 17:25
# @File : excel_manage.py
# @IDE : PyCharm
# @desc : EXCEL 文件操作
import datetime
import os
import re
from pathlib import Path
from openpyxl.utils import get_column_letter
from openpyxl import load_workbook, Workbook
from application.settings import STATIC_ROOT, STATIC_URL
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
from utils.file.file_base import FileBase
from .excel_schema import AlignmentModel, FontModel, PatternFillModel
class ExcelManage:
"""
excel 文件序列化
"""
# 列名A-Z
EXCEL_COLUMNS = [chr(a) for a in range(ord('A'), ord('Z') + 1)]
def __init__(self):
self.sheet = None
self.wb = None
def open_workbook(self, file: str, read_only: bool = False, data_only: bool = False) -> None:
"""
初始化 excel 文件
:param file: 文件名称或者对象
:param read_only: 是否只读,优化读取速度
:param data_only: 是否加载文件对象
:return:
"""
# 加载excel文件获取表单
self.wb = load_workbook(file, read_only=read_only, data_only=data_only)
def open_sheet(
self,
sheet_name: str = None,
file: str = None,
read_only: bool = False,
data_only: bool = False
) -> None:
"""
初始化 excel 文件
:param sheet_name: 表单名称,为空则默认第一个
:param file:
:param read_only:
:param data_only:
:return:
"""
# 加载excel文件获取表单
if not self.wb:
self.open_workbook(file, read_only, data_only)
if sheet_name:
if sheet_name in self.get_sheets():
self.sheet = self.wb[sheet_name]
else:
self.sheet = self.wb.create_sheet(sheet_name)
else:
self.sheet = self.wb.active
def get_sheets(self) -> list:
"""
读取所有工作区名称
:return: 一维数组
"""
return self.wb.sheetnames
def create_excel(self, sheet_name: str = None) -> None:
"""
创建 excel 文件
:param sheet_name: 表单名称,为空则默认第一个
:return:
"""
# 加载excel文件获取表单
self.wb = Workbook()
self.sheet = self.wb.active
if sheet_name:
self.sheet.title = sheet_name
def readlines(self, min_row: int = 1, min_col: int = 1, max_row: int = None, max_col: int = None) -> list:
"""
读取指定表单所有数据
:param min_row: 最小行
:param min_col: 最小列
:param max_row: 最大行
:param max_col: 最大列
:return: 二维数组
"""
rows = self.sheet.iter_rows(min_row=min_row, min_col=min_col, max_row=max_row, max_col=max_col)
result = []
for row in rows:
_row = []
for cell in row:
_row.append(cell.value)
if any(_row):
result.append(_row)
return result
def get_header(self, row: int = 1, col: int = None, asterisk: bool = False) -> list:
"""
读取指定表单的表头(第一行数据)
:param row: 指定行
:param col: 最大列
:param asterisk: 是否去除 * 号
:return: 一维数组
"""
rows = self.sheet.iter_rows(min_row=row, max_col=col)
result = []
for row in rows:
for cell in row:
value = cell.value.replace("*", "").strip() if asterisk else cell.value.strip()
result.append(value)
break
return result
def write_list(self, rows: list, header: list = None) -> None:
"""
写入 excel文件
:param rows: 行数据集
:param header: 表头
:return:
"""
if header:
self.sheet.append(header)
pattern_fill_style = PatternFillModel(start_color='D9D9D9', end_color='D9D9D9', fill_type='solid')
font_style = FontModel(bold=True)
self.__set_row_style(1, len(header), pattern_fill_style=pattern_fill_style, font_style=font_style)
for index, data in enumerate(rows):
format_columns = {
"date_columns": []
}
for i in range(0, len(data)):
if isinstance(data[i], datetime.datetime):
data[i] = data[i].strftime("%Y-%m-%d %H:%M:%S")
format_columns["date_columns"].append(i + 1)
elif isinstance(data[i], bool):
data[i] = 1 if data[i] else 0
self.sheet.append(data)
self.__set_row_style(index + 2 if header else index + 1, len(data))
self.__set_row_format(index + 2 if header else index + 1, format_columns)
self.__auto_width()
self.__set_row_border()
def save_excel(self, path: str = "excel_manage"):
"""
保存 excel 文件到本地 static 目录
:param path: static 指定目录类别
:return:
"""
file_path = FileBase.generate_static_file_path(path=path, suffix="xlsx")
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
self.wb.save(file_path)
return {
"local_path": file_path,
"remote_path": file_path.replace(STATIC_ROOT, STATIC_URL)
}
def __set_row_style(
self,
row: int,
max_column: int,
alignment_style: AlignmentModel = AlignmentModel(),
font_style: FontModel = FontModel(),
pattern_fill_style: PatternFillModel = PatternFillModel()
):
"""
设置行样式
:param row: 行
:param max_column: 最大列
:param alignment_style: 单元格内容的对齐设置
:param font_style: 单元格内容的字体样式设置
:param pattern_fill_style: 单元格的填充模式设置
:return:
"""
for index in range(0, max_column):
alignment = Alignment(**alignment_style.model_dump())
font = Font(**font_style.model_dump())
pattern_fill = PatternFill(**pattern_fill_style.model_dump())
self.sheet.cell(row=row, column=index + 1).alignment = alignment
self.sheet.cell(row=row, column=index + 1).font = font
self.sheet.cell(row=row, column=index + 1).fill = pattern_fill
def __set_row_format(self, row: int, columns: dict):
"""
格式化行数据类型
:param row: 行
:param columns: 列数据
:return:
"""
for index in columns.get("date_columns", []):
self.sheet.cell(row=row, column=index).number_format = "yyyy-mm-dd h:mm:ss"
def __set_row_border(self):
"""
设置行边框
:return:
"""
# 创建 Border 对象并设置边框样式
border = Border(
left=Side(border_style="thin", color="000000"),
right=Side(border_style="thin", color="000000"),
top=Side(border_style="thin", color="000000"),
bottom=Side(border_style="thin", color="000000")
)
# 设置整个表格的边框
for row in self.sheet.iter_rows():
for cell in row:
cell.border = border
def __auto_width(self):
"""
设置自适应列宽
:return:
"""
# 设置一个字典用于保存列宽数据
dims = {}
# 遍历表格数据,获取自适应列宽数据
for row in self.sheet.rows:
for cell in row:
if cell.value:
# 遍历整个表格,把该列所有的单元格文本进行长度对比,找出最长的单元格
# 在对比单元格文本时需要将中文字符识别为1.7个长度英文字符识别为1个这里只需要将文本长度直接加上中文字符数量即可
# re.findall('([\u4e00-\u9fa5])', cell.value)能够识别大部分中文字符
cell_len = 0.7 * len(re.findall('([\u4e00-\u9fa5])', str(cell.value))) + len(str(cell.value))
dims[cell.column] = max((dims.get(cell.column, 0), cell_len))
for col, value in dims.items():
# 设置列宽get_column_letter用于获取数字列号对应的字母列号最后值+2是用来调整最终效果的
self.sheet.column_dimensions[get_column_letter(col)].width = value + 10
def close(self):
"""
关闭文件
:return:
"""
self.wb.close()

View File

@ -0,0 +1,65 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2023/08/24 22:19
# @File : excel_schema.py
# @IDE : PyCharm
# @desc :
from pydantic import BaseModel, Field
class AlignmentModel(BaseModel):
horizontal: str = Field('center', description="水平对齐方式。可选值:'left''center''right''justify''distributed'")
vertical: str = Field('center', description="垂直对齐方式。可选值:'top''center''bottom''justify''distributed'")
textRotation: int = Field(0, description="文本旋转角度(以度为单位)。默认为 0。")
wrapText: bool = Field(None, description="自动换行文本。设置为 True 时,如果文本内容超出单元格宽度,会自动换行显示。")
shrinkToFit: bool = Field(
None,
description="缩小字体以适应单元格。设置为 True 时,如果文本内容超出单元格宽度,会自动缩小字体大小以适应。"
)
indent: int = Field(0, description="文本缩进级别。默认为 0。")
relativeIndent: int = Field(0, description="相对缩进级别。默认为 0。")
justifyLastLine: bool = Field(
None,
description="对齐换行文本的末尾行。设置为 True 时,如果设置了文本换行,末尾的行也会被对齐。"
)
readingOrder: int = Field(0, description="阅读顺序。默认为 0。")
class Config:
title = "对齐设置模型"
description = "用于设置单元格内容的对齐样式。"
class FontModel(BaseModel):
name: str = Field(None, description="字体名称")
size: float = Field(None, description="字体大小(磅为单位)")
bold: bool = Field(None, description="是否加粗")
italic: bool = Field(None, description="是否斜体")
underline: str = Field(None, description="下划线样式。可选值:'single''double''singleAccounting''doubleAccounting'")
strikethrough: bool = Field(None, description="是否有删除线")
outline: bool = Field(None, description="是否轮廓字体")
shadow: bool = Field(None, description="是否阴影字体")
condense: bool = Field(None, description="是否压缩字体")
extend: bool = Field(None, description="是否扩展字体")
vertAlign: str = Field(None, description="垂直对齐方式。可选值:'superscript''subscript''baseline'")
color: dict = Field(None, description="字体颜色")
scheme: str = Field(None, description="字体方案。可选值:'major''minor'")
charset: int = Field(None, description="字符集编号")
family: int = Field(None, description="字体族编号")
class Config:
title = "字体设置模型"
description = "用于设置单元格内容的字体样式"
class PatternFillModel(BaseModel):
start_color: str = Field("FFFFFF", description="起始颜色RGB 值或颜色名称)")
end_color: str = Field("FFFFFF", description="结束颜色RGB 值或颜色名称)")
fill_type: str = Field("solid", description="填充类型('none''solid''darkDown' 等)")
class Config:
title = "填充模式设置模型"
description = "单元格的填充模式设置"

View File

@ -0,0 +1,165 @@
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2021/12/5 8:45
# @File : import_manage.py
# @IDE : PyCharm
# @desc : 数据导入管理
from typing import List
from fastapi import UploadFile
from core.exception import CustomException
from utils import status
from .excel_manage import ExcelManage
from utils.file.file_manage import FileManage
from .write_xlsx import WriteXlsx
from ..tools import list_dict_find
from enum import Enum
class FieldType(Enum):
list = "list"
str = "str"
class ImportManage(ExcelManage):
"""
数据导入管理
只支持 XLSX 类型文件application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
1. 判断文件类型
2. 保存文件为临时文件
3. 获取文件中的数据
4. 逐行检查数据,通过则创建数据
5. 不通过则添加到错误列表
6. 统计数量并返回
"""
file_type = ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"]
def __init__(self, file: UploadFile, headers: List[dict]):
super().__init__()
self.__table_data = None
self.__table_header = None
self.errors = []
self.success = []
self.success_number = 0
self.error_number = 0
self.check_file_type(file)
self.file = file
self.headers = headers
@classmethod
def check_file_type(cls, file: UploadFile) -> None:
"""
验证文件类型
:param file: 上传文件
:return:
"""
if file.content_type not in cls.file_type:
raise CustomException(msg="文件类型必须为xlsx类型", code=status.HTTP_ERROR)
async def get_table_data(
self,
file_path: str = None,
sheet_name: str = None,
header_row: int = 1,
data_row: int = 2
) -> None:
"""
获取表格数据与表头
:param file_path:
:param sheet_name:
:param header_row: 表头在第几行
:param data_row: 数据开始行
:return:
"""
if file_path:
__filename = file_path
else:
__filename = await FileManage.async_save_temp_file(self.file)
self.open_sheet(sheet_name=sheet_name, file=__filename, read_only=True)
self.__table_header = self.get_header(header_row, len(self.headers), asterisk=True)
self.__table_data = self.readlines(min_row=data_row, max_col=len(self.headers))
self.close()
def check_table_data(self) -> None:
"""
检查表格数据
:return:
"""
for row in self.__table_data:
result = self.__check_row(row)
if not result[0]:
row.append(result[1])
self.errors.append(row)
self.error_number += 1
else:
self.success_number += 1
self.success.append(result[1])
def __check_row(self, row: list) -> tuple:
"""
检查行数据
检查条件:
1. 检查是否为必填项
2. 检查是否为选项列表
3. 检查是否符合规则
:param row: 数据行
:return:
"""
data = {}
for index, cell in enumerate(row):
value = cell
field = self.headers[index]
label = self.__table_header[index]
if not cell and field.get("required", False):
return False, f"{label}不能为空!"
elif field.get("options", []) and cell:
item = list_dict_find(field.get("options", []), "label", cell)
if item:
value = item.get("value")
else:
return False, f"请选择正确的{label}"
elif field.get("rules", []) and cell:
rules = field.get("rules")
for validator in rules:
try:
validator(str(cell))
except ValueError as e:
return False, f"{label}{e.__str__()}"
if value:
field_type = field.get("type", FieldType.str)
if field_type == FieldType.list:
data[field.get("field")] = [value]
elif field_type == FieldType.str:
data[field.get("field")] = str(value)
else:
data[field.get("field")] = value
data["old_data_list"] = row
return True, data
def generate_error_url(self) -> str:
"""
成功错误数据的文件链接
:return:
"""
if self.error_number <= 0:
return ""
em = WriteXlsx()
em.create_excel(sheet_name="用户导入失败数据", save_static=True)
em.generate_template(self.headers, max_row=self.error_number)
em.write_list(self.errors)
em.close()
return em.get_file_url()
def add_error_data(self, row: dict) -> None:
"""
增加错误数据
:param row: 错误的数据行
:return:
"""
self.errors.append(row)
self.error_number += 1
self.success_number -= 1

129
utils/excel/write_xlsx.py Normal file
View File

@ -0,0 +1,129 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# @version : 1.0
# @Create Time : 2022/11/11 12:01
# @File : write_xlsx.py
# @IDE : PyCharm
# @desc : 简要说明
"""
XlsxWriterhttps://github.com/jmcnamara/XlsxWriter
博客教程https://blog.csdn.net/lemonbit/article/details/113855768
"""
import os.path
import xlsxwriter
from typing import List
from application.settings import STATIC_ROOT, STATIC_URL
from utils.file.file_base import FileBase
from pathlib import Path
class WriteXlsx:
"""
写入xlsx文件
"""
def __init__(self):
self.file_path = None
self.sheet_name = None
self.wb = None
self.sheet = None
def create_excel(self, file_path: str = None, sheet_name: str = "sheet1", save_static: bool = False) -> None:
"""
创建 excel 文件
:param file_path: 文件绝对路径或相对路径
:param sheet_name: sheet 名称
:param save_static: 保存方式 static 静态资源或者临时文件
:return:
"""
if not file_path:
if save_static:
self.file_path = FileBase.generate_static_file_path(path="write_xlsx", suffix="xlsx")
else:
self.file_path = FileBase.generate_temp_file_path(suffix="xlsx")
elif not os.path.isabs(file_path):
if save_static:
self.file_path = FileBase.generate_static_file_path(path="write_xlsx", filename=file_path)
else:
self.file_path = FileBase.generate_temp_file_path(filename=file_path)
else:
self.file_path = file_path
Path(self.file_path).parent.mkdir(parents=True, exist_ok=True)
self.sheet_name = sheet_name
self.wb = xlsxwriter.Workbook(self.file_path)
self.sheet = self.wb.add_worksheet(sheet_name)
def generate_template(self, headers: List[dict] = None, max_row: int = 101) -> None:
"""
生成模板
:param headers: 表头
:param max_row: 设置下拉列表至最大行
:return: 文件链接地址
"""
max_row = max_row + 100
for index, field in enumerate(headers):
font_format = {
'bold': False, # 字体加粗
'align': 'center', # 水平位置设置:居中
'valign': 'vcenter', # 垂直位置设置,居中
'font_size': 11, # '字体大小设置'
}
if field.get("required", False):
# 设置单元格必填样式
field["label"] = "* " + field["label"]
font_format["font_color"] = "red"
if field.get("options", False):
# 添加数据验证,下拉列表
validate = {'validate': 'list', 'source': [item.get("label") for item in field.get("options", [])]}
self.sheet.data_validation(1, index, max_row, index, validate)
cell_format = self.wb.add_format(font_format)
self.sheet.write(0, index, field.get("label"), cell_format)
# 设置列宽
self.sheet.set_column(0, len(headers) - 1, 22)
# 设置行高
self.sheet.set_row(0, 25)
def write_list(self, rows: list, start_row: int = 1) -> None:
"""
写入 excel文件
:param rows: 行数据集
:param start_row: 开始行
"""
font_format = {
'bold': False, # 字体加粗
'align': 'center', # 水平位置设置:居中
'valign': 'vcenter', # 垂直位置设置,居中
'font_size': 11, # '字体大小设置'
}
cell_format = self.wb.add_format(font_format)
for index, row in enumerate(rows):
row_number = index+start_row
self.sheet.write_row(row_number, 0, row, cell_format)
# 设置列宽
self.sheet.set_column(0, len(rows[0]) - 1, 22)
# 设置行高
self.sheet.set_default_row(25)
def close(self) -> None:
"""
关闭文件
"""
self.wb.close()
def get_file_url(self) -> str:
"""
获取访问文件的 url
:return:
"""
if not self.file_path:
raise ValueError("还未创建文件,请先创建 excel 文件!")
assert isinstance(self.file_path, str)
if self.file_path.startswith(STATIC_ROOT):
return self.file_path.replace(STATIC_ROOT, STATIC_URL)
else:
print("write_xlsx 生成文件:", self.file_path)
raise ValueError("生成文件为临时文件,无法访问!")