From 43e77455b771accb2b4d2098c1e57e1b49094e29 Mon Sep 17 00:00:00 2001 From: "si@aidatagov.com" Date: Wed, 19 Nov 2025 01:48:18 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/service/metasecurity_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vue-fastapi-backend/module_admin/service/metasecurity_service.py b/vue-fastapi-backend/module_admin/service/metasecurity_service.py index 6fe6078..e8bb90f 100644 --- a/vue-fastapi-backend/module_admin/service/metasecurity_service.py +++ b/vue-fastapi-backend/module_admin/service/metasecurity_service.py @@ -681,7 +681,7 @@ async def replace_table_with_subquery(ctrSqlDict, oldStrSql): "USER", "CURRENT_DATE", "CURRENT_TIME", "CURRENT_TIMESTAMP", } # 动态获取子查询 - if original_table in ctrSqlDict and alias_name not in sql_keywords: + if original_table in ctrSqlDict and alias_name and alias_name.upper().split()[0] not in sql_keywords: # 使用 ctrSqlDict 中的子查询替换表名 replaced = f"{keyword} ({ctrSqlDict[original_table]}) {alias_part}" else: From 8b37fdb9d53bcb00372a4038d2fc17a6d6aea079 Mon Sep 17 00:00:00 2001 From: "si@aidatagov.com" Date: Sun, 23 Nov 2025 04:24:22 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=A0=87=E5=87=86=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AF=BC=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/datastd_controller.py | 24 +- .../module_admin/dao/datastd_dao.py | 32 +++ .../module_admin/service/datastd_service.py | 246 +++++++++++++++++- .../src/views/datastd/stdcode/index.vue | 78 +++++- .../views/metadataConfig/bizConfig/index.vue | 22 +- .../src/views/system/flow/dataStdCodeAppr.vue | 2 +- 6 files changed, 396 insertions(+), 8 deletions(-) diff --git a/vue-fastapi-backend/module_admin/controller/datastd_controller.py b/vue-fastapi-backend/module_admin/controller/datastd_controller.py index 2f558c9..bec0db2 100644 --- a/vue-fastapi-backend/module_admin/controller/datastd_controller.py +++ b/vue-fastapi-backend/module_admin/controller/datastd_controller.py @@ -878,4 +878,26 @@ async def save_tsmcb(request: Request, query_db: AsyncSession = Depends(get_db), current_user: CurrentUserModel = Depends(LoginService.get_current_user)): result = await DataStdService.edit_std_main_status(query_db, saveSscfModel) - return ResponseUtil.success(msg=result.message) \ No newline at end of file + return ResponseUtil.success(msg=result.message) + +@datastdController.post('/stdCode/importTemplate', dependencies=[Depends(CheckUserInterfaceAuth('system:user:import'))]) +async def export_std_code_template(request: Request, query_db: AsyncSession = Depends(get_db)): + code_import_template_result = await DataStdService.get_code_import_template_services() + logger.info('获取成功') + + return ResponseUtil.streaming(data=bytes2file_response(code_import_template_result)) +@datastdController.post('/stdCode/importData', dependencies=[Depends(CheckUserInterfaceAuth('system:user:import'))]) +@Log(title='数据标准', business_type=BusinessType.IMPORT) +async def batch_import_stdCode( + 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_code_std_services( + request, query_db, file, current_user + ) + logger.info(batch_import_result.message) + + return ResponseUtil.success(msg=batch_import_result.message) \ 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 9e6b8aa..3bac3a6 100644 --- a/vue-fastapi-backend/module_admin/dao/datastd_dao.py +++ b/vue-fastapi-backend/module_admin/dao/datastd_dao.py @@ -1344,3 +1344,35 @@ class DataStdDao: # 返回 FlowApproval 对象或布尔值 return approval # 或 return approval is not None + @classmethod + async def check_std_code_waiting(cls, oldInstId: str, db: AsyncSession): + """ + 检查 std_main_appr 的第一条记录是否存在待审批状态(pending 或 waiting) + """ + # 1. 查询 DataStdDictAppr 中的所有记录(按时间倒序) + query = ( + select(DataStdCodeAppr) + .where(DataStdCodeAppr.oldInstId == oldInstId) + .order_by(desc(DataStdCodeAppr.create_time)) + ) + result = await db.execute(query) + first_record = result.scalars().first() + + if not first_record: + return None # 或 raise 自定义异常,例如“未找到审批记录” + + # 2. 获取 instid,去 FlowApproval 中查询是否存在 pending 或 waiting 状态 + instid = first_record.flowId + + approval_query = ( + select(FlowApproval) + .where( + FlowApproval.businessId == instid, + FlowApproval.status.in_(["pending", "waiting"]) + ) + ) + approval_result = await db.execute(approval_query) + approval = approval_result.scalars().first() + + # 返回 FlowApproval 对象或布尔值 + return approval # 或 return approval is not None \ No newline at end of file diff --git a/vue-fastapi-backend/module_admin/service/datastd_service.py b/vue-fastapi-backend/module_admin/service/datastd_service.py index 50989cb..555b182 100644 --- a/vue-fastapi-backend/module_admin/service/datastd_service.py +++ b/vue-fastapi-backend/module_admin/service/datastd_service.py @@ -1773,7 +1773,21 @@ class DataStdService: ) return binary_data + @staticmethod + async def get_code_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( @@ -2138,4 +2152,234 @@ class DataStdService: await query_db.rollback() raise ServiceException(message=f"导入失败:{str(e)}") - \ No newline at end of file + @classmethod + async def batch_import_code_std_services( + cls, + request: Request, + query_db: AsyncSession, + file: UploadFile, + current_user: CurrentUserModel + ): + """ + 批量导入标准代码(带审批)。 + Excel 表头(中文)必须包含: + ['代码归属','归属系统','标准代码编号','标准代码名称','代码值','代码含义'] + + 规则: + - 每行表示一个 codeItem(子项),必须包含标准代码编号(父级) + - 按 标准代码编号 分组,父级取组首行 + - 父/子存在走修改审批,不存在走新增审批 + - 整批使用同一 flowId,写审批表后发起审批 + """ + import io + import pandas as pd + import uuid + from datetime import datetime + from fastapi import HTTPException + + # 读 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="导入文件内容为空") + + # 中文表头 -> 内部列名 + header_dict = { + '代码归属': 'code_vest', + '归属系统': 'source_system', + '标准代码编号': 'std_code_no', + '标准代码名称': 'std_code_name', + '代码值': 'code_value', + '代码含义': 'code_meaning' + } + df.rename(columns=header_dict, inplace=True) + + # 归属映射 + vest_mapping = { + '公司级': 'company', + '系统级': 'sys' + } + + def safe_str(val): + if pd.isna(val): + return "" + return str(val).strip() + + # 获取系统树 + data_tree_result = await MetataskService.get_data_source_tree(request, current_user) + + # 获取系统中已有父级标准代码 + std_codes_all = await DataStdDao.get_std_code_list_all(query_db) # 返回 DataStdCode 对象列表(class_id == 'code') + + batch_flow_id = str(uuid.uuid4()) + + # 基本校验 + if 'std_code_no' not in df.columns: + raise HTTPException(status_code=400, detail="导入文件缺少列:标准代码编号") + + # 统一空值并按 std_code_no 分组 + df['std_code_no'] = df['std_code_no'].fillna("").astype(str) + grouped = df.groupby(df['std_code_no']) + + try: + for std_code_no, group in grouped: + # 跳过空组或空行 + if not std_code_no or std_code_no.strip() == "": + first_idx = int(group.index[0]) + raise HTTPException(status_code=400, detail=f"第 {first_idx + 2} 行导入失败:缺少标准代码编号") + + # 匹配系统中是否已有该父级(匹配 cd_no) + matched_std = next( + (s for s in std_codes_all if getattr(s, 'cd_no', None) == std_code_no), + None + ) + + # 取组首行作为父级信息来源 + first_row = group.iloc[0] + parent_src_input = safe_str(first_row.get('source_system')) + matched_source = next( + (ds for ds in data_tree_result if getattr(ds, 'name', None) == parent_src_input or str(getattr(ds, 'id', '')) == parent_src_input), + None + ) + if matched_source: + src_sys_id = int(getattr(matched_source, 'id')) + elif not parent_src_input: + src_sys_id = 10000 + else: + first_idx = int(group.index[0]) + raise HTTPException(status_code=400, detail=f"第 {first_idx + 2} 行导入失败:来源系统 [{parent_src_input}] 不存在") + + # 构造父级 model(class_id = 'code') + parent_model = DataStdCodeModel( + onum=(matched_std.onum if matched_std else None), + cdNo=std_code_no, + cdValCnMean=safe_str(first_row.get('std_code_name')), + cdType=vest_mapping.get(safe_str(first_row.get('code_vest')), 'company'), + cdVal_stat="1", + srcSys=src_sys_id, + classId="code", + ) + + # 判断父级是否存在正式表(使用 DAO.get_data_code_by_info) + exist_parent = await DataStdDao.get_data_code_by_info(query_db, DataStdCodeModel(cdNo=parent_model.cd_no, classId='code')) + + # 父级审批准备(存在->update,不存在->add) + if exist_parent: + # 如果项目中存在检查“是否有待审批”函数,应该在此检查以避免重复发起审批(见下方需补充清单) + waiting = False + if hasattr(DataStdDao, "check_std_code_waiting"): + waiting = await DataStdDao.check_std_code_waiting(exist_parent.onum, query_db) + if waiting: + raise ServiceException(message=f"标准代码 [{std_code_no}] 正在审批中,请等待审批完成") + parent_model.onum = exist_parent.onum + parent_model.create_by = current_user.user.user_name + parent_model.create_time = datetime.now() + parent_appr = DataStdCodeApprModel(**parent_model.model_dump(exclude_unset=True, by_alias=True)) + last_appr = await DataStdDao.get_last_std_code_appr_by_id(query_db, parent_model.onum) + parent_appr.changeType = "update" + parent_appr.onum = str(uuid.uuid4()) + parent_appr.compareId = last_appr.onum if last_appr else parent_model.onum + parent_appr.oldInstId = parent_model.onum + parent_appr.approStatus = "waiting" + parent_appr.flowId = batch_flow_id + await DataStdDao.add_std_code_appr(query_db, parent_appr) + parent_appr_onum = parent_appr.onum + else: + # 父级新增审批 + parent_model.onum = str(uuid.uuid4()) + parent_model.create_by = current_user.user.user_name + parent_model.create_time = datetime.now() + + parent_appr = DataStdCodeApprModel(**parent_model.model_dump(exclude_unset=True, by_alias=True)) + parent_appr.changeType = "add" + parent_appr.compareId = parent_model.onum + parent_appr.oldInstId = parent_model.onum + parent_appr.approStatus = "waiting" + parent_appr.flowId = batch_flow_id + await DataStdDao.add_std_code_appr(query_db, parent_appr) + parent_appr_onum = parent_appr.onum + + # 处理代码项(组内每行) + for row_idx, row in group.iterrows(): + row_display = int(row_idx) + 2 + try: + code_value = safe_str(row.get('code_value')) + code_meaning = safe_str(row.get('code_meaning')) + + if code_value == "": + raise HTTPException(status_code=400, detail=f"第 {row_display} 行导入失败:代码值为空") + + item_model = DataStdCodeModel( + cdNo=code_value, + cdValCnMean=code_meaning, + cdType=vest_mapping.get(safe_str(row.get('code_vest')), parent_model.cd_type), + cdValStat="1", + srcSys=src_sys_id, + parentId=parent_model.onum, + classId="codeItem", + ) + + # 查询正式表中该代码项是否存在(按 parent_id + cd_no) + exist_item = await DataStdDao.get_data_code_by_info(query_db, DataStdCodeModel(parentId=item_model.parent_id, cdNo=item_model.cd_no, classId='codeItem')) + + if exist_item: + waiting_item = False + waiting_item = await DataStdDao.check_std_code_waiting(exist_item.onum, query_db) + if waiting_item: + raise ServiceException(message=f"第 {row_display} 行导入失败:代码项 [{code_value}] 正在审批中,请等待审批完成") + + item_model.onum = exist_item.onum + item_model.create_by = current_user.user.user_name + item_model.create_time = datetime.now() + + item_appr = DataStdCodeApprModel(**item_model.model_dump(exclude_unset=True, by_alias=True)) + last_item_appr = await DataStdDao.get_last_std_code_appr_by_id(query_db, item_model.onum) + item_appr.changeType = "update" + item_appr.onum = str(uuid.uuid4()) + item_appr.compareId = last_item_appr.onum if last_item_appr else item_model.onum + item_appr.oldInstId = item_model.onum + item_appr.parent_id = parent_appr_onum + item_appr.approStatus = "waiting" + item_appr.flowId = batch_flow_id + await DataStdDao.add_std_code_appr(query_db, item_appr) + else: + # 新增子项审批 + item_model.onum = str(uuid.uuid4()) + item_model.create_by = current_user.user.user_name + item_model.create_time = datetime.now() + + item_appr = DataStdCodeApprModel(**item_model.model_dump(exclude_unset=True, by_alias=True)) + item_appr.changeType = "add" + item_appr.compareId = item_model.onum + item_appr.oldInstId = item_model.onum + item_appr.parent_id = parent_appr_onum + item_appr.approStatus = "waiting" + item_appr.flowId = batch_flow_id + await DataStdDao.add_std_code_appr(query_db, item_appr) + + except Exception as row_err: + # 行级校验失败 -> 抛出以触发外层 rollback + raise HTTPException(status_code=400, detail=f"第 {row_display} 行导入失败:{str(row_err)}") + + # 全部行处理完,发起审批(单一 flow) + apply_model = ApplyModel() + apply_model.businessType = "dataStdCode" + apply_model.businessId = batch_flow_id + apply_model.applicant = current_user.user.user_name + await ApprovalService.apply_services(query_db, apply_model, 'dataStdCode') + + await query_db.commit() + return CrudResponseModel(is_success=True, message="批量导入标准代码成功(已提交审批)") + + except Exception as e: + await query_db.rollback() + if isinstance(e, HTTPException): + raise e + if isinstance(e, ServiceException): + raise e + raise ServiceException(message=f"导入失败:{str(e)}") + \ No newline at end of file diff --git a/vue-fastapi-frontend/src/views/datastd/stdcode/index.vue b/vue-fastapi-frontend/src/views/datastd/stdcode/index.vue index 6697d13..a120713 100644 --- a/vue-fastapi-frontend/src/views/datastd/stdcode/index.vue +++ b/vue-fastapi-frontend/src/views/datastd/stdcode/index.vue @@ -67,6 +67,14 @@ v-hasPermi="['meta:metaSecurityCol:edit']" >修改 + + 导入 + + + + +
将文件拖到此处,或点击上传
+ +
+ +
@@ -189,6 +229,7 @@ import CodeItem from './codeItem.vue'; // 引入第二个页面组件 import codeItemCommon from './codeItemCommon.vue'; // 引入第二个页面组件 import codeMap from './codeMap'; // 引入第二个页面组件 import codeStdMap from './codeStdMap.vue'; +import { getToken } from "@/utils/auth"; const { proxy } = getCurrentInstance(); const { std_code_status,std_code_appr } = proxy.useDict("std_code_status","std_code_appr"); @@ -214,12 +255,41 @@ const defaultProps = { const dialogVisible2 = ref(false); const dialogTitle2 = ref('标准代码'); const selectedRow = ref(null); // 传递给 codeItem 组件的数据 - +function handleImport() { + upload.title = "代码导入"; + upload.open = true; +}; const getSrcSysName = (id) => { const match = dbResourceOldList.value.find(item => item.id === id); return match ? match.name : id; }; - +function submitFileForm() { + proxy.$refs["uploadRef"].submit(); +}; +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(); +}; +const upload = reactive({ + // 是否显示弹出层(用户导入) + open: false, + // 弹出层标题(用户导入) + title: "", + // 是否禁用上传 + isUploading: false, + // 是否更新已经存在的用户数据 + updateSupport: 0, + // 设置上传的请求头部 + headers: { Authorization: "Bearer " + getToken() }, + // 上传的地址 + url: import.meta.env.VITE_APP_BASE_API + "/datastd/stdCode/importData" +}); const filterNode = (value, data) => { if (!value) return true; return data.name.indexOf(value) !== -1; @@ -235,6 +305,10 @@ const handleNodeClick = (data) => { handleQuery(); }; +function importTemplate() { + proxy.download("datastd/stdCode/importTemplate", { + }, `标准代码_template_${new Date().getTime()}.xlsx`); +}; watch(dbResoursName, (val) => { proxy.$refs["tree"].filter(val); diff --git a/vue-fastapi-frontend/src/views/metadataConfig/bizConfig/index.vue b/vue-fastapi-frontend/src/views/metadataConfig/bizConfig/index.vue index 6d90ab3..f02bfc1 100644 --- a/vue-fastapi-frontend/src/views/metadataConfig/bizConfig/index.vue +++ b/vue-fastapi-frontend/src/views/metadataConfig/bizConfig/index.vue @@ -168,7 +168,15 @@ @selection-change="handleLeftSelect" > - + + + @@ -198,8 +206,15 @@ @selection-change="handleRightSelect" > - - + + +
@@ -217,6 +232,7 @@ import { ref, reactive, onMounted } from 'vue' import { ElMessageBox, ElMessage } from 'element-plus' import useUserStore from '@/store/modules/user'; // 注意路径是否正确 import {getMetaDataList} from "@/api/meta/metaInfo" +import { getNameById, getIdByName } from '@/utils/dsSysUtils'; const userStore = useUserStore(); // 正确调用 const dsSysList = userStore.dsSysList; // 访问属性 diff --git a/vue-fastapi-frontend/src/views/system/flow/dataStdCodeAppr.vue b/vue-fastapi-frontend/src/views/system/flow/dataStdCodeAppr.vue index 9affb4b..0b1c3e3 100644 --- a/vue-fastapi-frontend/src/views/system/flow/dataStdCodeAppr.vue +++ b/vue-fastapi-frontend/src/views/system/flow/dataStdCodeAppr.vue @@ -12,7 +12,7 @@