diff --git a/vue-fastapi-backend/module_admin/controller/datastd_controller.py b/vue-fastapi-backend/module_admin/controller/datastd_controller.py index 1faf238..53b0e27 100644 --- a/vue-fastapi-backend/module_admin/controller/datastd_controller.py +++ b/vue-fastapi-backend/module_admin/controller/datastd_controller.py @@ -1,5 +1,5 @@ from datetime import datetime -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, Depends, Request,UploadFile,File from sqlalchemy.ext.asyncio import AsyncSession from config.enums import BusinessType from config.get_db import get_db @@ -19,6 +19,7 @@ from module_admin.entity.vo.data_ast_content_vo import DataCatalogPageQueryModel DataCatalogResponseWithChildren, DataAssetCatalogTreeResponse, DataCatalogMovedRequest, DataCatalogMergeRequest, \ DataCatalogChild, DataCatalogMoverelRequest from pydantic_validation_decorator import ValidateFields +from utils.common_util import bytes2file_response datastdController = APIRouter(prefix='/datastd', dependencies=[Depends(LoginService.get_current_user)]) @@ -803,3 +804,31 @@ async def get_code_map_list2( code_page_query_result = await DataStdService.get_code_map_list2(query_db, id) logger.info('获取列配置列表成功') return ResponseUtil.success(data=code_page_query_result) +# -----------------------------------------------------导入---------------------------------------------------------------------- +@datastdController.post('/stdMain/importTemplate', dependencies=[Depends(CheckUserInterfaceAuth('system:user:import'))]) +async def export_std_main_template(request: Request, query_db: AsyncSession = Depends(get_db)): + main_import_template_result = await DataStdService.get_main_import_template_services() + logger.info('获取成功') + + return ResponseUtil.streaming(data=bytes2file_response(main_import_template_result)) +@datastdController.post('/stdMain/importData', dependencies=[Depends(CheckUserInterfaceAuth('system:user:import'))]) +@Log(title='用户管理', business_type=BusinessType.IMPORT) +async def batch_import_system_user( + request: Request, + file: UploadFile = File(...), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), + +): + batch_import_result = await DataStdService.batch_import_std_services( + request, query_db, file, current_user + ) + logger.info(batch_import_result.message) + + return ResponseUtil.success(msg=batch_import_result.message) +@datastdController.post('/stdDict/importTemplate', dependencies=[Depends(CheckUserInterfaceAuth('system:user:import'))]) +async def export_std_dict_template(request: Request, query_db: AsyncSession = Depends(get_db)): + dict_import_template_result = await DataStdService.get_dict_import_template_services() + logger.info('获取成功') + + return ResponseUtil.streaming(data=bytes2file_response(dict_import_template_result)) \ No newline at end of file diff --git a/vue-fastapi-backend/module_admin/dao/datastd_dao.py b/vue-fastapi-backend/module_admin/dao/datastd_dao.py index 26e6b45..3ce28d4 100644 --- a/vue-fastapi-backend/module_admin/dao/datastd_dao.py +++ b/vue-fastapi-backend/module_admin/dao/datastd_dao.py @@ -143,7 +143,18 @@ class DataStdDao: .order_by(DataStdCodeAppr.upd_time.desc()) .limit(1) ) - return result.scalar_one_or_none() + return result.scalar_one_or_none() + @classmethod + async def get_std_code_list_all(cls, query_db: AsyncSession): + """ + 获取所有标准代码数据列表(仅有效的) + :param session: 异步数据库会话 + :return: List[DataStdCode] + """ + stmt = select(DataStdCode).where(DataStdCode.class_id == 'code') # 只查有效的 + result = await query_db.execute(stmt) + return result.scalars().all() + @classmethod async def get_std_code_map_list(cls, db: AsyncSession, query_object: DataStdCodeModel, is_page: bool = False): # 构建查询条件 diff --git a/vue-fastapi-backend/module_admin/entity/vo/datastd_vo.py b/vue-fastapi-backend/module_admin/entity/vo/datastd_vo.py index 929ca75..d3c3d3b 100644 --- a/vue-fastapi-backend/module_admin/entity/vo/datastd_vo.py +++ b/vue-fastapi-backend/module_admin/entity/vo/datastd_vo.py @@ -117,7 +117,8 @@ class DataStdMainModel(BaseModel): data_std_cn_name: Optional[str] = Field(default=None, description='标准中文名') data_std_type: Optional[str] = Field(default=None, description='标准类型(0:基础数据 1:指标数据)') data_sec_lvl: Optional[str] = Field(default=None, description='安全等级') - src_sys: Optional[int] = Field(default=None, description='归属系统') + data_std_vest: Optional[str] = Field(default=None, description='标准归属(sys:系统级 company:公司级') + src_sys: Optional[int] = Field(default=None, description='系统ID') data_std_busi_defn: Optional[str] = Field(default=None, description='标准业务定义') cd_id: Optional[str] = Field(default=None, description='代码id') std_status: Optional[str] = Field(default=None, description='标准状态(1:有效 0:无效)') diff --git a/vue-fastapi-backend/module_admin/service/datastd_service.py b/vue-fastapi-backend/module_admin/service/datastd_service.py index 2887cab..f8c9715 100644 --- a/vue-fastapi-backend/module_admin/service/datastd_service.py +++ b/vue-fastapi-backend/module_admin/service/datastd_service.py @@ -1,4 +1,4 @@ -from fastapi import Request +from fastapi import Request,UploadFile,HTTPException from sqlalchemy.ext.asyncio import AsyncSession from exceptions.exception import ServiceException from module_admin.dao.datastd_dao import DataStdDao @@ -6,15 +6,22 @@ from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.datastd_vo import DataStdCodeModel, DeleteDataStdModel, DataStdDictModel, DataStdMainModel,\ DataStdMainApprModel, DataStdDictApprModel, DataStdCodeApprModel, DataStdCodePageQueryModel, StdDictNoPageParam from utils.common_util import CamelCaseUtil +from module_admin.entity.vo.datastd_vo import DataStdCodeModel,DeleteDataStdModel,DataStdDictModel,DataStdMainModel,DataStdMainApprModel,DataStdDictApprModel,DataStdCodeApprModel,DataStdCodePageQueryModel import uuid from module_admin.entity.vo.approval_vo import ApplyModel from module_admin.service.approval_service import ApprovalService +from module_admin.service.metatask_service import MetataskService from collections import defaultdict from datetime import datetime from config.constant import CommonConstant from module_admin.entity.vo.data_ast_content_vo import DataCatalogPageQueryModel, DeleteDataCatalogModel,DataCatalogResponseWithChildren,DataCatalogMovedRequest,DataCatalogMergeRequest,DataCatalogChild,DataCatalogMoverelRequest from module_admin.entity.vo.user_vo import CurrentUserModel - +from utils.common_util import CamelCaseUtil, export_list2excel, get_excel_template +import io +from module_admin.service.config_service import ConfigService +import requests +import pandas as pd +from config.env import AppConfig class DataStdService: """ 数据源标准服务层 @@ -1254,26 +1261,6 @@ class DataStdService: await ApprovalService.apply_services(query_db, applyModel, 'dataStdMain') return CrudResponseModel(is_success=True, message='新增标准成功') @classmethod - async def add_std_dict_appr(cls, query_db: AsyncSession, model: DataStdDictModel): - if not await cls.check_dict_unique_services(query_db, model): - raise ServiceException(message=f"字典编号 {model.data_dict_no} 已存在") - model.onum=str(uuid.uuid4()) - model.data_dict_stat="1" - # 将 DataStdMainModel 转换为 DataStdMainApprModel,保留字段原始名 - apprModel = DataStdDictApprModel(**model.model_dump(exclude_unset=True, by_alias=True)) - apprModel.changeType="add" - apprModel.compareId=model.onum - apprModel.oldInstId=model.onum - apprModel.approStatus="waiting" - apprModel.flowId=str(uuid.uuid4()) - await DataStdDao.add_std_dict_appr(query_db, apprModel) - applyModel = ApplyModel() - applyModel.businessType = "dataStdDict" - applyModel.businessId = apprModel.flowId - applyModel.applicant = apprModel.create_by - await ApprovalService.apply_services(query_db, applyModel, 'dataStdDict') - return CrudResponseModel(is_success=True, message='新增标准成功') - @classmethod async def edit_std_main_appr(cls, query_db: AsyncSession, model: DataStdMainModel): if not await cls.check_std_num_unique(query_db, model): raise ServiceException(message=f"标准编号 {model.data_std_no} 已存在") @@ -1302,6 +1289,27 @@ class DataStdService: await ApprovalService.apply_services(query_db, applyModel, 'dataStdMain') return CrudResponseModel(is_success=True, message='修改标准成功') @classmethod + async def add_std_dict_appr(cls, query_db: AsyncSession, model: DataStdDictModel): + if not await cls.check_dict_unique_services(query_db, model): + raise ServiceException(message=f"字典编号 {model.data_dict_no} 已存在") + model.onum=str(uuid.uuid4()) + model.data_dict_stat="1" + # 将 DataStdMainModel 转换为 DataStdMainApprModel,保留字段原始名 + apprModel = DataStdDictApprModel(**model.model_dump(exclude_unset=True, by_alias=True)) + apprModel.changeType="add" + apprModel.compareId=model.onum + apprModel.oldInstId=model.onum + apprModel.approStatus="waiting" + apprModel.flowId=str(uuid.uuid4()) + await DataStdDao.add_std_dict_appr(query_db, apprModel) + applyModel = ApplyModel() + applyModel.businessType = "dataStdDict" + applyModel.businessId = apprModel.flowId + applyModel.applicant = apprModel.create_by + await ApprovalService.apply_services(query_db, applyModel, 'dataStdDict') + return CrudResponseModel(is_success=True, message='新增标准成功') + + @classmethod async def edit_std_dict_appr(cls, query_db: AsyncSession, model: DataStdDictModel): if not await cls.check_dict_unique_services(query_db, model): raise ServiceException(message=f"字典编号 {model.c} 已存在") @@ -1327,7 +1335,7 @@ class DataStdService: applyModel.businessId = apprModel.flowId applyModel.applicant = apprModel.create_by await ApprovalService.apply_services(query_db, applyModel, 'dataStdDict') - return CrudResponseModel(is_success=True, message='修改标准成功') + return CrudResponseModel(is_success=True, message='修改数据字典成功') @classmethod async def delete_std_main_Appr(cls, query_db: AsyncSession, ids: str): if ids: @@ -1577,4 +1585,191 @@ class DataStdService: return { "tableData": table_data, "children": children - } \ No newline at end of file + } + + @staticmethod + async def get_main_import_template_services(): + """ + 获取用户导入模板service + + :return: 用户导入模板excel的二进制数据 + """ + header_list = ['标准归属', '来源系统', '标准编号', '标准中文名', '标准英文名', '标准业务定义', '标准类型', '标准来源', '数据类别', '标准业务定义', '安全等级', '代码编号', '业务认则部门', '业务认则人员', '技术认则部门', '技术认则人员'] + selector_header_list = ['标准归属', '标准类型',"标准来源"] + option_list = [{'标准归属': ['公司级', '系统级']}, {'标准类型': ['基础数据', '指标数据']}, {'标准来源': ['行业标准', '自建标准']}] + binary_data = get_excel_template( + header_list=header_list, selector_header_list=selector_header_list, option_list=option_list + ) + + return binary_data + + + + @classmethod + async def batch_import_std_services( + cls, + request: Request, + query_db: AsyncSession, + file: UploadFile, + current_user: CurrentUserModel + ): + # Step 1: 读取 Excel + content = await file.read() + try: + df = pd.read_excel(io.BytesIO(content)) + except Exception: + raise HTTPException(status_code=400, detail="Excel 文件解析失败") + + if df.empty: + raise HTTPException(status_code=400, detail="导入文件内容为空") + std_type_mapping = { + '基础数据': '0', + '指标数据': '1' + } + std_vest_mapping = { + '公司级': 'company', + '系统级': 'sys' + } + # 获取全量标准代码 + std_code_list = await DataStdDao.get_std_code_list_all(query_db) + + # Step 2: 表头映射(中文转英文字段名) + header_dict = { + '标准归属': 'data_std_vest', + '来源系统': 'std_source_system', + '标准编号': 'data_std_no', + '标准中文名': 'std_name_zh', + '标准英文名': 'std_name_en', + '标准业务定义': 'std_definition', + '标准类型': 'std_type', + '标准来源': 'std_source', + '数据类别': 'data_category', + '安全等级': 'security_level', + '代码编号': 'code_no', + '业务认则部门': 'biz_dept', + '业务认则人员': 'biz_person', + '技术认则部门': 'tech_dept', + '技术认则人员': 'tech_person', + } + df.rename(columns=header_dict, inplace=True) + # ds系统列表 + data_tree_result = await MetataskService.get_data_source_tree( request,current_user) + + # Step 3: 生成统一 flowId + batch_flow_id = str(uuid.uuid4()) + + try: + for idx, row in df.iterrows(): + # VO 数据校验 + try: + input_code_no = row.get('code_no') + matched_code = next((item for item in std_code_list if item.cd_no == input_code_no), None) + + if input_code_no and not matched_code: + raise HTTPException(status_code=400, detail=f"第 {idx + 2} 行导入失败,代码编号 [{input_code_no}] 在系统中不存在") + # 获取来源系统字段 + input_src_sys = row.get("std_source_system") + matched_source = next((ds for ds in data_tree_result if ds.name == input_src_sys or str(ds.id) == str(input_src_sys)), None) + + # 如果能匹配到系统,则使用 id;否则判断是否为空 + if matched_source: + src_sys_id = matched_source.id + elif not input_src_sys or str(input_src_sys).strip() == "": + src_sys_id = 10000 + else: + raise HTTPException(status_code=400, detail=f"第 {idx + 2} 行导入失败,来源系统 [{input_src_sys}] 不存在") + + # 如果存在,使用对应 onum 作为 cdId + cd_id = matched_code.onum if matched_code else None + vo = DataStdMainModel( + dataStdVest=std_vest_mapping.get(row.get('data_std_vest'), 'company'), # 默认转为 'company', + srcSys=src_sys_id, + dataStdNo=row.get('data_std_no'), + dataStdCnName=row.get('std_name_zh'), + dataStdEngName=row.get('std_name_en'), + dataStdBusiDefn=row.get('std_definition'), + dataStdType=std_type_mapping.get(row.get('std_type'), '1'), # 默认转为 '1', + dataStdSrc=row.get('std_source'), + dataClas=row.get('data_category'), + dataSecLvl=str(row.get('security_level')), + cdId=cd_id, + dataStdBusiOwnershipDept=row.get('biz_dept'), + dataStdBusiOwnershipPrsn=row.get('biz_person'), + dataStdItOwnershipDept=row.get('tech_dept'), + dataStdItOwnershipPrsn=row.get('tech_person'), + beltDataStdContent=2, # 固定值 + ) + except Exception as ve: + raise HTTPException(status_code=400, detail=f"第 {idx + 2} 行数据校验失败: {ve}") + + # 构造查询对象并检查是否存在 + query_obj = DataStdMainModel(dataStdNo=vo.data_std_no) + exist_model = await DataStdDao.get_data_main_by_info(query_db, query_obj) + + # 构造审批模型(无论新增/修改) + model = DataStdMainModel(**vo.model_dump(exclude_unset=True, by_alias=True)) + model.create_by = current_user.user.user_name + model.std_status = "1" + model.belt_data_std_content = 2 + model.create_time = datetime.now() + + if exist_model: + # === 修改审批逻辑 === + # 标准正在审批中则抛错 + wating_list = await DataStdDao.check_std_main_waiting(exist_model.onum, query_db) + if wating_list: + raise ServiceException(message=f"第 {idx + 2} 行数据标准正在审批中,请等待审批完成") + + last_appr = await DataStdDao.get_last_std_main_appr_by_id(query_db, exist_model.onum) + model.onum = exist_model.onum + + appr_model = DataStdMainApprModel(**model.model_dump(exclude_unset=True, by_alias=True)) + appr_model.changeType = "edit" + appr_model.onum = str(uuid.uuid4()) + appr_model.oldInstId = model.onum + appr_model.compareId = last_appr.onum if last_appr else model.onum + appr_model.approStatus = "waiting" + appr_model.flowId = batch_flow_id + else: + # === 新增审批逻辑 === + model.onum = str(uuid.uuid4()) + appr_model = DataStdMainApprModel(**model.model_dump(exclude_unset=True, by_alias=True)) + appr_model.changeType = "add" + appr_model.compareId = model.onum + appr_model.oldInstId = model.onum + appr_model.oldInstId = model.onum + appr_model.approStatus = "waiting" + appr_model.flowId = batch_flow_id + + # 保存审批数据 + await DataStdDao.add_std_main_appr(query_db, appr_model) + + # 全部处理完成后统一发起审批流程 + apply_model = ApplyModel() + apply_model.businessType = "dataStdMain" + apply_model.businessId = batch_flow_id + apply_model.applicant = current_user.user.user_name + await ApprovalService.apply_services(query_db, apply_model, 'dataStdMain') + + await query_db.commit() + return CrudResponseModel(is_success=True, message="批量导入标准成功") + + except Exception as e: + await query_db.rollback() + raise ServiceException(message=f"导入失败:{str(e)}") + + @staticmethod + async def get_dict_import_template_services(): + """ + 获取用户导入模板service + + :return: 用户导入模板excel的二进制数据 + """ + header_list = ['部门编号', '登录名称', '用户名称', '用户邮箱', '手机号码', '用户性别', '帐号状态'] + selector_header_list = ['用户性别', '帐号状态'] + option_list = [{'用户性别': ['男', '女', '未知']}, {'帐号状态': ['正常', '停用']}] + binary_data = get_excel_template( + header_list=header_list, selector_header_list=selector_header_list, option_list=option_list + ) + + return binary_data \ No newline at end of file diff --git a/vue-fastapi-frontend/src/views/datastd/main/index.vue b/vue-fastapi-frontend/src/views/datastd/main/index.vue index cd94876..81efc00 100644 --- a/vue-fastapi-frontend/src/views/datastd/main/index.vue +++ b/vue-fastapi-frontend/src/views/datastd/main/index.vue @@ -208,6 +208,14 @@ >修改 + + 导入 + + + + + 将文件拖到此处,或点击上传 + + + + 仅允许导入xls、xlsx格式文件。 + 下载模板 + + + + + + + @@ -445,6 +485,7 @@ import { import useUserStore from '@/store/modules/user' import { nextTick } from 'vue' import codeItemCommon from '../stdcode/codeItemCommon.vue' +import { getToken } from "@/utils/auth"; import { datasourcetree } from "@/api/meta/metatask"; const { proxy } = getCurrentInstance() @@ -463,7 +504,21 @@ const handleTargetCatalogNodeClick = (data) => { } const codeMapId = ref(null); const mapVisible = ref(false); - +/**文件上传中处理 */ +const handleFileUploadProgress = (event, file, fileList) => { + upload.isUploading = true; +}; +const handleFileSuccess = (response, file, fileList) => { + upload.open = false; + upload.isUploading = false; + proxy.$refs["uploadRef"].handleRemove(file); + proxy.$alert("" + response.msg + "", "导入结果", { dangerouslyUseHTMLString: true }); + getList(); +}; +function importTemplate() { + proxy.download("datastd/stdMain/importTemplate", { + }, `数据标准_template_${new Date().getTime()}.xlsx`); +}; const directoryTableData = ref([]) const queryParams = ref({ dataStdEngName: '', @@ -515,7 +570,9 @@ const handleQuery = () => { queryParams.value.pageNum = 1; getList(); }; - +function submitFileForm() { + proxy.$refs["uploadRef"].submit(); +}; const submitTree = async () => { const response = await changeStdMainOum({onum:ids.value.toString(),beltDataStdContent:chooseOnumNum.value}); if (response.success){ @@ -605,7 +662,21 @@ const handleAdd = () => { // 清空选中的数据 dialogVisible.value = true; }; - +/*** 用户导入参数 */ +const upload = reactive({ + // 是否显示弹出层(用户导入) + open: false, + // 弹出层标题(用户导入) + title: "", + // 是否禁用上传 + isUploading: false, + // 是否更新已经存在的用户数据 + updateSupport: 0, + // 设置上传的请求头部 + headers: { Authorization: "Bearer " + getToken() }, + // 上传的地址 + url: import.meta.env.VITE_APP_BASE_API + "/datastd/stdMain/importData" +}); const handleEdit = (row) => { const id = row.onum ? row.onum : ids.value.toString(); getStdMain(id).then((response) => { @@ -666,7 +737,11 @@ const isCollection = (data) => { const isCollected = (data) => { return data.bookmarkFlag === 1 } - +/** 导入按钮操作 */ +function handleImport() { + upload.title = "用户导入"; + upload.open = true; +}; // 是否子分类 const isDirectory = (data) => {