From b148bc06c96797950bf24f8efd669682ec8ca5e1 Mon Sep 17 00:00:00 2001 From: fit2cloud-chenyw Date: Tue, 23 Dec 2025 09:33:24 +0800 Subject: [PATCH 1/2] feat: User Batch Import --- backend/apps/system/api/user.py | 22 ++++++- backend/apps/system/crud/user_excel.py | 64 +++++++++++++++++++ backend/locales/en.json | 13 +++- backend/locales/ko-KR.json | 13 +++- backend/locales/zh-CN.json | 13 +++- frontend/src/api/user.ts | 2 +- frontend/src/views/system/user/User.vue | 12 ++-- frontend/src/views/system/user/UserImport.vue | 7 +- 8 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 backend/apps/system/crud/user_excel.py diff --git a/backend/apps/system/api/user.py b/backend/apps/system/api/user.py index 078754a3..b91fbe96 100644 --- a/backend/apps/system/api/user.py +++ b/backend/apps/system/api/user.py @@ -1,9 +1,10 @@ from collections import defaultdict from typing import Optional -from fastapi import APIRouter, Path, Query +from fastapi import APIRouter, File, Path, Query, UploadFile from pydantic import Field from sqlmodel import SQLModel, or_, select, delete as sqlmodel_delete from apps.system.crud.user import check_account_exists, check_email_exists, check_email_format, check_pwd_format, get_db_user, single_delete, user_ws_options +from apps.system.crud.user_excel import batchUpload, downTemplate from apps.system.models.system_model import UserWsModel, WorkspaceModel from apps.system.models.user import UserModel from apps.system.schemas.auth import CacheName, CacheNamespace @@ -19,6 +20,15 @@ router = APIRouter(tags=["system_user"], prefix="/user") + +@router.get("/template") +async def templateExcel(trans: Trans): + return await downTemplate(trans) + +@router.post("/upload") +async def upload_excel(trans: Trans, current_user: CurrentUser, file: UploadFile = File(...)): + batchUpload(trans, file) + @router.get("/info", summary=f"{PLACEHOLDER_PREFIX}system_user_current_user", description=f"{PLACEHOLDER_PREFIX}system_user_current_user_desc") async def user_info(current_user: CurrentUser) -> UserInfoDTO: return current_user @@ -266,4 +276,12 @@ async def statusChange(session: SessionDep, current_user: CurrentUser, trans: Tr db_user: UserModel = get_db_user(session=session, user_id=statusDto.id) db_user.status = status session.add(db_user) - session.commit() \ No newline at end of file + session.commit() + + + +""" async def batchUpload(): + pass + +async def errorData(): + pass """ \ No newline at end of file diff --git a/backend/apps/system/crud/user_excel.py b/backend/apps/system/crud/user_excel.py new file mode 100644 index 00000000..1f00dc41 --- /dev/null +++ b/backend/apps/system/crud/user_excel.py @@ -0,0 +1,64 @@ + + +import asyncio +from http.client import HTTPException +import io +from fastapi.responses import StreamingResponse +import pandas as pd + + +async def downTemplate(trans): + def inner(): + data = { + trans('i18n_user.account'): ['sqlbot1', 'sqlbot2'], + trans('i18n_user.name'): ['sqlbot_employee1', 'sqlbot_employee2'], + trans('i18n_user.email'): ['employee1@sqlbot.com', 'employee2@sqlbot.com'], + trans('i18n_user.workspace'): [trans('i18n_default_workspace'), trans('i18n_default_workspace')], + trans('i18n_user.role'): [trans('i18n_user.administrator'), trans('i18n_user.ordinary_member')], + trans('i18n_user.status'): [trans('i18n_user.status_enabled'), trans('i18n_user.status_disabled')], + trans('i18n_user.origin'): [trans('i18n_user.local_creation'), trans('i18n_user.local_creation')], + trans('i18n_user.platform_user_id'): [None, None], + } + df = pd.DataFrame(data) + buffer = io.BytesIO() + with pd.ExcelWriter(buffer, engine='xlsxwriter', engine_kwargs={'options': {'strings_to_numbers': False}}) as writer: + df.to_excel(writer, sheet_name='Sheet1', index=False) + + workbook = writer.book + worksheet = writer.sheets['Sheet1'] + + header_format = workbook.add_format({ + 'bold': True, + 'font_size': 12, + 'font_name': '微软雅黑', + 'align': 'center', + 'valign': 'vcenter', + 'border': 0, + 'text_wrap': False, + }) + + for i, col in enumerate(df.columns): + max_length = max( + len(str(col).encode('utf-8')) * 1.1, + (df[col].astype(str)).apply(len).max() + ) + worksheet.set_column(i, i, max_length + 12) + + worksheet.write(0, i, col, header_format) + + + worksheet.set_row(0, 30) + for row in range(1, len(df) + 1): + worksheet.set_row(row, 25) + + buffer.seek(0) + return io.BytesIO(buffer.getvalue()) + + result = await asyncio.to_thread(inner) + return StreamingResponse(result, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") + +async def batchUpload(trans, file): + ALLOWED_EXTENSIONS = {"xlsx", "xls"} + if not file.filename.lower().endswith(tuple(ALLOWED_EXTENSIONS)): + raise HTTPException(400, "Only support .xlsx/.xls") + pass \ No newline at end of file diff --git a/backend/locales/en.json b/backend/locales/en.json index b7054866..2b12de82 100644 --- a/backend/locales/en.json +++ b/backend/locales/en.json @@ -20,7 +20,18 @@ "email": "Email", "password": "Password", "language_not_support": "The system does not support [{key}] language!", - "ws_miss": "Current user is not in the workspace [{ws}]!" + "ws_miss": "Current user is not in the workspace [{ws}]!", + "name": "Name", + "status": "User Status", + "origin": "User Source", + "workspace": "Workspace", + "role": "Role", + "platform_user_id": "External User Unique Identifier", + "administrator": "Administrator", + "ordinary_member": "Member", + "status_enabled": "Enabled", + "status_disabled": "Disabled", + "local_creation": "Local" }, "i18n_ws": { "title": "Workspace" diff --git a/backend/locales/ko-KR.json b/backend/locales/ko-KR.json index 786cc549..8e33d521 100644 --- a/backend/locales/ko-KR.json +++ b/backend/locales/ko-KR.json @@ -20,7 +20,18 @@ "email": "이메일", "password": "비밀번호", "language_not_support": "시스템이 [{key}] 언어를 지원하지 않습니다!", - "ws_miss": "현재 사용자가 [{ws}] 작업 공간에 속해 있지 않습니다!" + "ws_miss": "현재 사용자가 [{ws}] 작업 공간에 속해 있지 않습니다!", + "name": "성명", + "status": "사용자 상태", + "origin": "사용자 출처", + "workspace": "작업 공간", + "role": "역할", + "platform_user_id": "외부 사용자 고유 식별자", + "administrator": "관리자", + "ordinary_member": "일반 멤버", + "status_enabled": "활성화됨", + "status_disabled": "비활성화됨", + "local_creation": "로컬 생성" }, "i18n_ws": { "title": "작업 공간" diff --git a/backend/locales/zh-CN.json b/backend/locales/zh-CN.json index 9154c424..2a4474b0 100644 --- a/backend/locales/zh-CN.json +++ b/backend/locales/zh-CN.json @@ -20,7 +20,18 @@ "email": "邮箱", "password": "密码", "language_not_support": "系统不支持[{key}]语言!", - "ws_miss": "当前用户不在工作空间[{ws}]中!" + "ws_miss": "当前用户不在工作空间[{ws}]中!", + "name": "姓名", + "status": "用户状态", + "origin": "用户来源", + "workspace": "工作空间", + "role": "角色", + "platform_user_id": "外部用户唯一标识", + "administrator": "管理员", + "ordinary_member": "普通成员", + "status_enabled": "已启用", + "status_disabled": "已禁用", + "local_creation": "本地创建" }, "i18n_ws": { "title": "工作空间" diff --git a/frontend/src/api/user.ts b/frontend/src/api/user.ts index 7722ceb2..3a16e25c 100644 --- a/frontend/src/api/user.ts +++ b/frontend/src/api/user.ts @@ -1,7 +1,7 @@ import { request } from '@/utils/request' export const userImportApi = { - downExcelTemplateApi: () => request.post('/user/excelTemplate', {}, { responseType: 'blob' }), + downExcelTemplateApi: () => request.get('/user/template', { responseType: 'blob' }), importUserApi: (data: any) => request.post('/user/batchImport', data, { headers: { diff --git a/frontend/src/views/system/user/User.vue b/frontend/src/views/system/user/User.vue index 2b74a329..04b743bb 100644 --- a/frontend/src/views/system/user/User.vue +++ b/frontend/src/views/system/user/User.vue @@ -23,12 +23,12 @@ {{ $t('user.filter') }} - + {{ $t('user.filter') }} - + - + { }) } -const handleUserImport = () => { +/* const handleUserImport = () => { userImportRef.value.showDialog() -} +} */ const handleConfirmPassword = () => { passwordRef.value.validate((val: any) => { diff --git a/frontend/src/views/system/user/UserImport.vue b/frontend/src/views/system/user/UserImport.vue index c79bfcbe..96b4cbb0 100644 --- a/frontend/src/views/system/user/UserImport.vue +++ b/frontend/src/views/system/user/UserImport.vue @@ -152,7 +152,7 @@ const sure = () => { .importUserApi(param) .then((res) => { closeLoading() - const data = res.data + const data = res errorFileKey.value = data.dataKey closeDialog() showTips(data.successCount, data.errorCount) @@ -167,12 +167,13 @@ const downErrorExcel = () => { userImportApi .downErrorRecordApi(errorFileKey.value) .then((res) => { - const blobData = res.data - const blob = new Blob([blobData], { type: 'application/vnd.ms-excel' }) + const blob = new Blob([res], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }) const link = document.createElement('a') link.style.display = 'none' link.href = URL.createObjectURL(blob) - link.download = 'error.xlsx' // 下载的文件名 + link.download = 'error.xlsx' document.body.appendChild(link) link.click() document.body.removeChild(link) @@ -185,20 +186,16 @@ const downErrorExcel = () => { } } const showTips = (successCount: any, errorCount: any) => { - let title = !errorCount - ? t('user.data_import_successful') - : successCount - ? t('user.data_import_failed') - : t('user.data_import_failed_de') + let title = successCount ? t('user.data_import_completed') : t('user.data_import_failed') const childrenDomList = [ h('strong', null, title), h('br', {}, {}), - h('span', null, t('user.imported_1_data', { msg: successCount })), + h('span', null, t('user.imported_100_data', { msg: successCount })), ] if (errorCount) { - const errorCountDom = h('span', null, t('user.import_1_data', { msg: errorCount })) + const errorCountDom = h('span', null, t('user.failed_100_can', { msg: errorCount })) const errorDom = h('div', { class: 'error-record-tip flex-align-center' }, [ - h('span', null, t('user.can')), + /* h('span', null, t('user.can')), */ h( ElButton, { @@ -226,20 +223,20 @@ const showTips = (successCount: any, errorCount: any) => { confirmButtonText: t('user.continue_importing'), }) .then(() => { - clearErrorRecord() + // clearErrorRecord() showDialog() emits('refresh-grid') }) .catch(() => { - clearErrorRecord() + // clearErrorRecord() toGrid() }) } -const clearErrorRecord = () => { +/* const clearErrorRecord = () => { if (errorFileKey.value) { userImportApi.clearErrorApi(errorFileKey.value) } -} +} */ const rules = { file: [