From 6ffcf4dca74f87005e1b0368cce1f664e025a313 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 11 Dec 2024 08:47:14 +0800 Subject: [PATCH 01/34] =?UTF-8?q?feat:=20=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=94=AF=E6=8C=81=E5=88=86=E6=A0=8F=E6=8B=96=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-frontend/package.json | 1 + .../src/assets/styles/ruoyi.scss | 7 +- .../src/views/system/user/index.vue | 1029 ++++++++++------- 3 files changed, 644 insertions(+), 393 deletions(-) diff --git a/ruoyi-fastapi-frontend/package.json b/ruoyi-fastapi-frontend/package.json index 996c73b..1f63a99 100644 --- a/ruoyi-fastapi-frontend/package.json +++ b/ruoyi-fastapi-frontend/package.json @@ -31,6 +31,7 @@ "jsencrypt": "3.3.2", "nprogress": "0.2.0", "pinia": "2.1.7", + "splitpanes": "3.1.5", "vue": "3.4.15", "vue-cropper": "1.1.1", "vue-router": "4.4.0" diff --git a/ruoyi-fastapi-frontend/src/assets/styles/ruoyi.scss b/ruoyi-fastapi-frontend/src/assets/styles/ruoyi.scss index 155fdb7..68cbf3c 100644 --- a/ruoyi-fastapi-frontend/src/assets/styles/ruoyi.scss +++ b/ruoyi-fastapi-frontend/src/assets/styles/ruoyi.scss @@ -103,7 +103,7 @@ /** 表格布局 **/ .pagination-container { position: relative; - height: 25px; + height: 32px; margin-bottom: 10px; margin-top: 15px; padding: 10px 20px !important; @@ -279,3 +279,8 @@ .top-right-btn { margin-left: auto; } + +/* 分割面板样式 */ +.splitpanes.default-theme .splitpanes__pane { + background-color: #fff!important; +} diff --git a/ruoyi-fastapi-frontend/src/views/system/user/index.vue b/ruoyi-fastapi-frontend/src/views/system/user/index.vue index 2bf486b..f539004 100644 --- a/ruoyi-fastapi-frontend/src/views/system/user/index.vue +++ b/ruoyi-fastapi-frontend/src/views/system/user/index.vue @@ -1,341 +1,525 @@ \ No newline at end of file diff --git a/ruoyi-fastapi-frontend/src/layout/components/Navbar.vue b/ruoyi-fastapi-frontend/src/layout/components/Navbar.vue index 05e15af..4ec7cad 100644 --- a/ruoyi-fastapi-frontend/src/layout/components/Navbar.vue +++ b/ruoyi-fastapi-frontend/src/layout/components/Navbar.vue @@ -18,6 +18,13 @@ + +
+ + +
+
+ @@ -98,6 +105,10 @@ const emits = defineEmits(['setLayout']) function setLayout() { emits('setLayout'); } + +function toggleTheme() { + settingsStore.toggleTheme() +} diff --git a/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue b/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue index 8212fec..0b826d6 100644 --- a/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue +++ b/ruoyi-fastapi-frontend/src/layout/components/TagsView/index.vue @@ -237,13 +237,13 @@ function handleScroll() { } -` +} + +function buildFormTemplate(conf, child, type) { + let labelPosition = '' + if (conf.labelPosition !== 'right') { + labelPosition = `label-position="${conf.labelPosition}"` + } + const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : '' + let str = ` + ${child} + ${buildFromBtns(conf, type)} + ` + if (someSpanIsNot24) { + str = ` + ${str} + ` + } + return str +} + +function buildFromBtns(conf, type) { + let str = '' + if (conf.formBtns && type === 'file') { + str = ` + 提交 + 重置 + ` + if (someSpanIsNot24) { + str = ` + ${str} + ` + } + } + return str +} + +// span不为24的用el-col包裹 +function colWrapper(element, str) { + if (someSpanIsNot24 || element.span !== 24) { + return ` + ${str} + ` + } + return str +} + +const layouts = { + colFormItem(element) { + let labelWidth = '' + if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) { + labelWidth = `label-width="${element.labelWidth}px"` + } + const required = !trigger[element.tag] && element.required ? 'required' : '' + const tagDom = tags[element.tag] ? tags[element.tag](element) : null + let str = ` + ${tagDom} + ` + str = colWrapper(element, str) + return str + }, + rowFormItem(element) { + const type = element.type === 'default' ? '' : `type="${element.type}"` + const justify = element.type === 'default' ? '' : `justify="${element.justify}"` + const align = element.type === 'default' ? '' : `align="${element.align}"` + const gutter = element.gutter ? `gutter="${element.gutter}"` : '' + const children = element.children.map(el => layouts[el.layout](el)) + let str = ` + ${children.join('\n')} + ` + str = colWrapper(element, str) + return str + } +} + +const tags = { + 'el-button': el => { + const { + tag, disabled + } = attrBuilder(el) + const type = el.type ? `type="${el.type}"` : '' + const icon = el.icon ? `icon="${el.icon}"` : '' + const size = el.size ? `size="${el.size}"` : '' + let child = buildElButtonChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${type} ${icon} ${size} ${disabled}>${child}` + }, + 'el-input': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : '' + const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : '' + const readonly = el.readonly ? 'readonly' : '' + const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : '' + const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : '' + const showPassword = el['show-password'] ? 'show-password' : '' + const type = el.type ? `type="${el.type}"` : '' + const autosize = el.autosize && el.autosize.minRows + ? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"` + : '' + let child = buildElInputChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}` + }, + 'el-input-number': el => { + const { disabled, vModel, placeholder } = attrBuilder(el) + const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : '' + const min = el.min ? `:min='${el.min}'` : '' + const max = el.max ? `:max='${el.max}'` : '' + const step = el.step ? `:step='${el.step}'` : '' + const stepStrictly = el['step-strictly'] ? 'step-strictly' : '' + const precision = el.precision ? `:precision='${el.precision}'` : '' + + return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}>` + }, + 'el-select': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const filterable = el.filterable ? 'filterable' : '' + const multiple = el.multiple ? 'multiple' : '' + let child = buildElSelectChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}` + }, + 'el-radio-group': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + let child = buildElRadioGroupChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${size} ${disabled}>${child}` + }, + 'el-checkbox-group': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + const min = el.min ? `:min="${el.min}"` : '' + const max = el.max ? `:max="${el.max}"` : '' + let child = buildElCheckboxGroupChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}` + }, + 'el-switch': el => { + const { disabled, vModel } = attrBuilder(el) + const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : '' + const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : '' + const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : '' + const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : '' + const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : '' + const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : '' + + return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}>` + }, + 'el-cascader': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const options = el.options ? `:options="${el.vModel}Options"` : '' + const props = el.props ? `:props="${el.vModel}Props"` : '' + const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"' + const filterable = el.filterable ? 'filterable' : '' + const separator = el.separator === '/' ? '' : `separator="${el.separator}"` + + return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}>` + }, + 'el-slider': el => { + const { disabled, vModel } = attrBuilder(el) + const min = el.min ? `:min='${el.min}'` : '' + const max = el.max ? `:max='${el.max}'` : '' + const step = el.step ? `:step='${el.step}'` : '' + const range = el.range ? 'range' : '' + const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : '' + + return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}>` + }, + 'el-time-picker': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : '' + const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : '' + const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : '' + const isRange = el['is-range'] ? 'is-range' : '' + const format = el.format ? `format="${el.format}"` : '' + const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : '' + const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : '' + + return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}>` + }, + 'el-date-picker': el => { + const { + disabled, vModel, clearable, placeholder, width + } = attrBuilder(el) + const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : '' + const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : '' + const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : '' + const format = el.format ? `format="${el.format}"` : '' + const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : '' + const type = el.type === 'date' ? '' : `type="${el.type}"` + const readonly = el.readonly ? 'readonly' : '' + + return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}>` + }, + 'el-rate': el => { + const { disabled, vModel } = attrBuilder(el) + const max = el.max ? `:max='${el.max}'` : '' + const allowHalf = el['allow-half'] ? 'allow-half' : '' + const showText = el['show-text'] ? 'show-text' : '' + const showScore = el['show-score'] ? 'show-score' : '' + + return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}>` + }, + 'el-color-picker': el => { + const { disabled, vModel } = attrBuilder(el) + const size = `size="${el.size}"` + const showAlpha = el['show-alpha'] ? 'show-alpha' : '' + const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : '' + + return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}>` + }, + 'el-upload': el => { + const disabled = el.disabled ? ':disabled=\'true\'' : '' + const action = el.action ? `:action="${el.vModel}Action"` : '' + const multiple = el.multiple ? 'multiple' : '' + const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : '' + const accept = el.accept ? `accept="${el.accept}"` : '' + const name = el.name !== 'file' ? `name="${el.name}"` : '' + const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : '' + const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"` + const fileList = `:file-list="${el.vModel}fileList"` + const ref = `ref="${el.vModel}"` + let child = buildElUploadChild(el) + + if (child) child = `\n${child}\n` // 换行 + return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}` + } +} + +function attrBuilder(el) { + return { + vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`, + clearable: el.clearable ? 'clearable' : '', + placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '', + width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '', + disabled: el.disabled ? ':disabled=\'true\'' : '' + } +} + +// el-buttin 子级 +function buildElButtonChild(conf) { + const children = [] + if (conf.default) { + children.push(conf.default) + } + return children.join('\n') +} + +// el-input innerHTML +function buildElInputChild(conf) { + const children = [] + if (conf.prepend) { + children.push(``) + } + if (conf.append) { + children.push(``) + } + return children.join('\n') +} + +function buildElSelectChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + children.push(``) + } + return children.join('\n') +} + +function buildElRadioGroupChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio' + const border = conf.border ? 'border' : '' + children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}`) + } + return children.join('\n') +} + +function buildElCheckboxGroupChild(conf) { + const children = [] + if (conf.options && conf.options.length) { + const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox' + const border = conf.border ? 'border' : '' + children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}`) + } + return children.join('\n') +} + +function buildElUploadChild(conf) { + const list = [] + if (conf['list-type'] === 'picture-card') list.push('') + else list.push(`${conf.buttonText}`) + if (conf.showTip) list.push(`
只能上传不超过 ${conf.fileSize}${conf.sizeUnit} 的${conf.accept}文件
`) + return list.join('\n') +} + +export function makeUpHtml(conf, type) { + const htmlList = [] + confGlobal = conf + someSpanIsNot24 = conf.fields.some(item => item.span !== 24) + conf.fields.forEach(el => { + htmlList.push(layouts[el.layout](el)) + }) + const htmlStr = htmlList.join('\n') + + let temp = buildFormTemplate(conf, htmlStr, type) + if (type === 'dialog') { + temp = dialogWrapper(temp) + } + confGlobal = null + return temp +} diff --git a/ruoyi-fastapi-frontend/src/utils/generator/icon.json b/ruoyi-fastapi-frontend/src/utils/generator/icon.json new file mode 100755 index 0000000..2d9999a --- /dev/null +++ b/ruoyi-fastapi-frontend/src/utils/generator/icon.json @@ -0,0 +1 @@ +["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"] \ No newline at end of file diff --git a/ruoyi-fastapi-frontend/src/utils/generator/js.js b/ruoyi-fastapi-frontend/src/utils/generator/js.js new file mode 100755 index 0000000..dc38bfe --- /dev/null +++ b/ruoyi-fastapi-frontend/src/utils/generator/js.js @@ -0,0 +1,370 @@ +import { titleCase } from '@/utils/index' +import { trigger } from './config' +// 文件大小设置 +const units = { + KB: '1024', + MB: '1024 / 1024', + GB: '1024 / 1024 / 1024', +} +/** + * @name: 生成js需要的数据 + * @description: 生成js需要的数据 + * @param {*} conf + * @param {*} type 弹窗或表单 + * @return {*} + */ +export function makeUpJs(conf, type) { + conf = JSON.parse(JSON.stringify(conf)) + const dataList = [] + const ruleList = [] + const optionsList = [] + const propsList = [] + const methodList = [] + const uploadVarList = [] + + conf.fields.forEach((el) => { + buildAttributes( + el, + dataList, + ruleList, + optionsList, + methodList, + propsList, + uploadVarList + ) + }) + + const script = buildexport( + conf, + type, + dataList.join('\n'), + ruleList.join('\n'), + optionsList.join('\n'), + uploadVarList.join('\n'), + propsList.join('\n'), + methodList.join('\n') + ) + + return script +} +/** + * @name: 生成参数 + * @description: 生成参数,包括表单数据表单验证数据,多选选项数据,上传数据等 + * @return {*} + */ +function buildAttributes( + el, + dataList, + ruleList, + optionsList, + methodList, + propsList, + uploadVarList +){ + buildData(el, dataList) + buildRules(el, ruleList) + + if (el.options && el.options.length) { + buildOptions(el, optionsList) + if (el.dataType === 'dynamic') { + const model = `${el.vModel}Options` + const options = titleCase(model) + buildOptionMethod(`get${options}`, model, methodList) + } + } + + if (el.props && el.props.props) { + buildProps(el, propsList) + } + + if (el.action && el.tag === 'el-upload') { + uploadVarList.push( + ` + // 上传请求路径 + const ${el.vModel}Action = ref('${el.action}') + // 上传文件列表 + const ${el.vModel}fileList = ref([])` + ) + methodList.push(buildBeforeUpload(el)) + if (!el['auto-upload']) { + methodList.push(buildSubmitUpload(el)) + } + } + + if (el.children) { + el.children.forEach((el2) => { + buildAttributes( + el2, + dataList, + ruleList, + optionsList, + methodList, + propsList, + uploadVarList + ) + }) + } +} +/** + * @name: 生成表单数据formData + * @description: 生成表单数据formData + * @param {*} conf + * @param {*} dataList 数据列表 + * @return {*} + */ +function buildData(conf, dataList) { + if (conf.vModel === undefined) return + let defaultValue + if (typeof conf.defaultValue === 'string' && !conf.multiple) { + defaultValue = `'${conf.defaultValue}'` + } else { + defaultValue = `${JSON.stringify(conf.defaultValue)}` + } + dataList.push(`${conf.vModel}: ${defaultValue},`) +} +/** + * @name: 生成表单验证数据rule + * @description: 生成表单验证数据rule + * @param {*} conf + * @param {*} ruleList 验证数据列表 + * @return {*} + */ +function buildRules(conf, ruleList) { + if (conf.vModel === undefined) return + const rules = [] + if (trigger[conf.tag]) { + if (conf.required) { + const type = Array.isArray(conf.defaultValue) ? "type: 'array'," : '' + let message = Array.isArray(conf.defaultValue) + ? `请至少选择一个${conf.vModel}` + : conf.placeholder + if (message === undefined) message = `${conf.label}不能为空` + rules.push( + `{ required: true, ${type} message: '${message}', trigger: '${ + trigger[conf.tag] + }' }` + ) + } + if (conf.regList && Array.isArray(conf.regList)) { + conf.regList.forEach((item) => { + if (item.pattern) { + rules.push( + `{ pattern: new RegExp(${item.pattern}), message: '${ + item.message + }', trigger: '${trigger[conf.tag]}' }` + ) + } + }) + } + ruleList.push(`${conf.vModel}: [${rules.join(',')}],`) + } +} +/** + * @name: 生成选项数据 + * @description: 生成选项数据,单选多选下拉等 + * @param {*} conf + * @param {*} optionsList 选项数据列表 + * @return {*} + */ +function buildOptions(conf, optionsList) { + if (conf.vModel === undefined) return + if (conf.dataType === 'dynamic') { + conf.options = [] + } + const str = `const ${conf.vModel}Options = ref(${JSON.stringify(conf.options)})` + optionsList.push(str) +} +/** + * @name: 生成方法 + * @description: 生成方法 + * @param {*} methodName 方法名 + * @param {*} model + * @param {*} methodList 方法列表 + * @return {*} + */ +function buildOptionMethod(methodName, model, methodList) { + const str = `function ${methodName}() { + // TODO 发起请求获取数据 + ${model}.value + }` + methodList.push(str) +} +/** + * @name: 生成表单组件需要的props设置 + * @description: 生成表单组件需要的props设置,如;级联组件 + * @param {*} conf + * @param {*} propsList + * @return {*} + */ +function buildProps(conf, propsList) { + if (conf.dataType === 'dynamic') { + conf.valueKey !== 'value' && (conf.props.props.value = conf.valueKey) + conf.labelKey !== 'label' && (conf.props.props.label = conf.labelKey) + conf.childrenKey !== 'children' && + (conf.props.props.children = conf.childrenKey) + } + const str = ` + // props设置 + const ${conf.vModel}Props = ref(${JSON.stringify(conf.props.props)})` + propsList.push(str) +} +/** + * @name: 生成上传组件的相关内容 + * @description: 生成上传组件的相关内容 + * @param {*} conf + * @return {*} + */ +function buildBeforeUpload(conf) { + const unitNum = units[conf.sizeUnit] + let rightSizeCode = '' + let acceptCode = '' + const returnList = [] + if (conf.fileSize) { + rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize} + if(!isRightSize){ + proxy.$modal.msgError('文件大小超过 ${conf.fileSize}${conf.sizeUnit}') + }` + returnList.push('isRightSize') + } + if (conf.accept) { + acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type) + if(!isAccept){ + proxy.$modal.msgError('应该选择${conf.accept}类型的文件') + }` + returnList.push('isAccept') + } + const str = ` + /** + * @name: 上传之前的文件判断 + * @description: 上传之前的文件判断,判断文件大小文件类型等 + * @param {*} file + * @return {*} + */ + function ${conf.vModel}BeforeUpload(file) { + ${rightSizeCode} + ${acceptCode} + return ${returnList.join('&&')} + }` + return returnList.length ? str : '' +} +/** + * @name: 生成提交表单方法 + * @description: 生成提交表单方法 + * @param {Object} conf vModel 表单ref + * @return {*} + */ +function buildSubmitUpload(conf) { + const str = `function submitUpload() { + this.$refs['${conf.vModel}'].submit() + }` + return str +} +/** + * @name: 组装js代码 + * @description: 组装js代码方法 + * @return {*} + */ +function buildexport( + conf, + type, + data, + rules, + selectOptions, + uploadVar, + props, + methods +) { + let str = ` + const { proxy } = getCurrentInstance() + const ${conf.formRef} = ref() + const data = reactive({ + ${conf.formModel}: { + ${data} + }, + ${conf.formRules}: { + ${rules} + } + }) + + const {${conf.formModel}, ${conf.formRules}} = toRefs(data) + + ${selectOptions} + + ${uploadVar} + + ${props} + + ${methods} + ` + + if(type === 'dialog') { + str += ` + // 弹窗设置 + const dialogVisible = defineModel() + // 弹窗确认回调 + const emit = defineEmits(['confirm']) + /** + * @name: 弹窗打开后执行 + * @description: 弹窗打开后执行方法 + * @return {*} + */ + function onOpen(){ + + } + /** + * @name: 弹窗关闭时执行 + * @description: 弹窗关闭方法,重置表单 + * @return {*} + */ + function onClose(){ + ${conf.formRef}.value.resetFields() + } + /** + * @name: 弹窗取消 + * @description: 弹窗取消方法 + * @return {*} + */ + function close(){ + dialogVisible.value = false + } + /** + * @name: 弹窗表单提交 + * @description: 弹窗表单提交方法 + * @return {*} + */ + function handelConfirm(){ + ${conf.formRef}.value.validate((valid) => { + if (!valid) return + // TODO 提交表单 + + close() + // 回调父级组件 + emit('confirm') + }) + } + ` + } else { + str += ` + /** + * @name: 表单提交 + * @description: 表单提交方法 + * @return {*} + */ + function submitForm() { + ${conf.formRef}.value.validate((valid) => { + if (!valid) return + // TODO 提交表单 + }) + } + /** + * @name: 表单重置 + * @description: 表单重置方法 + * @return {*} + */ + function resetForm() { + ${conf.formRef}.value.resetFields() + } + ` + } + return str +} diff --git a/ruoyi-fastapi-frontend/src/utils/generator/render.js b/ruoyi-fastapi-frontend/src/utils/generator/render.js new file mode 100755 index 0000000..d6d4414 --- /dev/null +++ b/ruoyi-fastapi-frontend/src/utils/generator/render.js @@ -0,0 +1,156 @@ +import { defineComponent, h } from 'vue' +import { makeMap } from '@/utils/index' + +const isAttr = makeMap( + 'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' + + 'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' + + 'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' + + 'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' + + 'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' + + 'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' + + 'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' + + 'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' + + 'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' + + 'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' + + 'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' + + 'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' + + 'target,title,type,usemap,value,width,wrap' + 'prefix-icon' +) +const isNotProps = makeMap( + 'layout,prepend,regList,tag,document,changeTag,defaultValue' +) + +function useVModel(props, emit) { + return { + modelValue: props.defaultValue, + 'onUpdate:modelValue': (val) => emit('update:modelValue', val), + } +} +const componentChild = { + 'el-button': { + default(h, conf, key) { + return conf[key] + }, + }, + 'el-select': { + options(h, conf, key) { + return conf.options.map(item => h(resolveComponent('el-option'), { + label: item.label, + value: item.value, + })) + } + }, + 'el-radio-group': { + options(h, conf, key) { + return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), { + label: item.value, + }, () => item.label)) : conf.options.map(item => h(resolveComponent('el-radio'), { + label: item.value, + border: conf.border, + }, () => item.label)) + } + }, + 'el-checkbox-group': { + options(h, conf, key) { + return conf.optionType === 'button' ? conf.options.map(item => h(resolveComponent('el-checkbox-button'), { + label: item.value, + }, () => item.label)) : conf.options.map(item => h(resolveComponent('el-checkbox'), { + label: item.value, + border: conf.border, + }, () => item.label)) + } + }, + 'el-upload': { + 'list-type': (h, conf, key) => { + const option = {} + // if (conf.showTip) { + // tip = h('div', { + // class: "el-upload__tip" + // }, () => '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件') + // } + if (conf['list-type'] === 'picture-card') { + return h(resolveComponent('el-icon'), option, () => h(resolveComponent('Plus'))) + } else { + // option.size = "small" + option.type = "primary" + option.icon = "Upload" + return h(resolveComponent('el-button'), option, () => conf.buttonText) + } + }, + + } +} +const componentSlot = { + 'el-upload': { + 'tip': (h, conf, key) => { + if (conf.showTip) { + return () => h('div', { + class: "el-upload__tip" + }, '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件') + } + }, + } +} +export default defineComponent({ + + // 使用 render 函数 + render() { + const dataObject = { + attrs: {}, + props: {}, + on: {}, + style: {} + } + const confClone = JSON.parse(JSON.stringify(this.conf)) + const children = [] + const slot = {} + const childObjs = componentChild[confClone.tag] + if (childObjs) { + Object.keys(childObjs).forEach(key => { + const childFunc = childObjs[key] + if (confClone[key]) { + children.push(childFunc(h, confClone, key)) + } + }) + } + const slotObjs = componentSlot[confClone.tag] + if (slotObjs) { + Object.keys(slotObjs).forEach(key => { + const childFunc = slotObjs[key] + if (confClone[key]) { + slot[key] = childFunc(h, confClone, key) + } + }) + } + Object.keys(confClone).forEach(key => { + const val = confClone[key] + if (dataObject[key]) { + dataObject[key] = val + } else if (isAttr(key)) { + dataObject.attrs[key] = val + } else if (!isNotProps(key)) { + dataObject.props[key] = val + } + }) + if(children.length > 0){ + slot.default = () => children + } + + return h(resolveComponent(this.conf.tag), + { + modelValue: this.$attrs.modelValue, + ...dataObject.props, + ...dataObject.attrs, + style: { + ...dataObject.style + }, + } + , slot ?? null) + }, + props: { + conf: { + type: Object, + required: true, + }, + } +}) \ No newline at end of file diff --git a/ruoyi-fastapi-frontend/src/views/tool/build/CodeTypeDialog.vue b/ruoyi-fastapi-frontend/src/views/tool/build/CodeTypeDialog.vue new file mode 100755 index 0000000..de0beb7 --- /dev/null +++ b/ruoyi-fastapi-frontend/src/views/tool/build/CodeTypeDialog.vue @@ -0,0 +1,71 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-frontend/src/views/tool/build/DraggableItem.vue b/ruoyi-fastapi-frontend/src/views/tool/build/DraggableItem.vue new file mode 100755 index 0000000..927aafb --- /dev/null +++ b/ruoyi-fastapi-frontend/src/views/tool/build/DraggableItem.vue @@ -0,0 +1,68 @@ + + \ No newline at end of file diff --git a/ruoyi-fastapi-frontend/src/views/tool/build/IconsDialog.vue b/ruoyi-fastapi-frontend/src/views/tool/build/IconsDialog.vue new file mode 100755 index 0000000..98d9c13 --- /dev/null +++ b/ruoyi-fastapi-frontend/src/views/tool/build/IconsDialog.vue @@ -0,0 +1,115 @@ + + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/build/RightPanel.vue b/ruoyi-fastapi-frontend/src/views/tool/build/RightPanel.vue new file mode 100755 index 0000000..9729da3 --- /dev/null +++ b/ruoyi-fastapi-frontend/src/views/tool/build/RightPanel.vue @@ -0,0 +1,918 @@ + + + + + \ No newline at end of file diff --git a/ruoyi-fastapi-frontend/src/views/tool/build/TreeNodeDialog.vue b/ruoyi-fastapi-frontend/src/views/tool/build/TreeNodeDialog.vue new file mode 100755 index 0000000..372d3af --- /dev/null +++ b/ruoyi-fastapi-frontend/src/views/tool/build/TreeNodeDialog.vue @@ -0,0 +1,93 @@ + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/build/index.vue b/ruoyi-fastapi-frontend/src/views/tool/build/index.vue old mode 100644 new mode 100755 index c3543a9..60159b0 --- a/ruoyi-fastapi-frontend/src/views/tool/build/index.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/build/index.vue @@ -1,3 +1,653 @@ - \ No newline at end of file + + + + + -- Gitee From 1d408e6a09d993dff9184bd95cef06c2403acead Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Fri, 20 Dec 2024 10:26:00 +0800 Subject: [PATCH 08/34] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=AF=BC?= =?UTF-8?q?=E5=87=BA=E6=96=B9=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/service/config_service.py | 12 +- .../module_admin/service/dict_service.py | 21 +--- .../module_admin/service/job_log_service.py | 10 +- .../module_admin/service/job_service.py | 11 +- .../module_admin/service/log_service.py | 20 +--- .../module_admin/service/post_service.py | 12 +- .../module_admin/service/role_service.py | 12 +- .../module_admin/service/user_service.py | 14 +-- ruoyi-fastapi-backend/utils/excel_util.py | 104 ++++++++++++++++++ 9 files changed, 139 insertions(+), 77 deletions(-) create mode 100644 ruoyi-fastapi-backend/utils/excel_util.py diff --git a/ruoyi-fastapi-backend/module_admin/service/config_service.py b/ruoyi-fastapi-backend/module_admin/service/config_service.py index 1d8ec76..312006d 100644 --- a/ruoyi-fastapi-backend/module_admin/service/config_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/config_service.py @@ -7,7 +7,8 @@ from exceptions.exception import ServiceException from module_admin.dao.config_dao import ConfigDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.config_vo import ConfigModel, ConfigPageQueryModel, DeleteConfigModel -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil class ConfigService: @@ -207,17 +208,12 @@ class ConfigService: 'remark': '备注', } - data = config_list - - for item in data: + for item in config_list: if item.get('configType') == 'Y': item['configType'] = '是' else: item['configType'] = '否' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(config_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/dict_service.py b/ruoyi-fastapi-backend/module_admin/service/dict_service.py index 5354083..bfe6489 100644 --- a/ruoyi-fastapi-backend/module_admin/service/dict_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/dict_service.py @@ -15,7 +15,8 @@ from module_admin.entity.vo.dict_vo import ( DictTypeModel, DictTypePageQueryModel, ) -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil class DictTypeService: @@ -192,17 +193,12 @@ class DictTypeService: 'remark': '备注', } - data = dict_type_list - - for item in data: + for item in dict_type_list: if item.get('status') == '0': item['status'] = '正常' else: item['status'] = '停用' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(dict_type_list, mapping_dict) return binary_data @@ -448,9 +444,7 @@ class DictDataService: 'remark': '备注', } - data = dict_data_list - - for item in data: + for item in dict_data_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -459,9 +453,6 @@ class DictDataService: item['isDefault'] = '是' else: item['isDefault'] = '否' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(dict_data_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/job_log_service.py b/ruoyi-fastapi-backend/module_admin/service/job_log_service.py index f4c9f37..596abe7 100644 --- a/ruoyi-fastapi-backend/module_admin/service/job_log_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/job_log_service.py @@ -6,7 +6,7 @@ from module_admin.dao.job_log_dao import JobLogDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.job_vo import DeleteJobLogModel, JobLogModel, JobLogPageQueryModel from module_admin.service.dict_service import DictDataService -from utils.common_util import export_list2excel +from utils.excel_util import ExcelUtil class JobLogService: @@ -115,7 +115,6 @@ class JobLogService: 'createTime': '创建时间', } - data = job_log_list job_group_list = await DictDataService.query_dict_data_list_from_cache_services( request.app.state.redis, dict_type='sys_job_group' ) @@ -129,7 +128,7 @@ class JobLogService: ] job_executor_option_dict = {item.get('value'): item for item in job_executor_option} - for item in data: + for item in job_log_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -138,9 +137,6 @@ class JobLogService: item['jobGroup'] = job_group_option_dict.get(str(item.get('jobGroup'))).get('label') if str(item.get('jobExecutor')) in job_executor_option_dict.keys(): item['jobExecutor'] = job_executor_option_dict.get(str(item.get('jobExecutor'))).get('label') - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(job_log_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/job_service.py b/ruoyi-fastapi-backend/module_admin/service/job_service.py index 2d06b6f..55263c1 100644 --- a/ruoyi-fastapi-backend/module_admin/service/job_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/job_service.py @@ -8,8 +8,9 @@ from module_admin.dao.job_dao import JobDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.job_vo import DeleteJobModel, EditJobModel, JobModel, JobPageQueryModel from module_admin.service.dict_service import DictDataService -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil from utils.cron_util import CronUtil +from utils.excel_util import ExcelUtil from utils.string_util import StringUtil @@ -227,7 +228,6 @@ class JobService: 'remark': '备注', } - data = job_list job_group_list = await DictDataService.query_dict_data_list_from_cache_services( request.app.state.redis, dict_type='sys_job_group' ) @@ -241,7 +241,7 @@ class JobService: ] job_executor_option_dict = {item.get('value'): item for item in job_executor_option} - for item in data: + for item in job_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -260,9 +260,6 @@ class JobService: item['concurrent'] = '允许' else: item['concurrent'] = '禁止' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(job_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/log_service.py b/ruoyi-fastapi-backend/module_admin/service/log_service.py index 0c80a60..0983b1a 100644 --- a/ruoyi-fastapi-backend/module_admin/service/log_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/log_service.py @@ -14,7 +14,7 @@ from module_admin.entity.vo.log_vo import ( UnlockUser, ) from module_admin.service.dict_service import DictDataService -from utils.common_util import export_list2excel +from utils.excel_util import ExcelUtil class OperationLogService: @@ -122,7 +122,6 @@ class OperationLogService: 'costTime': '消耗时间(毫秒)', } - data = operation_log_list operation_type_list = await DictDataService.query_dict_data_list_from_cache_services( request.app.state.redis, dict_type='sys_oper_type' ) @@ -131,18 +130,14 @@ class OperationLogService: ] operation_type_option_dict = {item.get('value'): item for item in operation_type_option} - for item in data: + for item in operation_log_list: if item.get('status') == 0: item['status'] = '成功' else: item['status'] = '失败' if str(item.get('businessType')) in operation_type_option_dict.keys(): item['businessType'] = operation_type_option_dict.get(str(item.get('businessType'))).get('label') - - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(operation_log_list, mapping_dict) return binary_data @@ -253,16 +248,11 @@ class LoginLogService: 'loginTime': '登录日期', } - data = login_log_list - - for item in data: + for item in login_log_list: if item.get('status') == '0': item['status'] = '成功' else: item['status'] = '失败' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(login_log_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/post_service.py b/ruoyi-fastapi-backend/module_admin/service/post_service.py index 5215539..9338a9f 100644 --- a/ruoyi-fastapi-backend/module_admin/service/post_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/post_service.py @@ -5,7 +5,8 @@ from exceptions.exception import ServiceException from module_admin.dao.post_dao import PostDao from module_admin.entity.vo.common_vo import CrudResponseModel from module_admin.entity.vo.post_vo import DeletePostModel, PostModel, PostPageQueryModel -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil class PostService: @@ -172,16 +173,11 @@ class PostService: 'remark': '备注', } - data = post_list - - for item in data: + for item in post_list: if item.get('status') == '0': item['status'] = '正常' else: item['status'] = '停用' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(post_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/role_service.py b/ruoyi-fastapi-backend/module_admin/service/role_service.py index 24f9bee..4b633de 100644 --- a/ruoyi-fastapi-backend/module_admin/service/role_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/role_service.py @@ -15,7 +15,8 @@ from module_admin.entity.vo.role_vo import ( from module_admin.entity.vo.user_vo import UserInfoModel, UserRolePageQueryModel from module_admin.dao.role_dao import RoleDao from module_admin.dao.user_dao import UserDao -from utils.common_util import CamelCaseUtil, export_list2excel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil from utils.page_util import PageResponseModel @@ -295,17 +296,12 @@ class RoleService: 'remark': '备注', } - data = role_list - - for item in data: + for item in role_list: if item.get('status') == '0': item['status'] = '正常' else: item['status'] = '停用' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(role_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_admin/service/user_service.py b/ruoyi-fastapi-backend/module_admin/service/user_service.py index dc79430..c149b56 100644 --- a/ruoyi-fastapi-backend/module_admin/service/user_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/user_service.py @@ -31,7 +31,8 @@ from module_admin.service.config_service import ConfigService from module_admin.service.dept_service import DeptService from module_admin.service.post_service import PostService from module_admin.service.role_service import RoleService -from utils.common_util import CamelCaseUtil, export_list2excel, get_excel_template +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil from utils.page_util import PageResponseModel from utils.pwd_util import PwdUtil @@ -461,7 +462,7 @@ class UserService: header_list = ['部门编号', '登录名称', '用户名称', '用户邮箱', '手机号码', '用户性别', '帐号状态'] selector_header_list = ['用户性别', '帐号状态'] option_list = [{'用户性别': ['男', '女', '未知']}, {'帐号状态': ['正常', '停用']}] - binary_data = get_excel_template( + binary_data = ExcelUtil.get_excel_template( header_list=header_list, selector_header_list=selector_header_list, option_list=option_list ) @@ -492,9 +493,7 @@ class UserService: 'remark': '备注', } - data = user_list - - for item in data: + for item in user_list: if item.get('status') == '0': item['status'] = '正常' else: @@ -505,10 +504,7 @@ class UserService: item['sex'] = '女' else: item['sex'] = '未知' - new_data = [ - {mapping_dict.get(key): value for key, value in item.items() if mapping_dict.get(key)} for item in data - ] - binary_data = export_list2excel(new_data) + binary_data = ExcelUtil.export_list2excel(user_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/utils/excel_util.py b/ruoyi-fastapi-backend/utils/excel_util.py new file mode 100644 index 0000000..875a41d --- /dev/null +++ b/ruoyi-fastapi-backend/utils/excel_util.py @@ -0,0 +1,104 @@ +import io +import pandas as pd +from openpyxl import Workbook +from openpyxl.styles import Alignment, PatternFill +from openpyxl.utils import get_column_letter +from openpyxl.worksheet.datavalidation import DataValidation +from typing import Dict, List + + +class ExcelUtil: + """ + Excel操作类 + """ + + @classmethod + def __mapping_list(cls, list_data: List, mapping_dict: Dict): + """ + 工具方法:将list数据中的字段名映射为对应的中文字段名 + + :param list_data: 数据列表 + :param mapping_dict: 映射字典 + :return: 映射后的数据列表 + """ + mapping_data = [{mapping_dict.get(key): item.get(key) for key in mapping_dict} for item in list_data] + + return mapping_data + + @classmethod + def export_list2excel(cls, list_data: List, mapping_dict: Dict): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param list_data: 数据列表 + :param mapping_dict: 映射字典 + :return: list数据对应excel的二进制数据 + """ + mapping_data = cls.__mapping_list(list_data, mapping_dict) + df = pd.DataFrame(mapping_data) + binary_data = io.BytesIO() + df.to_excel(binary_data, index=False, engine='openpyxl') + binary_data = binary_data.getvalue() + + return binary_data + + @classmethod + def get_excel_template(cls, header_list: List, selector_header_list: List, option_list: List[Dict]): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param header_list: 表头数据列表 + :param selector_header_list: 需要设置为选择器格式的表头数据列表 + :param option_list: 选择器格式的表头预设的选项列表 + :return: 模板excel的二进制数据 + """ + # 创建Excel工作簿 + wb = Workbook() + # 选择默认的活动工作表 + ws = wb.active + + # 设置表头文字 + headers = header_list + + # 设置表头背景样式为灰色,前景色为白色 + header_fill = PatternFill(start_color='ababab', end_color='ababab', fill_type='solid') + + # 将表头写入第一行 + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num) + cell.value = header + cell.fill = header_fill + # 设置列宽度为16 + ws.column_dimensions[chr(64 + col_num)].width = 12 + # 设置水平居中对齐 + cell.alignment = Alignment(horizontal='center') + + # 设置选择器的预设选项 + options = option_list + + # 获取selector_header的字母索引 + for selector_header in selector_header_list: + column_selector_header_index = headers.index(selector_header) + 1 + + # 创建数据有效性规则 + header_option = [] + for option in options: + if option.get(selector_header): + header_option = option.get(selector_header) + dv = DataValidation(type='list', formula1=f'"{",".join(header_option)}"') + # 设置数据有效性规则的起始单元格和结束单元格 + dv.add( + f'{get_column_letter(column_selector_header_index)}2:{get_column_letter(column_selector_header_index)}1048576' + ) + # 添加数据有效性规则到工作表 + ws.add_data_validation(dv) + + # 保存Excel文件为字节类型的数据 + file = io.BytesIO() + wb.save(file) + file.seek(0) + + # 读取字节数据 + excel_data = file.getvalue() + + return excel_data -- Gitee From 691076bb14fa183fe4d256f05e9a35c1d5d93f70 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Thu, 26 Dec 2024 08:48:22 +0800 Subject: [PATCH 09/34] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=BD=93=E5=89=8D=E7=99=BB=E5=BD=95=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=A4=B1=E6=95=88=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/controller/user_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py index 4af1850..a54b939 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py @@ -135,7 +135,7 @@ async def delete_system_user( ): user_id_list = user_ids.split(',') if user_ids else [] if user_id_list: - if current_user.user.user_id in user_id_list: + if current_user.user.user_id in list(map(int, user_id_list)): logger.warning('当前登录用户不能删除') return ResponseUtil.failure(msg='当前登录用户不能删除') -- Gitee From a67c629fa6d897b1be550d4671819fb622218d24 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Thu, 26 Dec 2024 08:48:47 +0800 Subject: [PATCH 10/34] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E4=B8=AD=E6=93=8D=E4=BD=9C=E6=96=B9=E6=B3=95=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py index e038e0b..5ba35a1 100644 --- a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py @@ -51,7 +51,7 @@ class Log: # 获取项目根路径 project_root = os.getcwd() # 处理文件路径,去除项目根路径部分 - relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.') + relative_path = os.path.relpath(file_path, start=project_root)[0:-2].replace('\\', '.').replace('/', '.') # 获取当前被装饰函数所在路径 func_path = f'{relative_path}{func.__name__}()' # 获取上下文信息 -- Gitee From 3e05f1b8b3a2315778b7e317366ca0cf70e9ff7c Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Fri, 27 Dec 2024 15:02:53 +0800 Subject: [PATCH 11/34] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E4=BB=BB=E5=8A=A1=E7=9B=AE=E6=A0=87=E5=AD=97=E7=AC=A6?= =?UTF-8?q?=E4=B8=B2=E8=A7=84=E5=88=99=E6=A0=A1=E9=AA=8C=E4=B8=8D=E5=85=A8?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/utils/string_util.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ruoyi-fastapi-backend/utils/string_util.py b/ruoyi-fastapi-backend/utils/string_util.py index 0be9e65..962afdc 100644 --- a/ruoyi-fastapi-backend/utils/string_util.py +++ b/ruoyi-fastapi-backend/utils/string_util.py @@ -96,6 +96,5 @@ class StringUtil: :return: 查找结果 """ if search_str and compare_str_list: - for compare_str in compare_str_list: - return cls.startswith_case(search_str, compare_str) + return any([cls.startswith_case(search_str, compare_str) for compare_str in compare_str_list]) return False -- Gitee From 1cfd85f9de2bc1db129e17fe37e10e6eac5f7aa5 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 8 Jan 2025 15:32:56 +0800 Subject: [PATCH 12/34] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=8D=95=E6=AC=A1=E4=BB=BB=E5=8A=A1=E6=97=B6=E4=BC=9A?= =?UTF-8?q?=E8=A6=86=E7=9B=96=E5=B7=B2=E5=90=AF=E7=94=A8=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=9A=84=E9=97=AE=E9=A2=98=20#IBEKD2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/config/get_scheduler.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ruoyi-fastapi-backend/config/get_scheduler.py b/ruoyi-fastapi-backend/config/get_scheduler.py index 2c9457b..4473390 100644 --- a/ruoyi-fastapi-backend/config/get_scheduler.py +++ b/ruoyi-fastapi-backend/config/get_scheduler.py @@ -6,7 +6,9 @@ from apscheduler.jobstores.memory import MemoryJobStore from apscheduler.jobstores.redis import RedisJobStore from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore from apscheduler.schedulers.asyncio import AsyncIOScheduler +from apscheduler.triggers.combining import OrTrigger from apscheduler.triggers.cron import CronTrigger +from apscheduler.triggers.date import DateTrigger from asyncio import iscoroutinefunction from datetime import datetime, timedelta from sqlalchemy.engine import create_engine @@ -201,8 +203,7 @@ class SchedulerUtil: job_executor = 'default' scheduler.add_job( func=eval(job_info.invoke_target), - trigger='date', - run_date=datetime.now() + timedelta(seconds=1), + trigger=OrTrigger(triggers=[DateTrigger(), MyCronTrigger.from_crontab(job_info.cron_expression)]), args=job_info.job_args.split(',') if job_info.job_args else None, kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None, id=str(job_info.job_id), -- Gitee From 00011f84199e929d523c4583ab7ab15c8089d443 Mon Sep 17 00:00:00 2001 From: py1ren <942290627@qq.com> Date: Fri, 24 Jan 2025 01:34:33 +0000 Subject: [PATCH 13/34] =?UTF-8?q?!27=20feat:=20=E6=96=B0=E5=A2=9Etrace?= =?UTF-8?q?=E4=B8=AD=E9=97=B4=E4=BB=B6=E5=BC=BA=E5=8C=96=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E9=93=BE=E8=B7=AF=E8=BF=BD=E8=B8=AA=E5=92=8C=E5=93=8D=E5=BA=94?= =?UTF-8?q?=E5=A4=B4=20*=20refactor:=20trace=5Flog=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E4=B8=BAtrace=5Fmiddleware=20*=20refactor:=20?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=A4=84=E7=90=86=E5=99=A8=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E4=B8=BA=E7=B1=BB=E5=BC=8F=E5=86=99=E6=B3=95=20*=20perf:=20?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E6=97=A0=E7=94=A8=E6=96=87=E4=BB=B6=20*=20pe?= =?UTF-8?q?rf:=20=E4=BC=98=E5=8C=96trace=E4=B8=AD=E9=97=B4=E4=BB=B6?= =?UTF-8?q?=E9=83=A8=E5=88=86=E5=86=99=E6=B3=95=20*=20style:=20=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96=E4=BB=A3=E7=A0=81=20*=20Merge=20branch=20'ma?= =?UTF-8?q?ster'=20into=20develop=20*=20feature:=201.=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0traceId=E9=93=BE=E8=B7=AF=E8=BF=BD=E8=B8=AA?= =?UTF-8?q?=202.response-header=E9=BB=98=E8=AE=A4=E6=B7=BB=E5=8A=A0request?= =?UTF-8?q?-id=E4=B8=8EtraceId=E5=AF=B9=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/middlewares/handle.py | 3 + .../middlewares/trace_middleware/__init__.py | 17 ++++++ .../middlewares/trace_middleware/ctx.py | 23 +++++++ .../middlewares/trace_middleware/middle.py | 47 ++++++++++++++ .../middlewares/trace_middleware/span.py | 52 ++++++++++++++++ ruoyi-fastapi-backend/utils/log_util.py | 61 +++++++++++++++++-- 6 files changed, 197 insertions(+), 6 deletions(-) create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py create mode 100644 ruoyi-fastapi-backend/middlewares/trace_middleware/span.py diff --git a/ruoyi-fastapi-backend/middlewares/handle.py b/ruoyi-fastapi-backend/middlewares/handle.py index ea447d4..abb2d0d 100644 --- a/ruoyi-fastapi-backend/middlewares/handle.py +++ b/ruoyi-fastapi-backend/middlewares/handle.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from middlewares.cors_middleware import add_cors_middleware from middlewares.gzip_middleware import add_gzip_middleware +from middlewares.trace_middleware import add_trace_middleware def handle_middleware(app: FastAPI): @@ -11,3 +12,5 @@ def handle_middleware(app: FastAPI): add_cors_middleware(app) # 加载gzip压缩中间件 add_gzip_middleware(app) + # 加载trace中间件 + add_trace_middleware(app) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py new file mode 100644 index 0000000..76f8d85 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/__init__.py @@ -0,0 +1,17 @@ +from fastapi import FastAPI +from .ctx import TraceCtx +from .middle import TraceASGIMiddleware + +__all__ = ('TraceASGIMiddleware', 'TraceCtx') + +__version__ = '0.1.0' + + +def add_trace_middleware(app: FastAPI): + """ + 添加trace中间件 + + :param app: FastAPI对象 + :return: + """ + app.add_middleware(TraceASGIMiddleware) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py new file mode 100644 index 0000000..558a5c9 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/ctx.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: ctx.py +@time: 2025/1/17 16:57 +""" + +import contextvars +from uuid import uuid4 + +CTX_REQUEST_ID: contextvars.ContextVar[str] = contextvars.ContextVar('request-id', default='') + + +class TraceCtx: + @staticmethod + def set_id(): + _id = uuid4().hex + CTX_REQUEST_ID.set(_id) + return _id + + @staticmethod + def get_id(): + return CTX_REQUEST_ID.get() diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py new file mode 100644 index 0000000..a071692 --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/middle.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: middle.py +@time: 2025/1/17 16:57 +""" + +from functools import wraps +from starlette.types import ASGIApp, Message, Receive, Scope, Send +from .span import get_current_span, Span + + +class TraceASGIMiddleware: + """ + fastapi-example: + app = FastAPI() + app.add_middleware(TraceASGIMiddleware) + """ + + def __init__(self, app: ASGIApp) -> None: + self.app = app + + @staticmethod + async def my_receive(receive: Receive, span: Span): + await span.request_before() + + @wraps(receive) + async def my_receive(): + message = await receive() + await span.request_after(message) + return message + + return my_receive + + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: + if scope['type'] != 'http': + await self.app(scope, receive, send) + return + + async with get_current_span(scope) as span: + handle_outgoing_receive = await self.my_receive(receive, span) + + async def handle_outgoing_request(message: 'Message') -> None: + await span.response(message) + await send(message) + + await self.app(scope, handle_outgoing_receive, handle_outgoing_request) diff --git a/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py new file mode 100644 index 0000000..1e38eab --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/trace_middleware/span.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +""" +@author: peng +@file: span.py +@time: 2025/1/17 16:57 +""" + +from contextlib import asynccontextmanager +from starlette.types import Scope, Message +from .ctx import TraceCtx + + +class Span: + """ + 整个http生命周期: + request(before) --> request(after) --> response(before) --> response(after) + """ + + def __init__(self, scope: Scope): + self.scope = scope + + async def request_before(self): + """ + request_before: 处理header信息等, 如记录请求体信息 + """ + TraceCtx.set_id() + + async def request_after(self, message: Message): + """ + request_after: 处理请求bytes, 如记录请求参数 + + example: + message: {'type': 'http.request', 'body': b'{\r\n "name": "\xe8\x8b\x8f\xe8\x8b\x8f\xe8\x8b\x8f"\r\n}', 'more_body': False} + """ + return message + + async def response(self, message: Message): + """ + if message['type'] == "http.response.start": -----> request-before + pass + if message['type'] == "http.response.body": -----> request-after + message.get('body', b'') + pass + """ + if message['type'] == 'http.response.start': + message['headers'].append((b'request-id', TraceCtx.get_id().encode())) + return message + + +@asynccontextmanager +async def get_current_span(scope: Scope): + yield Span(scope) diff --git a/ruoyi-fastapi-backend/utils/log_util.py b/ruoyi-fastapi-backend/utils/log_util.py index e42f393..f953f55 100644 --- a/ruoyi-fastapi-backend/utils/log_util.py +++ b/ruoyi-fastapi-backend/utils/log_util.py @@ -1,11 +1,60 @@ import os +import sys import time -from loguru import logger +from loguru import logger as _logger +from typing import Dict +from middlewares.trace_middleware import TraceCtx -log_path = os.path.join(os.getcwd(), 'logs') -if not os.path.exists(log_path): - os.mkdir(log_path) -log_path_error = os.path.join(log_path, f'{time.strftime("%Y-%m-%d")}_error.log') +class LoggerInitializer: + def __init__(self): + self.log_path = os.path.join(os.getcwd(), 'logs') + self.__ensure_log_directory_exists() + self.log_path_error = os.path.join(self.log_path, f'{time.strftime("%Y-%m-%d")}_error.log') -logger.add(log_path_error, rotation='50MB', encoding='utf-8', enqueue=True, compression='zip') + def __ensure_log_directory_exists(self): + """ + 确保日志目录存在,如果不存在则创建 + """ + if not os.path.exists(self.log_path): + os.mkdir(self.log_path) + + @staticmethod + def __filter(log: Dict): + """ + 自定义日志过滤器,添加trace_id + """ + log['trace_id'] = TraceCtx.get_id() + return log + + def init_log(self): + """ + 初始化日志配置 + """ + # 自定义日志格式 + format_str = ( + '{time:YYYY-MM-DD HH:mm:ss.SSS} | ' + '{trace_id} | ' + '{level: <8} | ' + '{name}:{function}:{line} - ' + '{message}' + ) + _logger.remove() + # 移除后重新添加sys.stderr, 目的: 控制台输出与文件日志内容和结构一致 + _logger.add(sys.stderr, filter=self.__filter, format=format_str, enqueue=True) + _logger.add( + self.log_path_error, + filter=self.__filter, + format=format_str, + rotation='50MB', + encoding='utf-8', + enqueue=True, + compression='zip', + ) + + return _logger + + +# 初始化日志处理器 +log_initializer = LoggerInitializer() +logger = log_initializer.init_log() -- Gitee From 7cca5c88f3fdb855e2a5547d0252a8721e1f3045 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Sun, 26 Jan 2025 16:34:27 +0800 Subject: [PATCH 14/34] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E8=A3=85=E9=A5=B0=E5=99=A8=E8=8E=B7=E5=8F=96=E6=A0=B8?= =?UTF-8?q?=E5=BF=83=E5=8F=82=E6=95=B0=E7=9A=84=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_admin/annotation/log_annotation.py | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py index 5ba35a1..1d01c1c 100644 --- a/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py +++ b/ruoyi-fastapi-backend/module_admin/annotation/log_annotation.py @@ -7,7 +7,8 @@ from datetime import datetime from fastapi import Request from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse from functools import lru_cache, wraps -from typing import Literal, Optional +from sqlalchemy.ext.asyncio import AsyncSession +from typing import Any, Callable, Literal, Optional from user_agents import parse from config.enums import BusinessType from config.env import AppConfig @@ -55,9 +56,11 @@ class Log: # 获取当前被装饰函数所在路径 func_path = f'{relative_path}{func.__name__}()' # 获取上下文信息 - request: Request = kwargs.get('request') + request_name_list = get_function_parameters_name_by_type(func, Request) + request = get_function_parameters_value_by_name(func, request_name_list[0], *args, **kwargs) token = request.headers.get('Authorization') - query_db = kwargs.get('query_db') + session_name_list = get_function_parameters_name_by_type(func, AsyncSession) + query_db = get_function_parameters_value_by_name(func, session_name_list[0], *args, **kwargs) request_method = request.method operator_type = 0 user_agent = request.headers.get('User-Agent') @@ -222,3 +225,37 @@ def get_ip_location(oper_ip: str): oper_location = '未知' print(e) return oper_location + + +def get_function_parameters_name_by_type(func: Callable, param_type: Any): + """ + 获取函数指定类型的参数名称 + + :param func: 函数 + :param arg_type: 参数类型 + :return: 函数指定类型的参数名称 + """ + # 获取函数的参数信息 + parameters = inspect.signature(func).parameters + # 找到指定类型的参数名称 + parameters_name_list = [] + for name, param in parameters.items(): + if param.annotation == param_type: + parameters_name_list.append(name) + return parameters_name_list + + +def get_function_parameters_value_by_name(func: Callable, name: str, *args, **kwargs): + """ + 获取函数指定参数的值 + + :param func: 函数 + :param name: 参数名 + :return: 参数值 + """ + # 获取参数值 + bound_parameters = inspect.signature(func).bind(*args, **kwargs) + bound_parameters.apply_defaults() + parameters_value = bound_parameters.arguments.get(name) + + return parameters_value -- Gitee From 1d36c0c56e44e32561fe5dae7250dcc4cd85a9c7 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Fri, 14 Feb 2025 17:42:36 +0800 Subject: [PATCH 15/34] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/config/constant.py | 205 ++++++ ruoyi-fastapi-backend/config/env.py | 28 + .../controller/gen_controller.py | 158 +++++ .../module_generator/dao/gen_dao.py | 340 ++++++++++ .../module_generator/entity/do/gen_do.py | 73 +++ .../module_generator/entity/vo/gen_vo.py | 266 ++++++++ .../module_generator/service/gen_service.py | 491 ++++++++++++++ .../templates/js/api.js.jinja2 | 44 ++ .../templates/python/controller.py.jinja2 | 121 ++++ .../templates/python/dao.py.jinja2 | 139 ++++ .../templates/python/do.py.jinja2 | 40 ++ .../templates/python/service.py.jinja2 | 171 +++++ .../templates/python/vo.py.jinja2 | 72 +++ .../module_generator/templates/sql/sql.jinja2 | 22 + .../templates/vue/v3/index-tree.vue.jinja2 | 0 .../templates/vue/v3/index.vue.jinja2 | 572 +++++++++++++++++ ruoyi-fastapi-backend/server.py | 2 + .../sql/ruoyi-fastapi-pg.sql | 8 +- ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql | 4 +- ruoyi-fastapi-backend/utils/common_util.py | 4 + ruoyi-fastapi-backend/utils/gen_util.py | 152 +++++ ruoyi-fastapi-backend/utils/string_util.py | 80 ++- ruoyi-fastapi-backend/utils/template_util.py | 296 +++++++++ ruoyi-fastapi-frontend/src/api/tool/gen.js | 161 ++--- .../src/views/tool/gen/basicInfoForm.vue | 96 +-- .../src/views/tool/gen/createTable.vue | 46 ++ .../src/views/tool/gen/editTable.vue | 400 ++++++------ .../src/views/tool/gen/genInfoForm.vue | 603 +++++++++--------- .../src/views/tool/gen/importTable.vue | 244 +++---- .../src/views/tool/gen/index.vue | 594 ++++++++--------- 30 files changed, 4400 insertions(+), 1032 deletions(-) create mode 100644 ruoyi-fastapi-backend/module_generator/controller/gen_controller.py create mode 100644 ruoyi-fastapi-backend/module_generator/dao/gen_dao.py create mode 100644 ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py create mode 100644 ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py create mode 100644 ruoyi-fastapi-backend/module_generator/service/gen_service.py create mode 100644 ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 create mode 100644 ruoyi-fastapi-backend/utils/gen_util.py create mode 100644 ruoyi-fastapi-backend/utils/template_util.py create mode 100644 ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue diff --git a/ruoyi-fastapi-backend/config/constant.py b/ruoyi-fastapi-backend/config/constant.py index 6db32da..c9e0ed7 100644 --- a/ruoyi-fastapi-backend/config/constant.py +++ b/ruoyi-fastapi-backend/config/constant.py @@ -150,3 +150,208 @@ class MenuConstant: LAYOUT = 'Layout' PARENT_VIEW = 'ParentView' INNER_LINK = 'InnerLink' + + +class GenConstant: + """ + 代码生成常量 + """ + + """单表(增删改查)""" + TPL_CRUD = 'crud' + + """树表(增删改查)""" + TPL_TREE = 'tree' + + """主子表(增删改查)""" + TPL_SUB = 'sub' + + """树编码字段""" + TREE_CODE = 'treeCode' + + """树父编码字段""" + TREE_PARENT_CODE = 'treeParentCode' + + """树名称字段""" + TREE_NAME = 'treeName' + + """上级菜单ID字段""" + PARENT_MENU_ID = 'parentMenuId' + + """上级菜单名称字段""" + PARENT_MENU_NAME = 'parentMenuName' + + """数据库字符串类型""" + COLUMNTYPE_STR = ['char', 'varchar', 'nvarchar', 'varchar2'] + + """数据库文本类型""" + COLUMNTYPE_TEXT = ['tinytext', 'text', 'mediumtext', 'longtext'] + + """数据库时间类型""" + COLUMNTYPE_TIME = ['datetime', 'time', 'date', 'timestamp'] + + """数据库数字类型""" + COLUMNTYPE_NUMBER = [ + 'tinyint', + 'smallint', + 'mediumint', + 'int', + 'number', + 'integer', + 'bit', + 'bigint', + 'float', + 'double', + 'decimal', + ] + + """页面不需要编辑字段""" + COLUMNNAME_NOT_EDIT = ['id', 'create_by', 'create_time', 'del_flag'] + + """页面不需要显示的列表字段""" + COLUMNNAME_NOT_LIST = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time'] + + """页面不需要查询字段""" + COLUMNNAME_NOT_QUERY = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time', 'remark'] + + """Entity基类字段""" + BASE_ENTITY = ['createBy', 'createTime', 'updateBy', 'updateTime', 'remark'] + + """Tree基类字段""" + TREE_ENTITY = ['parentName', 'parentId', 'orderNum', 'ancestors', 'children'] + + """文本框""" + HTML_INPUT = 'input' + + """文本域""" + HTML_TEXTAREA = 'textarea' + + """下拉框""" + HTML_SELECT = 'select' + + """单选框""" + HTML_RADIO = 'radio' + + """复选框""" + HTML_CHECKBOX = 'checkbox' + + """日期控件""" + HTML_DATETIME = 'datetime' + + """图片上传控件""" + HTML_IMAGE_UPLOAD = 'imageUpload' + + """文件上传控件""" + HTML_FILE_UPLOAD = 'fileUpload' + + """富文本控件""" + HTML_EDITOR = 'editor' + + """高精度计算类型""" + TYPE_DECIMAL = 'Decimal' + + """时间类型""" + TYPE_DATE = ['date', 'time', 'datetime'] + + """模糊查询""" + QUERY_LIKE = 'LIKE' + + """相等查询""" + QUERY_EQ = 'EQ' + + """需要""" + REQUIRE = '1' + + MYSQL_TO_SQLALCHEMY_TYPE_MAPPING = { + # 数值类型 + 'TINYINT': 'SmallInteger', + 'SMALLINT': 'SmallInteger', + 'MEDIUMINT': 'Integer', + 'INT': 'Integer', + 'INTEGER': 'Integer', + 'BIGINT': 'BigInteger', + 'FLOAT': 'Float', + 'DOUBLE': 'Float', + 'DECIMAL': 'DECIMAL', + 'BIT': 'Integer', + # 日期和时间类型 + 'DATE': 'Date', + 'TIME': 'Time', + 'DATETIME': 'DateTime', + 'TIMESTAMP': 'TIMESTAMP', + 'YEAR': 'Integer', + # 字符串类型 + 'CHAR': 'CHAR', + 'VARCHAR': 'String', + 'TINYTEXT': 'Text', + 'TEXT': 'Text', + 'MEDIUMTEXT': 'Text', + 'LONGTEXT': 'Text', + 'BINARY': 'BINARY', + 'VARBINARY': 'VARBINARY', + 'TINYBLOB': 'LargeBinary', + 'BLOB': 'LargeBinary', + 'MEDIUMBLOB': 'LargeBinary', + 'LONGBLOB': 'LargeBinary', + # 枚举和集合类型 + 'ENUM': 'Enum', + 'SET': 'String', + # JSON 类型 + 'JSON': 'JSON', + # 空间数据类型(需要扩展支持,如 GeoAlchemy2) + 'GEOMETRY': 'geoalchemy2.Geometry', # 需要安装 geoalchemy2 + 'POINT': 'geoalchemy2.Geometry', + 'LINESTRING': 'geoalchemy2.Geometry', + 'POLYGON': 'geoalchemy2.Geometry', + 'MULTIPOINT': 'geoalchemy2.Geometry', + 'MULTILINESTRING': 'geoalchemy2.Geometry', + 'MULTIPOLYGON': 'geoalchemy2.Geometry', + 'GEOMETRYCOLLECTION': 'geoalchemy2.Geometry', + } + + MYSQL_TO_PYTHON_TYPE_MAPPING = { + # 数值类型 + 'TINYINT': 'int', + 'SMALLINT': 'int', + 'MEDIUMINT': 'int', + 'INT': 'int', + 'INTEGER': 'int', + 'BIGINT': 'int', + 'FLOAT': 'float', + 'DOUBLE': 'float', + 'DECIMAL': 'Decimal', + 'BIT': 'int', + # 日期和时间类型 + 'DATE': 'date', + 'TIME': 'time', + 'DATETIME': 'datetime', + 'TIMESTAMP': 'datetime', + 'YEAR': 'int', + # 字符串类型 + 'CHAR': 'str', + 'VARCHAR': 'str', + 'TINYTEXT': 'str', + 'TEXT': 'str', + 'MEDIUMTEXT': 'str', + 'LONGTEXT': 'str', + 'BINARY': 'bytes', + 'VARBINARY': 'bytes', + 'TINYBLOB': 'bytes', + 'BLOB': 'bytes', + 'MEDIUMBLOB': 'bytes', + 'LONGBLOB': 'bytes', + # 枚举和集合类型 + 'ENUM': 'str', + 'SET': 'str', + # JSON 类型 + 'JSON': 'dict', + # 空间数据类型(通常需要特殊处理) + 'GEOMETRY': 'bytes', + 'POINT': 'bytes', + 'LINESTRING': 'bytes', + 'POLYGON': 'bytes', + 'MULTIPOINT': 'bytes', + 'MULTILINESTRING': 'bytes', + 'MULTIPOLYGON': 'bytes', + 'GEOMETRYCOLLECTION': 'bytes', + } diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py index 78378a6..f4d0c60 100644 --- a/ruoyi-fastapi-backend/config/env.py +++ b/ruoyi-fastapi-backend/config/env.py @@ -64,6 +64,24 @@ class RedisSettings(BaseSettings): redis_database: int = 2 +class GenSettings: + """ + 代码生成配置 + """ + + author = 'insistence' + package_name = 'module_admin.system' + auto_remove_pre = False + table_prefix = 'sys_' + allow_overwrite = True + + GEN_PATH = 'vf_admin/gen_path' + + def __init__(self): + if not os.path.exists(self.GEN_PATH): + os.makedirs(self.GEN_PATH) + + class UploadSettings: """ 上传配置 @@ -159,6 +177,14 @@ class GetConfig: # 实例化Redis配置模型 return RedisSettings() + @lru_cache() + def get_gen_config(self): + """ + 获取代码生成配置 + """ + # 实例化代码生成配置 + return GenSettings() + @lru_cache() def get_upload_config(self): """ @@ -204,5 +230,7 @@ JwtConfig = get_config.get_jwt_config() DataBaseConfig = get_config.get_database_config() # Redis配置 RedisConfig = get_config.get_redis_config() +# 代码生成配置 +GenConfig = get_config.get_gen_config() # 上传配置 UploadConfig = get_config.get_upload_config() diff --git a/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py new file mode 100644 index 0000000..8396f1b --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py @@ -0,0 +1,158 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, Query, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.env import GenConfig +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckRoleInterfaceAuth, CheckUserInterfaceAuth +from module_admin.service.login_service import LoginService +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_generator.entity.vo.gen_vo import DeleteGenTableModel, EditGenTableModel, GenTablePageQueryModel +from module_generator.service.gen_service import GenTableColumnService, GenTableService +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +genController = APIRouter(prefix='/tool/gen', dependencies=[Depends(LoginService.get_current_user)]) + + +@genController.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))] +) +async def get_gen_table_list( + request: Request, + gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + gen_page_query_result = await GenTableService.get_gen_table_list_services(query_db, gen_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=gen_page_query_result) + + +@genController.get( + '/db/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:list'))] +) +async def get_gen_db_table_list( + request: Request, + gen_page_query: GenTablePageQueryModel = Depends(GenTablePageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): + # 获取分页数据 + gen_page_query_result = await GenTableService.get_gen_db_table_list_services(query_db, gen_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content=gen_page_query_result) + + +@genController.post('/importTable', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:import'))]) +@Log(title='代码生成', business_type=BusinessType.IMPORT) +async def import_gen_table( + request: Request, + tables: str = Query(), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + table_names = tables.split(',') if tables else [] + add_gen_table_list = await GenTableService.get_gen_db_table_list_by_name_services(query_db, table_names) + add_gen_table_result = await GenTableService.import_gen_table_services(query_db, add_gen_table_list, current_user) + logger.info(add_gen_table_result.message) + + return ResponseUtil.success(msg=add_gen_table_result.message) + + +@genController.put('', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))]) +@ValidateFields(validate_model='edit_post') +@Log(title='代码生成', business_type=BusinessType.UPDATE) +async def edit_gen_table( + request: Request, + edit_gen_table: EditGenTableModel, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_gen_table.update_by = current_user.user.user_name + edit_gen_table.update_time = datetime.now() + await GenTableService.validate_edit(edit_gen_table) + edit_gen_result = await GenTableService.edit_gen_table_services(query_db, edit_gen_table) + logger.info(edit_gen_result.message) + + return ResponseUtil.success(msg=edit_gen_result.message) + + +@genController.delete('/{table_ids}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:remove'))]) +@Log(title='代码生成', business_type=BusinessType.DELETE) +async def delete_gen_table(request: Request, table_ids: str, query_db: AsyncSession = Depends(get_db)): + delete_gen_table = DeleteGenTableModel(tableIds=table_ids) + delete_gen_table_result = await GenTableService.delete_gen_table_services(query_db, delete_gen_table) + logger.info(delete_gen_table_result.message) + + return ResponseUtil.success(msg=delete_gen_table_result.message) + + +@genController.post('/createTable', dependencies=[Depends(CheckRoleInterfaceAuth('admin'))]) +@Log(title='创建表', business_type=BusinessType.OTHER) +async def create_table( + request: Request, + sql: str = Query(), + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + create_table_result = await GenTableService.create_table_services(query_db, sql, current_user) + logger.info(create_table_result.message) + + return ResponseUtil.success(msg=create_table_result.message) + + +@genController.get('/batchGenCode', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))]) +@Log(title='代码生成', business_type=BusinessType.GENCODE) +async def batch_gen_code(request: Request, tables: str = Query(), query_db: AsyncSession = Depends(get_db)): + table_names = tables.split(',') if tables else [] + batch_gen_code_result = await GenTableService.batch_gen_code_services(query_db, table_names) + logger.info('生成代码成功') + + return ResponseUtil.streaming(data=bytes2file_response(batch_gen_code_result)) + + +@genController.get('/genCode/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:code'))]) +@Log(title='代码生成', business_type=BusinessType.GENCODE) +async def gen_code_local(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)): + if not GenConfig.allow_overwrite: + logger.error('【系统预设】不允许生成文件覆盖到本地') + return ResponseUtil.error('【系统预设】不允许生成文件覆盖到本地') + gen_code_local_result = await GenTableService.generate_code_services(query_db, table_name) + logger.info(gen_code_local_result.message) + + return ResponseUtil.success(msg=gen_code_local_result.message) + + +@genController.get('/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:query'))]) +async def query_detail_gen_table(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)): + gen_table = await GenTableService.get_gen_table_by_id_services(query_db, table_id) + gen_tables = await GenTableService.get_gen_table_all_services(query_db) + gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_services(query_db, table_id) + gen_table_detail_result = dict(info=gen_table, rows=gen_columns, tables=gen_tables) + logger.info(f'获取table_id为{table_id}的信息成功') + + return ResponseUtil.success(data=gen_table_detail_result) + + +@genController.get('/preview/{table_id}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:preview'))]) +async def preview_code(request: Request, table_id: int, query_db: AsyncSession = Depends(get_db)): + preview_code_result = await GenTableService.preview_code_services(query_db, table_id) + logger.info('获取预览代码成功') + + return ResponseUtil.success(data=preview_code_result) + + +@genController.get('/synchDb/{table_name}', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))]) +@Log(title='代码生成', business_type=BusinessType.UPDATE) +async def sync_db(request: Request, table_name: str, query_db: AsyncSession = Depends(get_db)): + sync_db_result = await GenTableService.sync_db_services(query_db, table_name) + logger.info(sync_db_result.message) + + return ResponseUtil.success(data=sync_db_result.message) diff --git a/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py new file mode 100644 index 0000000..8844ab2 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py @@ -0,0 +1,340 @@ +from datetime import datetime, time +from sqlalchemy import delete, func, select, text, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload +from typing import List +from module_generator.entity.do.gen_do import GenTable, GenTableColumn +from module_generator.entity.vo.gen_vo import ( + GenTableBaseModel, + GenTableColumnBaseModel, + GenTableColumnModel, + GenTableModel, + GenTablePageQueryModel, +) +from utils.page_util import PageUtil + + +class GenTableDao: + """ + 代码生成表模块数据库操作层 + """ + + @classmethod + async def get_gen_table_by_id(cls, db: AsyncSession, table_id: int): + """ + 根据表格id获取需要生成的表格信息 + + :param db: orm对象 + :param table_id: 岗位id + :return: 需要生成的表格信息对象 + """ + gen_table_info = ( + ( + await db.execute( + select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_id == table_id) + ) + ) + .scalars() + .first() + ) + + return gen_table_info + + @classmethod + async def get_gen_table_by_name(cls, db: AsyncSession, table_name: str): + """ + 根据表格名称获取需要生成的表格信息 + + :param db: orm对象 + :param table_name: 表格名称 + :return: 需要生成的表格信息对象 + """ + gen_table_info = ( + ( + await db.execute( + select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_name == table_name) + ) + ) + .scalars() + .first() + ) + + return gen_table_info + + @classmethod + async def get_gen_table_all(cls, db: AsyncSession): + """ + 根据表格id获取需要生成的表格详细信息 + + :param db: orm对象 + :return: 需要生成的表格信息对象 + """ + gen_table_all = (await db.execute(select(GenTable).options(selectinload(GenTable.columns)))).scalars().all() + + return gen_table_all + + @classmethod + async def create_table_by_sql_dao(cls, db: AsyncSession, sql: str): + """ + 根据sql语句创建表结构 + + :param db: orm对象 + :param sql: sql语句 + :return: + """ + await db.execute(text(sql)) + + @classmethod + async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取代码生成列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 代码生成列表信息对象 + """ + query = ( + select(GenTable) + .options(selectinload(GenTable.columns)) + .where( + func.lower(GenTable.table_name).like(f'%{query_object.table_name.lower()}%') + if query_object.table_name + else True, + func.lower(GenTable.table_comment).like(f'%{query_object.table_comment.lower()}%') + if query_object.table_comment + else True, + GenTable.create_time.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + ) + .distinct() + ) + gen_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return gen_table_list + + @classmethod + async def get_gen_db_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + """ + 根据查询参数获取数据库列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + query_sql = """ + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + information_schema.tables + where + table_schema = (select database()) + and table_name not like 'apscheduler\_%' + and table_name not like 'gen\_%' + and table_name not in (select table_name from gen_table) + """ + if query_object.table_name: + query_sql += """and lower(table_name) like lower(concat('%', :table_name, '%'))""" + if query_object.table_comment: + query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))""" + if query_object.begin_time: + query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:begin_time, '%Y%m%d')""" + if query_object.end_time: + query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:end_time, '%Y%m%d')""" + query_sql += """order by create_time desc""" + query = select( + text(query_sql).bindparams( + **{ + k: v + for k, v in query_object.model_dump(exclude_none=True, exclude={'page_num', 'page_size'}).items() + } + ) + ) + gen_db_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return gen_db_table_list + + @classmethod + async def get_gen_db_table_list_by_names(cls, db: AsyncSession, table_names: List[str]): + """ + 根据查询参数获取数据库列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + query_sql = """ + select + table_name as table_name, + table_comment as table_comment, + create_time as create_time, + update_time as update_time + from + information_schema.tables + where + table_name not like 'qrtz\_%' + and table_name not like 'gen\_%' + and table_schema = (select database()) + and table_name in :table_names + """ + query = text(query_sql).bindparams(table_names=tuple(table_names)) + gen_db_table_list = (await db.execute(query)).fetchall() + + return gen_db_table_list + + @classmethod + async def add_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): + """ + 新增岗位数据库操作 + + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + db_gen_table = GenTable(**GenTableBaseModel(**gen_table.model_dump(by_alias=True)).model_dump()) + db.add(db_gen_table) + await db.flush() + + return db_gen_table + + @classmethod + async def edit_gen_table_dao(cls, db: AsyncSession, gen_table: dict): + """ + 编辑岗位数据库操作 + + :param db: orm对象 + :param post: 需要更新的岗位字典 + :return: + """ + await db.execute(update(GenTable), [GenTableBaseModel(**gen_table).model_dump()]) + + @classmethod + async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): + """ + 删除岗位数据库操作 + + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + await db.execute(delete(GenTable).where(GenTable.table_id.in_([gen_table.table_id]))) + + +class GenTableColumnDao: + """ + 代码生成列模块数据库操作层 + """ + + @classmethod + async def get_gen_table_column_list_by_table_id(cls, db: AsyncSession, table_id: int): + """ + 根据表格id获取需要生成的列列表信息 + + :param db: orm对象 + :param table_id: 表格id + :return: 需要生成的列列表信息对象 + """ + gen_table_column_list = ( + (await db.execute(select(GenTableColumn).where(GenTableColumn.table_id == table_id))).scalars().all() + ) + + return gen_table_column_list + + @classmethod + async def get_gen_db_table_columns_by_name(cls, db: AsyncSession, table_name: str): + """ + 根据查询参数获取数据库列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + query_sql = """ + select + column_name as column_name, + case + when is_nullable = 'no' and column_key != 'PRI' then '1' + else '0' + end as is_required, + case + when column_key = 'PRI' then '1' + else '0' + end as is_pk, + ordinal_position as sort, + column_comment as column_comment, + case + when extra = 'auto_increment' then '1' + else '0' + end as is_increment, + column_type as column_type + from + information_schema.columns + where + table_schema = (select database()) + and table_name = :table_name + order by + ordinal_position + """ + query = text(query_sql).bindparams(table_name=table_name) + gen_db_table_columns = (await db.execute(query)).fetchall() + + return gen_db_table_columns + + @classmethod + async def add_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 新增岗位数据库操作 + + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + db_gen_table_column = GenTableColumn( + **GenTableColumnBaseModel(**gen_table_column.model_dump(by_alias=True)).model_dump() + ) + db.add(db_gen_table_column) + await db.flush() + + return db_gen_table_column + + @classmethod + async def edit_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: dict): + """ + 编辑岗位数据库操作 + + :param db: orm对象 + :param post: 需要更新的岗位字典 + :return: + """ + await db.execute(update(GenTableColumn), [GenTableColumnBaseModel(**gen_table_column).model_dump()]) + + @classmethod + async def delete_gen_table_column_by_table_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 删除岗位数据库操作 + + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + await db.execute(delete(GenTableColumn).where(GenTableColumn.table_id.in_([gen_table_column.table_id]))) + + @classmethod + async def delete_gen_table_column_by_column_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): + """ + 删除岗位数据库操作 + + :param db: orm对象 + :param post: 岗位对象 + :return: + """ + await db.execute(delete(GenTableColumn).where(GenTableColumn.column_id.in_([gen_table_column.column_id]))) diff --git a/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py new file mode 100644 index 0000000..bcc3c87 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py @@ -0,0 +1,73 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship +from config.database import Base + + +class GenTable(Base): + """ + 代码生成业务表 + """ + + __tablename__ = 'gen_table' + + table_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号') + table_name = Column(String(200), nullable=True, default='', comment='表名称') + table_comment = Column(String(500), nullable=True, default='', comment='表描述') + sub_table_name = Column(String(64), nullable=True, comment='关联子表的表名') + sub_table_fk_name = Column(String(64), nullable=True, comment='子表关联的外键名') + class_name = Column(String(100), nullable=True, default='', comment='实体类名称') + tpl_category = Column(String(200), nullable=True, default='crud', comment='使用的模板(crud单表操作 tree树表操作)') + tpl_web_type = Column( + String(30), nullable=True, default='', comment='前端模板类型(element-ui模版 element-plus模版)' + ) + package_name = Column(String(100), nullable=True, comment='生成包路径') + module_name = Column(String(30), nullable=True, comment='生成模块名') + business_name = Column(String(30), nullable=True, comment='生成业务名') + function_name = Column(String(100), nullable=True, comment='生成功能名') + function_author = Column(String(100), nullable=True, comment='生成功能作者') + gen_type = Column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1自定义路径)') + gen_path = Column(String(200), nullable=True, default='/', comment='生成路径(不填默认项目路径)') + options = Column(String(1000), nullable=True, comment='其它生成选项') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + remark = Column(String(500), nullable=True, default=None, comment='备注') + + columns = relationship('GenTableColumn', order_by='GenTableColumn.sort', back_populates='tables') + + +class GenTableColumn(Base): + """ + 代码生成业务表字段 + """ + + __tablename__ = 'gen_table_column' + + column_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号') + table_id = Column(Integer, ForeignKey('gen_table.table_id'), nullable=True, comment='归属表编号') + column_name = Column(String(200), nullable=True, comment='列名称') + column_comment = Column(String(500), nullable=True, comment='列描述') + column_type = Column(String(100), nullable=True, comment='列类型') + python_type = Column(String(500), nullable=True, comment='PYTHON类型') + python_field = Column(String(200), nullable=True, comment='PYTHON字段名') + is_pk = Column(String(1), nullable=True, comment='是否主键(1是)') + is_increment = Column(String(1), nullable=True, comment='是否自增(1是)') + is_required = Column(String(1), nullable=True, comment='是否必填(1是)') + is_insert = Column(String(1), nullable=True, comment='是否为插入字段(1是)') + is_edit = Column(String(1), nullable=True, comment='是否编辑字段(1是)') + is_list = Column(String(1), nullable=True, comment='是否列表字段(1是)') + is_query = Column(String(1), nullable=True, comment='是否查询字段(1是)') + query_type = Column(String(200), nullable=True, default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)') + html_type = Column( + String(200), nullable=True, comment='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)' + ) + dict_type = Column(String(200), nullable=True, default='', comment='字典类型') + sort = Column(Integer, nullable=True, comment='排序') + create_by = Column(String(64), default='', comment='创建者') + create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间') + update_by = Column(String(64), default='', comment='更新者') + update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间') + + tables = relationship('GenTable', back_populates='columns') diff --git a/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py new file mode 100644 index 0000000..a28d3b0 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py @@ -0,0 +1,266 @@ +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import List, Literal, Optional +from config.constant import GenConstant +from module_admin.annotation.pydantic_annotation import as_query + + +class GenTableBaseModel(BaseModel): + """ + 代码生成业务表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + table_id: Optional[int] = Field(default=None, description='编号') + table_name: Optional[str] = Field(default=None, description='表名称') + table_comment: Optional[str] = Field(default=None, description='表描述') + sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名') + sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名') + class_name: Optional[str] = Field(default=None, description='实体类名称') + tpl_category: Optional[str] = Field(default=None, description='使用的模板(crud单表操作 tree树表操作)') + tpl_web_type: Optional[str] = Field(default=None, description='前端模板类型(element-ui模版 element-plus模版)') + package_name: Optional[str] = Field(default=None, description='生成包路径') + module_name: Optional[str] = Field(default=None, description='生成模块名') + business_name: Optional[str] = Field(default=None, description='生成业务名') + function_name: Optional[str] = Field(default=None, description='生成功能名') + function_author: Optional[str] = Field(default=None, description='生成功能作者') + gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1自定义路径)') + gen_path: Optional[str] = Field(default=None, description='生成路径(不填默认项目路径)') + options: Optional[str] = Field(default=None, description='其它生成选项') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') + remark: Optional[str] = Field(default=None, description='备注') + + @NotBlank(field_name='table_name', message='表名称不能为空') + def get_table_name(self): + return self.table_name + + @NotBlank(field_name='table_comment', message='表描述不能为空') + def get_table_comment(self): + return self.table_comment + + @NotBlank(field_name='class_name', message='实体类名称不能为空') + def get_class_name(self): + return self.class_name + + @NotBlank(field_name='package_name', message='生成包路径不能为空') + def get_package_name(self): + return self.package_name + + @NotBlank(field_name='module_name', message='生成模块名不能为空') + def get_module_name(self): + return self.module_name + + @NotBlank(field_name='business_name', message='生成业务名不能为空') + def get_business_name(self): + return self.business_name + + @NotBlank(field_name='function_name', message='生成功能名不能为空') + def get_function_name(self): + return self.function_name + + @NotBlank(field_name='function_author', message='生成功能作者不能为空') + def get_function_author(self): + return self.function_author + + def validate_fields(self): + self.get_table_name() + self.get_table_comment() + self.get_class_name() + self.get_package_name() + self.get_module_name() + self.get_business_name() + self.get_function_name() + self.get_function_author() + + +class GenTableModel(GenTableBaseModel): + """ + 代码生成业务表模型 + """ + + pk_column: Optional['GenTableColumnModel'] = Field(default=None, description='主键信息') + sub_table: Optional['GenTableModel'] = Field(default=None, description='子表信息') + columns: Optional[List['GenTableColumnModel']] = Field(default=None, description='表列信息') + tree_code: Optional[str] = Field(default=None, description='树编码字段') + tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') + tree_name: Optional[str] = Field(default=None, description='树名称字段') + parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') + parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段') + sub: Optional[bool] = Field(default=None, description='是否为子表') + tree: Optional[bool] = Field(default=None, description='是否为树表') + crud: Optional[bool] = Field(default=None, description='是否为单表') + + @model_validator(mode='after') + def check_some_is(self) -> 'GenTableModel': + self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False + self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False + self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False + return self + + +class EditGenTableModel(GenTableModel): + """ + 修改代码生成业务表模型 + """ + + params: Optional['GenTableParamsModel'] = Field(default=None, description='业务表参数') + + +class GenTableParamsModel(BaseModel): + """ + 代码生成业务表参数模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + tree_code: Optional[str] = Field(default=None, description='树编码字段') + tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') + tree_name: Optional[str] = Field(default=None, description='树名称字段') + parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') + + +class GenTableQueryModel(GenTableBaseModel): + """ + 代码生成业务表不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class GenTablePageQueryModel(GenTableQueryModel): + """ + 代码生成业务表分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteGenTableModel(BaseModel): + """ + 删除代码生成业务表模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + table_ids: str = Field(description='需要删除的代码生成业务表ID') + + +class GenTableColumnBaseModel(BaseModel): + """ + 代码生成业务表字段对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + column_id: Optional[int] = Field(default=None, description='编号') + table_id: Optional[int] = Field(default=None, description='归属表编号') + column_name: Optional[str] = Field(default=None, description='列名称') + column_comment: Optional[str] = Field(default=None, description='列描述') + column_type: Optional[str] = Field(default=None, description='列类型') + python_type: Optional[str] = Field(default=None, description='PYTHON类型') + python_field: Optional[str] = Field(default=None, description='PYTHON字段名') + is_pk: Optional[str] = Field(default=None, description='是否主键(1是)') + is_increment: Optional[str] = Field(default=None, description='是否自增(1是)') + is_required: Optional[str] = Field(default=None, description='是否必填(1是)') + is_insert: Optional[str] = Field(default=None, description='是否为插入字段(1是)') + is_edit: Optional[str] = Field(default=None, description='是否编辑字段(1是)') + is_list: Optional[str] = Field(default=None, description='是否列表字段(1是)') + is_query: Optional[str] = Field(default=None, description='是否查询字段(1是)') + query_type: Optional[str] = Field(default=None, description='查询方式(等于、不等于、大于、小于、范围)') + html_type: Optional[str] = Field( + default=None, description='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)' + ) + dict_type: Optional[str] = Field(default=None, description='字典类型') + sort: Optional[int] = Field(default=None, description='排序') + create_by: Optional[str] = Field(default=None, description='创建者') + create_time: Optional[datetime] = Field(default=None, description='创建时间') + + @NotBlank(field_name='python_field', message='Python属性不能为空') + def get_python_field(self): + return self.python_field + + def validate_fields(self): + self.get_python_field() + + +class GenTableColumnModel(GenTableColumnBaseModel): + """ + 代码生成业务表字段模型 + """ + + cap_python_field: Optional[str] = Field(default=None, description='字段大写形式') + pk: Optional[bool] = Field(default=None, description='是否为子表') + increment: Optional[bool] = Field(default=None, description='是否为树表') + required: Optional[bool] = Field(default=None, description='是否为必填字段') + insert: Optional[bool] = Field(default=None, description='是否为必填字段') + edit: Optional[bool] = Field(default=None, description='是否为必填字段') + list: Optional[bool] = Field(default=None, description='是否为必填字段') + query: Optional[bool] = Field(default=None, description='是否为必填字段') + super_column: Optional[bool] = Field(default=None, description='是否为基类字段') + usable_column: Optional[bool] = Field(default=None, description='是否为基类字段') + + @model_validator(mode='after') + def check_some_is(self) -> 'GenTableModel': + self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None + self.pk = True if self.is_pk and self.is_pk == '1' else False + self.increment = True if self.is_increment and self.is_increment == '1' else False + self.required = True if self.is_required and self.is_required == '1' else False + self.insert = True if self.is_insert and self.is_insert == '1' else False + self.edit = True if self.is_edit and self.is_edit == '1' else False + self.list = True if self.is_list and self.is_list == '1' else False + self.query = True if self.is_query and self.is_query == '1' else False + self.super_column = ( + True + if any( + self.python_field and self.python_field.lower() == field.lower() + for field in GenConstant.TREE_ENTITY + GenConstant.BASE_ENTITY + ) + else False + ) + self.usable_column = ( + True + if any( + self.python_field and self.python_field.lower() == field.lower() + for field in ['parentId', 'orderNum', 'remark'] + ) + else False + ) + return self + + +class GenTableColumnQueryModel(GenTableColumnBaseModel): + """ + 代码生成业务表字段不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class GenTableColumnPageQueryModel(GenTableColumnQueryModel): + """ + 代码生成业务表字段分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class DeleteGenTableColumnModel(BaseModel): + """ + 删除代码生成业务表字段模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + column_ids: str = Field(description='需要删除的代码生成业务表字段ID') diff --git a/ruoyi-fastapi-backend/module_generator/service/gen_service.py b/ruoyi-fastapi-backend/module_generator/service/gen_service.py new file mode 100644 index 0000000..0879d31 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/service/gen_service.py @@ -0,0 +1,491 @@ +import io +import json +import os +import re +import zipfile +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import GenConstant +from config.env import GenConfig +from exceptions.exception import ServiceException +from module_admin.entity.vo.common_vo import CrudResponseModel +from module_admin.entity.vo.user_vo import CurrentUserModel +from module_generator.entity.vo.gen_vo import ( + DeleteGenTableModel, + EditGenTableModel, + GenTableColumnModel, + GenTableModel, + GenTablePageQueryModel, +) +from module_generator.dao.gen_dao import GenTableColumnDao, GenTableDao +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil +from utils.gen_util import GenUtils +from utils.template_util import TemplateInitializer, TemplateUtils + + +class GenTableService: + """ + 岗位管理模块服务层 + """ + + @classmethod + async def get_gen_table_list_services( + cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False + ): + """ + 获取代码生成列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 代码生成列表信息对象 + """ + gen_table_list_result = await GenTableDao.get_gen_table_list(query_db, query_object, is_page) + + return gen_table_list_result + + @classmethod + async def get_gen_db_table_list_services( + cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False + ): + """ + 获取数据库列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + gen_db_table_list_result = await GenTableDao.get_gen_db_table_list(query_db, query_object, is_page) + + return gen_db_table_list_result + + @classmethod + async def get_gen_db_table_list_by_name_services(cls, query_db: AsyncSession, table_names: List[str]): + """ + 获取数据库列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: 数据库列表信息对象 + """ + gen_db_table_list_result = await GenTableDao.get_gen_db_table_list_by_names(query_db, table_names) + + return [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_db_table_list_result)] + + @classmethod + async def import_gen_table_services( + cls, query_db: AsyncSession, gen_table_list: List[GenTableModel], current_user: CurrentUserModel + ): + try: + for table in gen_table_list: + table_name = table.table_name + GenUtils.init_table(table, current_user.user.user_name) + add_gen_table = await GenTableDao.add_gen_table_dao(query_db, table) + if add_gen_table: + table.table_id = add_gen_table.table_id + gen_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name) + for column in [ + GenTableColumnModel(**gen_table_column) + for gen_table_column in CamelCaseUtil.transform_result(gen_table_columns) + ]: + GenUtils.init_column_field(column, table) + await GenTableColumnDao.add_gen_table_column_dao(query_db, column) + await query_db.commit() + return CrudResponseModel(is_success=True, message='导入成功') + except Exception as e: + await query_db.rollback() + raise ServiceException(message=f'导入失败, {str(e)}') + + @classmethod + async def edit_gen_table_services(cls, query_db: AsyncSession, page_object: EditGenTableModel): + """ + 编辑岗位信息service + + :param query_db: orm对象 + :param page_object: 编辑岗位对象 + :return: 编辑岗位校验结果 + """ + edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True) + gen_table_info = await cls.get_gen_table_by_id_services(query_db, page_object.table_id) + if gen_table_info.table_id: + try: + edit_gen_table['options'] = json.dumps(edit_gen_table.get('params')) + await GenTableDao.edit_gen_table_dao(query_db, edit_gen_table) + for gen_table_column in page_object.columns: + await GenTableColumnDao.edit_gen_table_column_dao( + query_db, gen_table_column.model_dump(by_alias=True) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='业务表不存在') + + @classmethod + async def delete_gen_table_services(cls, query_db: AsyncSession, page_object: DeleteGenTableModel): + """ + 删除岗位信息service + + :param query_db: orm对象 + :param page_object: 删除岗位对象 + :return: 删除岗位校验结果 + """ + if page_object.table_ids: + table_id_list = page_object.table_ids.split(',') + try: + for table_id in table_id_list: + await GenTableDao.delete_gen_table_dao(query_db, GenTableModel(tableId=table_id)) + await GenTableColumnDao.delete_gen_table_column_by_table_id_dao( + query_db, GenTableColumnModel(tableId=table_id) + ) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入业务表id为空') + + @classmethod + async def get_gen_table_by_id_services(cls, query_db: AsyncSession, table_id: int): + """ + 获取需要生成的表格详细信息service + + :param query_db: orm对象 + :param table_id: 需要生成的表格id + :return: 需要生成的表格id对应的信息 + """ + gen_table = await GenTableDao.get_gen_table_by_id(query_db, table_id) + result = await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table))) + + return result + + @classmethod + async def get_gen_table_all_services(cls, query_db: AsyncSession): + """ + 获取需要生成的表格详细信息service + + :param query_db: orm对象 + :param table_id: 需要生成的表格id + :return: 需要生成的表格id对应的信息 + """ + gen_table_all = await GenTableDao.get_gen_table_all(query_db) + result = [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_table_all)] + + return result + + @classmethod + async def create_table_services(cls, query_db: AsyncSession, sql: str, current_user: CurrentUserModel): + """ + 创建表结构service + + :param query_db: orm对象 + :param sql: 建表语句 + :param current_user: 当前用户信息对象 + :return: 创建表结构结果 + """ + if cls.__is_valid_create_table(sql): + try: + table_names = re.findall(r'create\s+table\s+(\w+)', sql, re.IGNORECASE) + await GenTableDao.create_table_by_sql_dao(query_db, sql) + gen_table_list = await cls.get_gen_db_table_list_by_name_services(query_db, table_names) + await cls.import_gen_table_services(query_db, gen_table_list, current_user) + + return CrudResponseModel(is_success=True, message='创建表结构成功') + except Exception as e: + raise ServiceException(message=f'创建表结构异常,详细错误信息:{str(e)}') + else: + raise ServiceException(message='建表语句不合法') + + @classmethod + def __is_valid_create_table(cls, sql: str): + """ + 校验sql语句是否为合法的建表语句 + + :param sql: sql语句 + :return: 校验结果 + """ + create_table_pattern = r'^\s*CREATE\s+TABLE\s+' + if not re.search(create_table_pattern, sql, re.IGNORECASE): + return False + forbidden_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE'] + for keyword in forbidden_keywords: + if re.search(rf'\b{keyword}\b', sql, re.IGNORECASE): + return False + return True + + @classmethod + async def preview_code_services(cls, query_db: AsyncSession, table_id: int): + """ + 预览代码service + + :param query_db: orm对象 + :param table_id: 表格id + :return: 预览数据列表 + """ + gen_table = GenTableModel( + **CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_id(query_db, table_id)) + ) + await cls.set_sub_table(query_db, gen_table) + await cls.set_pk_column(gen_table) + env = TemplateInitializer.init_jinja2() + context = TemplateUtils.prepare_context(gen_table) + template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type) + preview_code_result = {} + for template in template_list: + render_content = env.get_template(template).render(**context) + preview_code_result[template] = render_content + return preview_code_result + + @classmethod + async def generate_code_services(cls, query_db: AsyncSession, table_name: str): + """ + 生成代码至指定路径service + + :param query_db: orm对象 + :param table_name: 表格名称 + :return: 生成代码结果 + """ + env = TemplateInitializer.init_jinja2() + render_info = await cls.__get_gen_render_info(query_db, table_name) + for template in render_info[0]: + try: + render_content = env.get_template(template).render(**render_info[2]) + gen_path = cls.__get_gen_path(render_info[3], template) + os.makedirs(os.path.dirname(gen_path), exist_ok=True) + with open(gen_path, 'w', encoding='utf-8') as f: + f.write(render_content) + except Exception as e: + raise ServiceException( + message=f'渲染模板失败,表名:{render_info[3].table_name},详细错误信息:{str(e)}' + ) + + return CrudResponseModel(is_success=True, message='生成代码成功') + + @classmethod + async def batch_gen_code_services(cls, query_db: AsyncSession, table_names: List[str]): + """ + 批量生成代码service + + :param query_db: orm对象 + :param table_names: 表格名称 + :return: 下载代码结果 + """ + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file: + for table_name in table_names: + env = TemplateInitializer.init_jinja2() + render_info = await cls.__get_gen_render_info(query_db, table_name) + for template_file, output_file in zip(render_info[0], render_info[1]): + render_content = env.get_template(template_file).render(**render_info[2]) + zip_file.writestr(output_file, render_content) + + zip_data = zip_buffer.getvalue() + zip_buffer.close() + return zip_data + + @classmethod + async def __get_gen_render_info(cls, query_db: AsyncSession, table_name: str): + """ + 获取生成代码渲染模板相关信息 + + :param query_db: orm对象 + :param table_name: 表格名称 + :return: 生成代码渲染模板相关信息 + """ + gen_table = GenTableModel( + **CamelCaseUtil.transform_result(await GenTableDao.get_gen_table_by_name(query_db, table_name)) + ) + await cls.set_sub_table(query_db, gen_table) + await cls.set_pk_column(gen_table) + context = TemplateUtils.prepare_context(gen_table) + template_list = TemplateUtils.get_template_list(gen_table.tpl_category, gen_table.tpl_web_type) + output_files = [TemplateUtils.get_file_name(template, gen_table) for template in template_list] + + return [template_list, output_files, context, gen_table] + + @classmethod + def __get_gen_path(cls, gen_table: GenTableModel, template: str): + """ + 根据GenTableModel对象和模板名称生成路径 + + :param gen_table: GenTableModel对象 + :param template: 模板名称 + :return: 生成的路径 + """ + gen_path = gen_table.gen_path + if gen_path == '/': + return os.path.join(os.getcwd(), GenConfig.GEN_PATH, TemplateUtils.get_file_name(template, gen_table)) + else: + return os.path.join(gen_path, TemplateUtils.get_file_name(template, gen_table)) + + @classmethod + async def sync_db_services(cls, query_db: AsyncSession, table_name: str): + """ + 获取需要生成的表格详细信息service + + :param query_db: orm对象 + :param table_name: 表格名称 + :return: 需要生成的表格名称对应的信息 + """ + gen_table = await GenTableDao.get_gen_table_by_name(query_db, table_name) + table = GenTableModel(**CamelCaseUtil.transform_result(gen_table)) + table_columns = table.columns + table_column_map = {column.column_name: column for column in table_columns} + query_db_table_columns = await GenTableColumnDao.get_gen_db_table_columns_by_name(query_db, table_name) + db_table_columns = [ + GenTableColumnModel(**column) for column in CamelCaseUtil.transform_result(query_db_table_columns) + ] + if not db_table_columns: + raise ServiceException('同步数据失败,原表结构不存在') + db_table_column_names = [column.column_name for column in db_table_columns] + try: + for column in db_table_columns: + GenUtils.init_column_field(column, table) + if column.column_name in table_column_map: + prev_column = table_column_map[column.column_name] + column.column_id = prev_column.column_id + if column.list: + column.dict_type = prev_column.dict_type + column.query_type = prev_column.query_type + if ( + prev_column.is_required != '' + and not column.pk + and (column.insert or column.edit) + and (column.usable_column or column.super_column) + ): + column.is_required = prev_column.is_required + column.html_type = prev_column.html_type + await GenTableColumnDao.edit_gen_table_column_dao(query_db, column.model_dump(by_alias=True)) + else: + await GenTableColumnDao.add_gen_table_column_dao(query_db, column) + del_columns = [column for column in table_columns if column.column_name not in db_table_column_names] + if del_columns: + for column in del_columns: + await GenTableColumnDao.delete_gen_table_column_by_column_id_dao(query_db, column) + await query_db.commit() + return CrudResponseModel(is_success=True, message='同步成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def set_sub_table(cls, query_db: AsyncSession, gen_table: GenTableModel): + """ + 设置主子表信息 + + :param query_db: orm对象 + :param gen_table: 业务表信息 + """ + if gen_table.sub_table_name: + sub_table = await GenTableDao.get_gen_table_by_name(query_db, gen_table.sub_table_name) + gen_table.sub_table = GenTableModel(**CamelCaseUtil.transform_result(sub_table)) + + @classmethod + async def set_pk_column(cls, gen_table: GenTableModel): + """ + 设置主键列信息 + + :param gen_table: 业务表信息 + """ + for column in gen_table.columns: + if column.pk: + gen_table.pk_column = column + break + if gen_table.pk_column is None: + gen_table.pk_column = gen_table.columns[0] + if gen_table.tpl_category == GenConstant.TPL_SUB: + for column in gen_table.sub_table.columns: + if column.pk: + gen_table.sub_table.pk_column = column + break + if gen_table.sub_table.columns is None: + gen_table.sub_table.pk_column = gen_table.sub_table.columns[0] + + @staticmethod + async def export_post_list_services(post_list: List): + """ + 导出岗位信息service + + :param post_list: 岗位信息列表 + :return: 岗位信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + 'postId': '岗位编号', + 'postCode': '岗位编码', + 'postName': '岗位名称', + 'postSort': '显示顺序', + 'status': '状态', + 'createBy': '创建者', + 'createTime': '创建时间', + 'updateBy': '更新者', + 'updateTime': '更新时间', + 'remark': '备注', + } + + for item in post_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '停用' + binary_data = ExcelUtil.export_list2excel(post_list, mapping_dict) + + return binary_data + + @classmethod + async def set_table_from_options(cls, gen_table: GenTableModel): + """ + 设置代码生成其他选项值 + + :param gen_table: 设置后的生成对象 + """ + params_obj = json.loads(gen_table.options) if gen_table.options else None + if params_obj: + gen_table.tree_code = params_obj.get(GenConstant.TREE_CODE) + gen_table.tree_parent_code = params_obj.get(GenConstant.TREE_PARENT_CODE) + gen_table.tree_name = params_obj.get(GenConstant.TREE_NAME) + gen_table.parent_menu_id = params_obj.get(GenConstant.PARENT_MENU_ID) + gen_table.parent_menu_name = params_obj.get(GenConstant.PARENT_MENU_NAME) + + return gen_table + + @classmethod + async def validate_edit(cls, edit_gen_table: EditGenTableModel): + if edit_gen_table.tpl_category == GenConstant.TPL_TREE: + params_obj = edit_gen_table.params + + if not getattr(params_obj, GenConstant.TREE_CODE): + raise ServiceException('树编码字段不能为空') + elif not getattr(params_obj, GenConstant.TREE_PARENT_CODE): + raise ServiceException('树父编码字段不能为空') + elif not getattr(params_obj, GenConstant.TREE_NAME): + raise ServiceException('树名称字段不能为空') + elif edit_gen_table.tpl_category == GenConstant.TPL_SUB: + if not edit_gen_table.sub_table_name: + raise ServiceException('关联子表的表名不能为空') + elif not edit_gen_table.sub_table_fk_name: + raise ServiceException('子表关联的外键名不能为空') + + +class GenTableColumnService: + @classmethod + async def get_gen_table_column_list_by_table_id_services(cls, query_db: AsyncSession, table_id: int): + """ + 获取代码生成列列表信息service + + :param query_db: orm对象 + :param table_id: 表格id + :return: 代码生成列列表信息对象 + """ + gen_table_column_list_result = await GenTableColumnDao.get_gen_table_column_list_by_table_id(query_db, table_id) + + return [ + GenTableColumnModel(**gen_table_column) + for gen_table_column in CamelCaseUtil.transform_result(gen_table_column_list_result) + ] diff --git a/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 new file mode 100644 index 0000000..3a2a5a9 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/js/api.js.jinja2 @@ -0,0 +1,44 @@ +import request from '@/utils/request' + +// 查询{{ functionName }}列表 +export function list{{ BusinessName }}(query) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/list', + method: 'get', + params: query + }) +} + +// 查询{{ functionName }}详细 +export function get{{ BusinessName }}({{ pkColumn.python_field }}) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/' + {{ pkColumn.python_field }}, + method: 'get' + }) +} + +// 新增{{ functionName }} +export function add{{ BusinessName }}(data) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}', + method: 'post', + data: data + }) +} + +// 修改{{ functionName }} +export function update{{ BusinessName }}(data) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}', + method: 'put', + data: data + }) +} + +// 删除{{ functionName }} +export function del{{ BusinessName }}({{ pkColumn.python_field }}) { + return request({ + url: '/{{ moduleName }}/{{ businessName }}/' + {{ pkColumn.python_field }}, + method: 'delete' + }) +} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 new file mode 100644 index 0000000..5f6c3d1 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 @@ -0,0 +1,121 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +from datetime import datetime +from fastapi import APIRouter, Depends, Form, Request +from pydantic_validation_decorator import ValidateFields +from sqlalchemy.ext.asyncio import AsyncSession +from config.enums import BusinessType +from config.get_db import get_db +from module_admin.annotation.log_annotation import Log +from module_admin.aspect.interface_auth import CheckUserInterfaceAuth +from module_admin.service.login_service import LoginService +from {{ packageName }}.service.{{ businessName }}_service import {{ BusinessName }}Service +from {{ packageName }}.entity.vo.{{ businessName }}_vo import Delete{{ BusinessName }}Model, {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +from {{ packageName }}.entity.vo.user_vo import CurrentUserModel +from utils.common_util import bytes2file_response +from utils.log_util import logger +from utils.page_util import PageResponseModel +from utils.response_util import ResponseUtil + + +{{ businessName }}Controller = APIRouter(prefix='/{{ moduleName }}/{{ businessName }}', dependencies=[Depends(LoginService.get_current_user)]) + + +@{{ businessName }}Controller.get( + '/list', response_model=PageResponseModel, dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:list'))] +) +async def get_{{ moduleName }}_{{ businessName }}_list( + request: Request, + {{ businessName }}_page_query: {{ BusinessName }}PageQueryModel = Depends({{ BusinessName }}PageQueryModel.as_query), + query_db: AsyncSession = Depends(get_db), +): +{% if table.crud or table.sub %} + # 获取分页数据 + {{ businessName }}_page_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=True) + logger.info('获取成功') + + return ResponseUtil.success(model_content={{ businessName }}_page_query_result) +{% elif table.tree %} + {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_query) + logger.info('获取成功') + + return ResponseUtil.success(data={{ businessName }}_query_result) +{% endif %} + + +@{{ businessName }}Controller.post('', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:add'))]) +@ValidateFields(validate_model='add_{{ businessName }}') +@Log(title='{{ functionName }}', business_type=BusinessType.INSERT) +async def add_{{ moduleName }}_{{ businessName }}( + request: Request, + add_{{ businessName }}: {{ BusinessName }}Model, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): +{% for column in columns %} +{% if column.python_field == "createBy" %} + add_{{ businessName }}.create_by = current_user.user.user_name +{% elif column.python_field == "createTime" %} + add_{{ businessName }}.create_time = datetime.now() +{% elif column.python_field == "updateBy" %} + add_{{ businessName }}.update_by = current_user.user.user_name +{% elif column.python_field == "updateTime" %} + add_{{ businessName }}.update_time = datetime.now() +{% endif %} +{% endfor %} + add_{{ businessName }}_result = await {{ BusinessName }}Service.add_{{ businessName }}_services(query_db, add_{{ businessName }}) + logger.info(add_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=add_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.put('', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:edit'))]) +@ValidateFields(validate_model='edit_{{ businessName }}') +@Log(title='{{ functionName }}', business_type=BusinessType.UPDATE) +async def edit_{{ moduleName }}_{{ businessName }}( + request: Request, + edit_{{ businessName }}: {{ BusinessName }}Model, + query_db: AsyncSession = Depends(get_db), + current_user: CurrentUserModel = Depends(LoginService.get_current_user), +): + edit_{{ businessName }}.update_by = current_user.user.user_name + edit_{{ businessName }}.update_time = datetime.now() + edit_{{ businessName }}_result = await {{ BusinessName }}Service.edit_{{ businessName }}_services(query_db, edit_{{ businessName }}) + logger.info(edit_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=edit_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.delete('/{% raw %}{{% endraw %}{{ pk_field }}s{% raw %}}{% endraw %}', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:remove'))]) +@Log(title='{{ functionName }}', business_type=BusinessType.DELETE) +async def delete_{{ moduleName }}_{{ businessName }}(request: Request, {{ pk_field }}s: str, query_db: AsyncSession = Depends(get_db)): + delete_{{ businessName }} = Delete{{ BusinessName }}Model({{ pkField }}s={{ pk_field }}s) + delete_{{ businessName }}_result = await {{ BusinessName }}Service.delete_{{ businessName }}_services(query_db, delete_{{ businessName }}) + logger.info(delete_{{ businessName }}_result.message) + + return ResponseUtil.success(msg=delete_{{ businessName }}_result.message) + + +@{{ businessName }}Controller.get( + '/{% raw %}{{% endraw %}{{ pk_field }}{% raw %}}{% endraw %}', response_model={{ BusinessName }}Model, dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:query'))] +) +async def query_detail_{{ moduleName }}_{{ businessName }}(request: Request, {{ pk_field }}: int, query_db: AsyncSession = Depends(get_db)): + {{ businessName }}_detail_result = await {{ BusinessName }}Service.{{ businessName }}_detail_services(query_db, {{ pk_field }}) + logger.info(f'获取{{ pk_field }}为{% raw %}{{% endraw %}{{ pk_field }}{% raw %}}{% endraw %}的信息成功') + + return ResponseUtil.success(data={{ businessName }}_detail_result) + + +@{{ businessName }}Controller.post('/export', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:export'))]) +@Log(title='{{ functionName }}', business_type=BusinessType.EXPORT) +async def export_{{ moduleName }}_{{ businessName }}_list( + request: Request, + {{ businessName }}_page_query: {{ BusinessName }}PageQueryModel = Form(), + query_db: AsyncSession = Depends(get_db), +): + # 获取全量数据 + {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=False) + {{ businessName }}_export_result = await {{ BusinessName }}Service.export_{{ businessName }}_list_services({{ businessName }}_query_result) + logger.info('导出成功') + + return ResponseUtil.streaming(data=bytes2file_response({{ businessName }}_export_result)) diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 new file mode 100644 index 0000000..b1b99f5 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 @@ -0,0 +1,139 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +from datetime import datetime, time +from sqlalchemy import delete, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from {{ packageName }}.entity.do.{{ businessName }}_do import {{ ClassName }} +from {{ packageName }}.entity.vo.{{ businessName }}_vo import {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +from utils.page_util import PageUtil + + +class {{ BusinessName }}Dao: + """ + {{ functionName }}模块数据库操作层 + """ + + @classmethod + async def get_{{ businessName }}_detail_by_id(cls, db: AsyncSession, {{ pk_field }}: int): + """ + 根据{{ functionName }}id获取{{ functionName }}详细信息 + + :param db: orm对象 + :param {{ pk_field }}: 通知公告id + :return: {{ functionName }}信息对象 + """ + {{ businessName }}_info = (await db.execute(select({{ ClassName }}).where({{ ClassName }}.{{ pk_field }} == {{ pk_field }}))).scalars().first() + + return {{ businessName }}_info + + @classmethod + async def get_{{ businessName }}_detail_by_info(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 根据{{ functionName }}参数获取{{ functionName }}信息 + + :param db: orm对象 + :param {{ businessName }}: {{ functionName }}参数对象 + :return: {{ functionName }}信息对象 + """ + {{ businessName }}_info = ( + ( + await db.execute( + select({{ ClassName }}).where( + {% for column in columns %} + {% if column.required %} + {{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }}, + {% endif %} + {% endfor %} + ) + ) + ) + .scalars() + .first() + ) + + return {{ businessName }}_info + + @classmethod + async def get_{{ businessName }}_list(cls, db: AsyncSession, query_object: {{ BusinessName }}PageQueryModel, is_page: bool = False): + """ + 根据查询参数获取{{ functionName }}列表信息 + + :param db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: {{ functionName }}列表信息对象 + """ + query = ( + select({{ ClassName }}) + .where( + {% for column in columns %} + {% set field = column.python_field | camel_to_snake %} + {% if column.query %} + {% if column.query_type == "EQ" %} + {{ ClassName }}.{{ field }} == query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "NE" %} + {{ ClassName }}.{{ field }} != query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "GT" %} + {{ ClassName }}.{{ field }} > query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "GTE" %} + {{ ClassName }}.{{ field }} >= query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LT" %} + {{ ClassName }}.{{ field }} < query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LTE" %} + {{ ClassName }}.{{ field }} <= query_object.{{ field }} if query_object.{{ field }} else True, + {% elif column.query_type == "LIKE" %} + {{ ClassName }}.{{ field }}.like(f'%{% raw %}{{% endraw %}query_object.{{ field }}{% raw %}}{% endraw %}%') if query_object.{{ field }} else True, + {% elif column.query_type == "BETWEEN" %} + {{ ClassName }}.{{ field }}.between( + datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), + datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), + ) + if query_object.begin_time and query_object.end_time + else True, + {% endif %} + {% endif %} + {% endfor %} + ) + .order_by({{ ClassName }}.{{ pk_field }}) + .distinct() + ) + {{ businessName }}_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + + return {{ businessName }}_list + + @classmethod + async def add_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 新增{{ functionName }}数据库操作 + + :param db: orm对象 + :param notice: {{ functionName }}对象 + :return: + """ + db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump()) + db.add(db_{{ businessName }}) + await db.flush() + + return db_{{ businessName }} + + @classmethod + async def edit_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: dict): + """ + 编辑{{ functionName }}数据库操作 + + :param db: orm对象 + :param notice: 需要更新的{{ functionName }}字典 + :return: + """ + await db.execute(update({{ ClassName }}), [{{ businessName }}]) + + @classmethod + async def delete_{{ businessName }}_dao(cls, db: AsyncSession, {{ businessName }}: {{ BusinessName }}Model): + """ + 删除{{ functionName }}数据库操作 + + :param db: orm对象 + :param notice: {{ functionName }}对象 + :return: + """ + await db.execute(delete({{ ClassName }}).where({{ ClassName }}.{{ pk_field }}.in_([{{ businessName }}.{{ pk_field }}]))) diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 new file mode 100644 index 0000000..404d76d --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 @@ -0,0 +1,40 @@ +from datetime import datetime +from sqlalchemy import Column, DateTime, Integer, String +{% if table.sub %} +from sqlalchemy.orm import relationship +{% endif %} +from config.database import Base + + +class {{ ClassName }}(Base): + """ + {{ functionName }}表 + """ + + __tablename__ = '{{ tableName }}' + + {% for column in columns %} + {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required %}nullable=True{% else %}nullable=False{% endif %}, comment='{{ column.column_comment }}') + {% endfor %} + + {% if table.sub %} + {{ table.sub_table.business_name }} = relationship('{{ table.sub_table.class_name }}', back_populates='{{ businessName }}') + {% endif %} + + +{% if table.sub %} +class {{ table.sub_table.class_name }}(Base): + """ + {{ table.sub_table.function_name }}表 + """ + + __tablename__ = '{{ table.sub_table.table_name }}' + + {% for column in table.sub_table.columns %} + {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required %}nullable=True{% else %}nullable=False{% endif %}, comment='{{ column.column_comment }}') + {% endfor %} + + {% if table.sub %} + {{ businessName }} = relationship('{{ ClassName }}', back_populates='{{ table.sub_table.business_name }}') + {% endif %} +{% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 new file mode 100644 index 0000000..1a2ad71 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 @@ -0,0 +1,171 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pk_field_comment = pkColumn.comment %} +from sqlalchemy.ext.asyncio import AsyncSession +from typing import List +from config.constant import CommonConstant +from exceptions.exception import ServiceException +from {{ packageName }}.dao.{{ businessName }}_dao import {{ BusinessName }}Dao +from module_admin.entity.vo.common_vo import CrudResponseModel +from {{ packageName }}.entity.vo.{{ businessName }}_vo import Delete{{ BusinessName }}Model, {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel +from utils.common_util import CamelCaseUtil +from utils.excel_util import ExcelUtil + + +class {{ BusinessName }}Service: + """ + {{ functionName }}模块服务层 + """ + + @classmethod + async def get_{{ businessName }}_list_services( + cls, query_db: AsyncSession, query_object: {{ BusinessName }}PageQueryModel, is_page: bool = False + ): + """ + 获取{{ functionName }}列表信息service + + :param query_db: orm对象 + :param query_object: 查询参数对象 + :param is_page: 是否开启分页 + :return: {{ functionName }}列表信息对象 + """ + {{ businessName }}_list_result = await {{ BusinessName }}Dao.get_{{ businessName }}_list(query_db, query_object, is_page) + + return {{ businessName }}_list_result + + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.required %} + @classmethod + async def check_{{ column.python_field | camel_to_snake }}_unique_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 检查{{ comment }}是否唯一service + + :param query_db: orm对象 + :param page_object: {{ functionName }}对象 + :return: 校验结果 + """ + {{ pk_field }} = -1 if page_object.{{ pk_field }} is None else page_object.{{ pk_field }} + {{ businessName }} = await {{ BusinessName }}Dao.get_{{ businessName }}_detail_by_info(query_db, {{ BusinessName }}Model({{ column.python_field }}=page_object.{{ column.python_field | camel_to_snake }})) + if {{ businessName }} and {{ businessName }}.{{ pk_field }} != {{ pk_field }}: + return CommonConstant.NOT_UNIQUE + return CommonConstant.UNIQUE + {% if not loop.last %}{{ "\n" }}{% endif %} + {% endif %} + {% endfor %} + + @classmethod + async def add_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 新增{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 新增{{ functionName }}对象 + :return: 新增{{ functionName }}校验结果 + """ + if not await cls.check_post_name_unique_services(query_db, page_object): + raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位名称已存在') + elif not await cls.check_post_code_unique_services(query_db, page_object): + raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位编码已存在') + else: + try: + await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e + + @classmethod + async def edit_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): + """ + 编辑{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 编辑{{ functionName }}对象 + :return: 编辑{{ functionName }}校验结果 + """ + edit_{{ businessName }} = page_object.model_dump(exclude_unset=True) + {{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.post_id) + if {{ businessName }}_info.post_id: + if not await cls.check_post_name_unique_services(query_db, page_object): + raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位名称已存在') + elif not await cls.check_post_code_unique_services(query_db, page_object): + raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位编码已存在') + else: + try: + await {{ BusinessName }}Dao.edit_{{ businessName }}_dao(query_db, edit_{{ businessName }}) + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='{{ functionName }}不存在') + + @classmethod + async def delete_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: Delete{{ BusinessName }}Model): + """ + 删除{{ functionName }}信息service + + :param query_db: orm对象 + :param page_object: 删除{{ functionName }}对象 + :return: 删除{{ functionName }}校验结果 + """ + if page_object.{{ pk_field }}s: + {{ pk_field }}_list = page_object.{{ pk_field }}s.split(',') + try: + for {{ pk_field }} in {{ pk_field }}_list: + {{ businessName }} = await cls.{{ businessName }}_detail_services(query_db, int({{ pk_field }})) + await {{ BusinessName }}Dao.delete_{{ businessName }}_dao(query_db, {{ BusinessName }}Model({{ pkField }}={{ pk_field }})) + await query_db.commit() + return CrudResponseModel(is_success=True, message='删除成功') + except Exception as e: + await query_db.rollback() + raise e + else: + raise ServiceException(message='传入{{ pk_field_comment }}为空') + + @classmethod + async def {{ businessName }}_detail_services(cls, query_db: AsyncSession, {{ pk_field }}: int): + """ + 获取{{ functionName }}详细信息service + + :param query_db: orm对象 + :param post_id: 岗位id + :return: 岗位id对应的信息 + """ + {{ businessName }} = await {{ BusinessName }}Dao.get_{{ businessName }}_detail_by_id(query_db, {{ pk_field }}={{ pk_field }}) + if {{ businessName }}: + result = {{ BusinessName }}Model(**CamelCaseUtil.transform_result({{ businessName }})) + else: + result = {{ BusinessName }}Model(**dict()) + + return result + + @staticmethod + async def export_{{ businessName }}_list_services({{ businessName }}_list: List): + """ + 导出{{ functionName }}信息service + + :param {{ businessName }}_list: {{ functionName }}信息列表 + :return: {{ functionName }}信息对应excel的二进制数据 + """ + # 创建一个映射字典,将英文键映射到中文键 + mapping_dict = { + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + '{{ column.python_field }}': '{{ comment }}', + {% endfor %} + } + + for item in {{ businessName }}_list: + if item.get('status') == '0': + item['status'] = '正常' + else: + item['status'] = '停用' + binary_data = ExcelUtil.export_list2excel({{ businessName }}_list, mapping_dict) + + return binary_data diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 new file mode 100644 index 0000000..cd07326 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 @@ -0,0 +1,72 @@ +{% set pkField = pkColumn.python_field %} +{% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pk_field_comment = pkColumn.comment %} +from datetime import datetime +from pydantic import BaseModel, ConfigDict, Field +from pydantic.alias_generators import to_camel +from pydantic_validation_decorator import NotBlank +from typing import Literal, Optional +from module_admin.annotation.pydantic_annotation import as_query + + +class {{ BusinessName }}Model(BaseModel): + """ + {{ functionName }}表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for column in columns %} + {{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}') + {% endfor %} + + {% for column in columns %} + {% if column.required %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + @NotBlank(field_name='{{ column.column_name }}', message='{{ comment }}不能为空') + def get_{{ column.column_name }}(self): + return self.{{ column.column_name }} + {% endif %} + {% endfor %} + + def validate_fields(self): + {% set vo_field_required = namespace(has_required=False) %} + {% for column in columns %} + {% if column.required %} + self.get_{{ column.column_name }}() + {% set vo_field_required.has_required = True %} + {% endif %} + {% endfor %} + {% if not vo_field_required.has_required %} + pass + {% endif %} + + +class {{ BusinessName }}QueryModel({{ BusinessName }}Model): + """ + {{ functionName }}不分页查询模型 + """ + + begin_time: Optional[str] = Field(default=None, description='开始时间') + end_time: Optional[str] = Field(default=None, description='结束时间') + + +@as_query +class {{ BusinessName }}PageQueryModel({{ BusinessName }}QueryModel): + """ + {{ functionName }}分页查询模型 + """ + + page_num: int = Field(default=1, description='当前页码') + page_size: int = Field(default=10, description='每页记录数') + + +class Delete{{ BusinessName }}Model(BaseModel): + """ + 删除{{ functionName }}模型 + """ + + model_config = ConfigDict(alias_generator=to_camel) + + {{ pk_field }}s: str = Field(description='需要删除的{{ pk_field_comment }}') diff --git a/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 new file mode 100644 index 0000000..809851c --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/sql/sql.jinja2 @@ -0,0 +1,22 @@ +-- 菜单 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}', '{{ parentMenuId }}', '1', '{{ businessName }}', '{{ moduleName }}/{{ businessName }}/index', 1, 0, 'C', '0', '0', '{{ permissionPrefix }}:list', '#', 'admin', sysdate(), '', null, '{{ functionName }}菜单'); + +-- 按钮父菜单ID +SELECT @parentId := LAST_INSERT_ID(); + +-- 按钮 SQL +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}查询', @parentId, '1', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:query', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}新增', @parentId, '2', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:add', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}修改', @parentId, '3', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:edit', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}删除', @parentId, '4', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:remove', '#', 'admin', sysdate(), '', null, ''); + +insert into sys_menu (menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) +values('{{ functionName }}导出', @parentId, '5', '#', '', 1, 0, 'F', '0', '0', '{{ permissionPrefix }}:export', '#', 'admin', sysdate(), '', null, ''); \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 new file mode 100644 index 0000000..e69de29 diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 new file mode 100644 index 0000000..31b3cf8 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 @@ -0,0 +1,572 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/server.py b/ruoyi-fastapi-backend/server.py index 00b4661..5c8ad9c 100644 --- a/ruoyi-fastapi-backend/server.py +++ b/ruoyi-fastapi-backend/server.py @@ -22,6 +22,7 @@ from module_admin.controller.post_controler import postController from module_admin.controller.role_controller import roleController from module_admin.controller.server_controller import serverController from module_admin.controller.user_controller import userController +from module_generator.controller.gen_controller import genController from sub_applications.handle import handle_sub_applications from utils.common_util import worship from utils.log_util import logger @@ -77,6 +78,7 @@ controller_list = [ {'router': serverController, 'tags': ['系统监控-菜单管理']}, {'router': cacheController, 'tags': ['系统监控-缓存监控']}, {'router': commonController, 'tags': ['通用模块']}, + {'router': genController, 'tags': ['代码生成']}, ] for controller in controller_list: diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql index 79b7767..3b33738 100644 --- a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql @@ -913,8 +913,8 @@ create table gen_table_column ( column_name varchar(200), column_comment varchar(500), column_type varchar(100), - java_type varchar(500), - java_field varchar(200), + python_type varchar(500), + python_field varchar(200), is_pk char(1), is_increment char(1), is_required char(1), @@ -937,8 +937,8 @@ comment on column gen_table_column.table_id is '归属表编号'; comment on column gen_table_column.column_name is '列名称'; comment on column gen_table_column.column_comment is '列描述'; comment on column gen_table_column.column_type is '列类型'; -comment on column gen_table_column.java_type is 'JAVA类型'; -comment on column gen_table_column.java_field is 'JAVA字段名'; +comment on column gen_table_column.python_type is 'PYTHON类型'; +comment on column gen_table_column.python_field is 'PYTHON字段名'; comment on column gen_table_column.is_pk is '是否主键(1是)'; comment on column gen_table_column.is_increment is '是否自增(1是)'; comment on column gen_table_column.is_required is '是否必填(1是)'; diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql index ef5a8de..294e21c 100644 --- a/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql @@ -691,8 +691,8 @@ create table gen_table_column ( column_name varchar(200) comment '列名称', column_comment varchar(500) comment '列描述', column_type varchar(100) comment '列类型', - java_type varchar(500) comment 'JAVA类型', - java_field varchar(200) comment 'JAVA字段名', + python_type varchar(500) comment 'PYTHON类型', + python_field varchar(200) comment 'PYTHON字段名', is_pk char(1) comment '是否主键(1是)', is_increment char(1) comment '是否自增(1是)', is_required char(1) comment '是否必填(1是)', diff --git a/ruoyi-fastapi-backend/utils/common_util.py b/ruoyi-fastapi-backend/utils/common_util.py index 86f502e..b56000e 100644 --- a/ruoyi-fastapi-backend/utils/common_util.py +++ b/ruoyi-fastapi-backend/utils/common_util.py @@ -7,6 +7,7 @@ from openpyxl.styles import Alignment, PatternFill from openpyxl.utils import get_column_letter from openpyxl.worksheet.datavalidation import DataValidation from sqlalchemy.engine.row import Row +from sqlalchemy.orm.collections import InstrumentedList from typing import Any, Dict, List, Literal, Union from config.database import Base from config.env import CachePathConfig @@ -58,6 +59,9 @@ class SqlalchemyUtil: if isinstance(obj, Base): base_dict = obj.__dict__.copy() base_dict.pop('_sa_instance_state', None) + for name, value in base_dict.items(): + if isinstance(value, InstrumentedList): + base_dict[name] = cls.serialize_result(value, 'snake_to_camel') elif isinstance(obj, dict): base_dict = obj.copy() if transform_case == 'snake_to_camel': diff --git a/ruoyi-fastapi-backend/utils/gen_util.py b/ruoyi-fastapi-backend/utils/gen_util.py new file mode 100644 index 0000000..d084918 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/gen_util.py @@ -0,0 +1,152 @@ +import re +from datetime import datetime +from typing import List +from config.constant import GenConstant +from config.env import GenConfig +from module_generator.entity.vo.gen_vo import GenTableColumnModel, GenTableModel +from utils.string_util import StringUtil + + +class GenUtils: + """代码生成器工具类""" + + @classmethod + def init_table(cls, gen_table: GenTableModel, oper_name: str) -> None: + """初始化表信息""" + gen_table.class_name = cls.convert_class_name(gen_table.table_name) + gen_table.package_name = GenConfig.package_name + gen_table.module_name = cls.get_module_name(GenConfig.package_name) + gen_table.business_name = cls.get_business_name(gen_table.table_name) + gen_table.function_name = cls.replace_text(gen_table.table_comment) + gen_table.function_author = GenConfig.author + gen_table.create_by = oper_name + gen_table.create_time = datetime.now() + gen_table.update_by = oper_name + gen_table.update_time = datetime.now() + + @classmethod + def init_column_field(cls, column: GenTableColumnModel, table: GenTableModel) -> None: + """初始化列属性字段""" + data_type = cls.get_db_type(column.column_type) + column_name = column.column_name + column.table_id = table.table_id + column.create_by = table.create_by + # 设置Python字段名 + column.python_field = cls.to_camel_case(column_name) + # 设置默认类型 + column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.MYSQL_TO_PYTHON_TYPE_MAPPING, data_type) + column.query_type = GenConstant.QUERY_EQ + + if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains( + GenConstant.COLUMNTYPE_TEXT, data_type + ): + # 字符串长度超过500设置为文本域 + column_length = cls.get_column_length(column.column_type) + html_type = ( + GenConstant.HTML_TEXTAREA + if column_length >= 500 or cls.arrays_contains(GenConstant.COLUMNTYPE_TEXT, data_type) + else GenConstant.HTML_INPUT + ) + column.html_type = html_type + elif cls.arrays_contains(GenConstant.COLUMNTYPE_TIME, data_type): + column.html_type = GenConstant.HTML_DATETIME + elif cls.arrays_contains(GenConstant.COLUMNTYPE_NUMBER, data_type): + column.html_type = GenConstant.HTML_INPUT + + # 插入字段(默认所有字段都需要插入) + column.is_insert = GenConstant.REQUIRE + + # 编辑字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.is_pk: + column.is_edit = GenConstant.REQUIRE + # 列表字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.is_pk: + column.is_list = GenConstant.REQUIRE + # 查询字段 + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.is_pk: + column.is_query = GenConstant.REQUIRE + + # 查询字段类型 + if column_name.lower().endswith('name'): + column.query_type = GenConstant.QUERY_LIKE + # 状态字段设置单选框 + if column_name.lower().endswith('status'): + column.html_type = GenConstant.HTML_RADIO + # 类型&性别字段设置下拉框 + elif column_name.lower().endswith('type') or column_name.lower().endswith('sex'): + column.html_type = GenConstant.HTML_SELECT + # 图片字段设置图片上传控件 + elif column_name.lower().endswith('image'): + column.html_type = GenConstant.HTML_IMAGE_UPLOAD + # 文件字段设置文件上传控件 + elif column_name.lower().endswith('file'): + column.html_type = GenConstant.HTML_FILE_UPLOAD + # 内容字段设置富文本控件 + elif column_name.lower().endswith('content'): + column.html_type = GenConstant.HTML_EDITOR + + @classmethod + def arrays_contains(cls, arr: List[str], target_value: str) -> bool: + """校验数组是否包含指定值""" + return target_value in arr + + @classmethod + def get_module_name(cls, package_name: str) -> str: + """获取模块名""" + return package_name.split('.')[-1] + + @classmethod + def get_business_name(cls, table_name: str) -> str: + """获取业务名""" + return table_name.split('_')[-1] + + @classmethod + def convert_class_name(cls, table_name: str) -> str: + """表名转换成Python类名""" + auto_remove_pre = GenConfig.auto_remove_pre + table_prefix = GenConfig.table_prefix + if auto_remove_pre and table_prefix: + search_list = table_prefix.split(',') + table_name = cls.replace_first(table_name, search_list) + return StringUtil.convert_to_camel_case(table_name) + + @classmethod + def replace_first(cls, replacement: str, search_list: List[str]) -> str: + """批量替换前缀""" + for search_string in search_list: + if replacement.startswith(search_string): + return replacement.replace(search_string, '', 1) + return replacement + + @classmethod + def replace_text(cls, text: str) -> str: + """关键字替换""" + return re.sub(r'(?:表|若依)', '', text) + + @classmethod + def get_db_type(cls, column_type: str) -> str: + """获取数据库类型字段""" + if '(' in column_type: + return column_type.split('(')[0] + return column_type + + @classmethod + def get_column_length(cls, column_type: str) -> int: + """获取字段长度""" + if '(' in column_type: + length = len(column_type.split('(')[1].split(')')[0]) + return length + return 0 + + @classmethod + def split_column_type(cls, column_type: str) -> List[str]: + """拆分列类型""" + if '(' in column_type and ')' in column_type: + return column_type.split('(')[1].split(')')[0].split(',') + return [] + + @classmethod + def to_camel_case(cls, text: str) -> str: + """将字符串转换为驼峰命名""" + parts = text.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) diff --git a/ruoyi-fastapi-backend/utils/string_util.py b/ruoyi-fastapi-backend/utils/string_util.py index 962afdc..7196bcf 100644 --- a/ruoyi-fastapi-backend/utils/string_util.py +++ b/ruoyi-fastapi-backend/utils/string_util.py @@ -1,4 +1,4 @@ -from typing import List +from typing import Dict, List from config.constant import CommonConstant @@ -36,6 +36,16 @@ class StringUtil: """ return string is None or len(string) == 0 + @classmethod + def is_not_empty(cls, string: str) -> bool: + """ + 校验字符串是否不是''和None + + :param string: 需要校验的字符串 + :return: 校验结果 + """ + return not cls.is_empty(string) + @classmethod def is_http(cls, link: str): """ @@ -49,7 +59,7 @@ class StringUtil: @classmethod def contains_ignore_case(cls, search_str: str, compare_str: str): """ - 查找指定字符串是否包含指定字符串同时串忽略大小写 + 查找指定字符串是否包含指定字符串同时忽略大小写 :param search_str: 查找的字符串 :param compare_str: 比对的字符串 @@ -62,15 +72,40 @@ class StringUtil: @classmethod def contains_any_ignore_case(cls, search_str: str, compare_str_list: List[str]): """ - 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时串忽略大小写 + 查找指定字符串是否包含指定字符串列表中的任意一个字符串同时忽略大小写 :param search_str: 查找的字符串 :param compare_str_list: 比对的字符串列表 :return: 查找结果 """ if search_str and compare_str_list: - for compare_str in compare_str_list: - return cls.contains_ignore_case(search_str, compare_str) + return any([cls.contains_ignore_case(search_str, compare_str) for compare_str in compare_str_list]) + return False + + @classmethod + def equals_ignore_case(cls, search_str: str, compare_str: str): + """ + 比较两个字符串是否相等同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str: 比对的字符串 + :return: 比较结果 + """ + if search_str and compare_str: + return search_str.lower() == compare_str.lower() + return False + + @classmethod + def equals_any_ignore_case(cls, search_str: str, compare_str_list: List[str]): + """ + 比较指定字符串是否与指定字符串列表中的任意一个字符串相等同时忽略大小写 + + :param search_str: 查找的字符串 + :param compare_str_list: 比对的字符串列表 + :return: 比较结果 + """ + if search_str and compare_str_list: + return any([cls.equals_ignore_case(search_str, compare_str) for compare_str in compare_str_list]) return False @classmethod @@ -98,3 +133,38 @@ class StringUtil: if search_str and compare_str_list: return any([cls.startswith_case(search_str, compare_str) for compare_str in compare_str_list]) return False + + @classmethod + def convert_to_camel_case(cls, name: str) -> str: + """ + 将下划线大写方式命名的字符串转换为驼峰式。如果转换前的下划线大写方式命名的字符串为空,则返回空字符串 + + :param name: 转换前的下划线大写方式命名的字符串 + :return: 转换后的驼峰式命名的字符串 + """ + if not name: + return '' + if '_' not in name: + return name[0].upper() + name[1:] + parts = name.split('_') + result = [] + for part in parts: + if not part: + continue + result.append(part[0].upper() + part[1:].lower()) + return ''.join(result) + + @classmethod + def get_mapping_value_by_key_ignore_case(cls, mapping: Dict[str, str], key: str) -> str: + """ + 根据忽略大小写的键获取字典中的对应的值 + + param mapping: 字典 + param key: 字典的键 + :return: 字典键对应的值 + """ + for k, v in mapping.items(): + if key.lower() == k.lower(): + return v + + return '' diff --git a/ruoyi-fastapi-backend/utils/template_util.py b/ruoyi-fastapi-backend/utils/template_util.py new file mode 100644 index 0000000..47c4ac3 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/template_util.py @@ -0,0 +1,296 @@ +import json +import os +from datetime import datetime +from jinja2 import Environment, FileSystemLoader +from typing import Dict, List, Set +from config.constant import GenConstant +from module_generator.entity.vo.gen_vo import GenTableModel, GenTableColumnModel +from utils.common_util import CamelCaseUtil, SnakeCaseUtil +from utils.string_util import StringUtil + + +class TemplateInitializer: + """ + 模板引擎初始化类 + """ + + @classmethod + def init_jinja2(cls): + """ + 初始化 Jinja2 模板引擎 + + :return: Jinja2 环境对象 + """ + try: + template_dir = os.path.join(os.getcwd(), 'module_generator', 'templates') + env = Environment( + loader=FileSystemLoader(template_dir), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + ) + env.filters.update( + { + 'camel_to_snake': SnakeCaseUtil.camel_to_snake, + 'snake_to_camel': CamelCaseUtil.snake_to_camel, + 'get_sqlalchemy_type': TemplateUtils.get_sqlalchemy_type, + } + ) + return env + except Exception as e: + raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}') + + +class TemplateUtils: + """ + 模板工具类 + """ + + # 项目路径 + FRONTEND_PROJECT_PATH = 'frontend' + BACKEND_PROJECT_PATH = 'backend' + DEFAULT_PARENT_MENU_ID = '3' + + @classmethod + def prepare_context(cls, gen_table: GenTableModel): + """ + 准备模板变量 + :param gen_table: 生成表的配置信息 + :return: 模板上下文字典 + """ + class_name = gen_table.class_name + module_name = gen_table.module_name + business_name = gen_table.business_name + package_name = gen_table.package_name + tpl_category = gen_table.tpl_category + function_name = gen_table.function_name + + context = { + 'tplCategory': tpl_category, + 'tableName': gen_table.table_name, + 'functionName': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】', + 'ClassName': class_name, + 'className': class_name.lower(), + 'moduleName': module_name, + 'BusinessName': business_name.capitalize(), + 'businessName': business_name, + 'basePackage': cls.get_package_prefix(package_name), + 'packageName': package_name, + 'author': gen_table.function_author, + 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'pkColumn': gen_table.pk_column, + 'importList': cls.get_import_list(gen_table), + 'permissionPrefix': cls.get_permission_prefix(module_name, business_name), + 'columns': gen_table.columns, + 'table': gen_table, + 'dicts': cls.get_dicts(gen_table), + } + + # 设置菜单、树形结构、子表的上下文 + cls.set_menu_context(context, gen_table) + if tpl_category == GenConstant.TPL_TREE: + cls.set_tree_context(context, gen_table) + if tpl_category == GenConstant.TPL_SUB: + cls.set_sub_context(context, gen_table) + + return context + + @classmethod + def set_menu_context(cls, context: Dict, gen_table: GenTableModel): + """设置菜单上下文""" + options = gen_table.options + params_obj = json.loads(options) + context['parentMenuId'] = cls.get_parent_menu_id(params_obj) + + @classmethod + def set_tree_context(cls, context: Dict, gen_table: GenTableModel): + """设置树形结构上下文""" + options = gen_table.options + params_obj = json.loads(options) + context['treeCode'] = cls.get_tree_code(params_obj) + context['treeParentCode'] = cls.get_tree_parent_code(params_obj) + context['treeName'] = cls.get_tree_name(params_obj) + context['expandColumn'] = cls.get_expand_column(gen_table) + + @classmethod + def set_sub_context(cls, context: Dict, gen_table: GenTableModel): + """设置子表上下文""" + sub_table = gen_table.sub_table + sub_table_name = gen_table.sub_table_name + sub_table_fk_name = gen_table.sub_table_fk_name + sub_class_name = sub_table.class_name + sub_table_fk_class_name = StringUtil.convert_to_camel_case(sub_table_fk_name) + context['subTable'] = sub_table + context['subTableName'] = sub_table_name + context['subTableFkName'] = sub_table_fk_name + context['subTableFkClassName'] = sub_table_fk_class_name + context['subTableFkclassName'] = sub_table_fk_class_name.lower() + context['subClassName'] = sub_class_name + context['subclassName'] = sub_class_name.lower() + context['subImportList'] = cls.get_import_list(sub_table) + + @classmethod + def get_template_list(cls, tpl_category, tpl_web_type): + """获取模板列表""" + use_web_type = 'vue' + if tpl_web_type == 'element-plus': + use_web_type = 'vue/v3' + templates = [ + 'python/controller.py.jinja2', + 'python/dao.py.jinja2', + 'python/do.py.jinja2', + 'python/service.py.jinja2', + 'python/vo.py.jinja2', + 'sql/sql.jinja2', + 'js/api.js.jinja2', + ] + if tpl_category == GenConstant.TPL_CRUD: + templates.append(f'{use_web_type}/index.vue.jinja2') + elif tpl_category == GenConstant.TPL_TREE: + templates.append(f'{use_web_type}/index-tree.vue.jinja2') + elif tpl_category == GenConstant.TPL_SUB: + templates.append(f'{use_web_type}/index.vue.jinja2') + # templates.append('python/sub-domain.python.jinja2') + return templates + + @classmethod + def get_file_name(cls, template, gen_table: GenTableModel): + """根据模板生成文件名""" + package_name = gen_table.package_name + module_name = gen_table.module_name + business_name = gen_table.business_name + + vue_path = cls.FRONTEND_PROJECT_PATH + python_path = f"{cls.BACKEND_PROJECT_PATH}/{package_name.replace('.', '/')}" + + if 'controller.py.jinja2' in template: + return f'{python_path}/controller/{business_name}_controller.py' + elif 'dao.py.jinja2' in template: + return f'{python_path}/dao/{business_name}_dao.py' + elif 'do.py.jinja2' in template: + return f'{python_path}/entity/do/{business_name}_do.py' + elif 'service.py.jinja2' in template: + return f'{python_path}/service/{business_name}_service.py' + elif 'vo.py.jinja2' in template: + return f'{python_path}/entity/vo/{business_name}_vo.py' + elif 'sql.jinja2' in template: + return f'{cls.BACKEND_PROJECT_PATH}/sql/{business_name}_menu.sql' + elif 'api.js.jinja2' in template: + return f'{vue_path}/api/{module_name}/{business_name}.js' + elif 'index.vue.jinja2' in template or 'index-tree.vue.j2' in template: + return f'{vue_path}/views/{module_name}/{business_name}/index.vue' + return '' + + @classmethod + def get_package_prefix(cls, package_name: str): + """获取包前缀""" + return package_name[: package_name.rfind('.')] + + @classmethod + def get_import_list(cls, gen_table: GenTableModel): + """获取导入包列表""" + columns = gen_table.columns or [] + sub_gen_table = gen_table.sub_table + import_list = set() + if sub_gen_table is not None: + import_list.add('python.util.List') + for column in columns: + if not column.super_column and column.python_type in GenConstant.TYPE_DATE: + import_list.add(f'from datetime import {column.python_type}') + elif not column.super_column and column.python_type == GenConstant.TYPE_DECIMAL: + import_list.add('from decimal import Decimal') + return list(import_list) + + @classmethod + def get_dicts(cls, gen_table: GenTableModel): + """获取字典列表""" + columns = gen_table.columns or [] + dicts = set() + cls.add_dicts(dicts, columns) + if gen_table.sub_table is not None: + cls.add_dicts(dicts, gen_table.sub_table.columns) + return ', '.join(dicts) + + @classmethod + def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnModel]): + """添加字典列表""" + for column in columns: + if ( + column.super_column + and StringUtil.is_not_empty(column.dict_type) + and StringUtil.equals_any_ignore_case( + column.html_type, [GenConstant.HTML_SELECT, GenConstant.HTML_RADIO, GenConstant.HTML_CHECKBOX] + ) + ): + dicts.add(f"'{column.dict_type}'") + + @classmethod + def get_permission_prefix(cls, module_name: str, business_name: str): + """获取权限前缀""" + return f'{module_name}:{business_name}' + + @classmethod + def get_parent_menu_id(cls, params_obj): + """获取上级菜单ID""" + if params_obj and params_obj.get(GenConstant.PARENT_MENU_ID): + return params_obj.get(GenConstant.PARENT_MENU_ID) + return cls.DEFAULT_PARENT_MENU_ID + + @classmethod + def get_tree_code(cls, params_obj: Dict): + """获取树编码""" + if GenConstant.TREE_CODE in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_CODE)) + return '' + + @classmethod + def get_tree_parent_code(cls, params_obj: Dict): + """获取树父编码""" + if GenConstant.TREE_PARENT_CODE in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_PARENT_CODE)) + return '' + + @classmethod + def get_tree_name(cls, params_obj: Dict): + """获取树名称""" + if GenConstant.TREE_NAME in params_obj: + return cls.to_camel_case(params_obj.get(GenConstant.TREE_NAME)) + return '' + + @classmethod + def get_expand_column(cls, gen_table: GenTableModel): + """获取展开列""" + options = gen_table.options + params_obj = json.loads(options) + tree_name = params_obj.get(GenConstant.TREE_NAME) + num = 0 + for column in gen_table.columns or []: + if column.list: + num += 1 + if column.column_name == tree_name: + break + return num + + @classmethod + def to_camel_case(cls, text: str) -> str: + """将字符串转换为驼峰命名""" + parts = text.split('_') + return parts[0] + ''.join(word.capitalize() for word in parts[1:]) + + @classmethod + def get_sqlalchemy_type(cls, column_type: str): + if '(' in column_type: + column_type_list = column_type.split('(') + sqlalchemy_type = ( + StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.MYSQL_TO_SQLALCHEMY_TYPE_MAPPING, column_type_list[0] + ) + + '(' + + column_type_list[1] + ) + else: + sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.MYSQL_TO_SQLALCHEMY_TYPE_MAPPING, column_type + ) + + return sqlalchemy_type diff --git a/ruoyi-fastapi-frontend/src/api/tool/gen.js b/ruoyi-fastapi-frontend/src/api/tool/gen.js index 4506927..5728980 100644 --- a/ruoyi-fastapi-frontend/src/api/tool/gen.js +++ b/ruoyi-fastapi-frontend/src/api/tool/gen.js @@ -1,76 +1,85 @@ -import request from '@/utils/request' - -// 查询生成表数据 -export function listTable(query) { - return request({ - url: '/tool/gen/list', - method: 'get', - params: query - }) -} -// 查询db数据库列表 -export function listDbTable(query) { - return request({ - url: '/tool/gen/db/list', - method: 'get', - params: query - }) -} - -// 查询表详细信息 -export function getGenTable(tableId) { - return request({ - url: '/tool/gen/' + tableId, - method: 'get' - }) -} - -// 修改代码生成信息 -export function updateGenTable(data) { - return request({ - url: '/tool/gen', - method: 'put', - data: data - }) -} - -// 导入表 -export function importTable(data) { - return request({ - url: '/tool/gen/importTable', - method: 'post', - params: data - }) -} - -// 预览生成代码 -export function previewTable(tableId) { - return request({ - url: '/tool/gen/preview/' + tableId, - method: 'get' - }) -} - -// 删除表数据 -export function delTable(tableId) { - return request({ - url: '/tool/gen/' + tableId, - method: 'delete' - }) -} - -// 生成代码(自定义路径) -export function genCode(tableName) { - return request({ - url: '/tool/gen/genCode/' + tableName, - method: 'get' - }) -} - -// 同步数据库 -export function synchDb(tableName) { - return request({ - url: '/tool/gen/synchDb/' + tableName, - method: 'get' - }) -} +import request from '@/utils/request' + +// 查询生成表数据 +export function listTable(query) { + return request({ + url: '/tool/gen/list', + method: 'get', + params: query + }) +} +// 查询db数据库列表 +export function listDbTable(query) { + return request({ + url: '/tool/gen/db/list', + method: 'get', + params: query + }) +} + +// 查询表详细信息 +export function getGenTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'get' + }) +} + +// 修改代码生成信息 +export function updateGenTable(data) { + return request({ + url: '/tool/gen', + method: 'put', + data: data + }) +} + +// 导入表 +export function importTable(data) { + return request({ + url: '/tool/gen/importTable', + method: 'post', + params: data + }) +} + +// 创建表 +export function createTable(data) { + return request({ + url: '/tool/gen/createTable', + method: 'post', + params: data + }) +} + +// 预览生成代码 +export function previewTable(tableId) { + return request({ + url: '/tool/gen/preview/' + tableId, + method: 'get' + }) +} + +// 删除表数据 +export function delTable(tableId) { + return request({ + url: '/tool/gen/' + tableId, + method: 'delete' + }) +} + +// 生成代码(自定义路径) +export function genCode(tableName) { + return request({ + url: '/tool/gen/genCode/' + tableName, + method: 'get' + }) +} + +// 同步数据库 +export function synchDb(tableName) { + return request({ + url: '/tool/gen/synchDb/' + tableName, + method: 'get' + }) +} diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/basicInfoForm.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/basicInfoForm.vue index 39c8515..025ae42 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/gen/basicInfoForm.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/basicInfoForm.vue @@ -1,48 +1,48 @@ - - - + + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue new file mode 100644 index 0000000..00fdfdb --- /dev/null +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/createTable.vue @@ -0,0 +1,46 @@ + + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue index ddcbfd5..0833de8 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue @@ -1,198 +1,202 @@ - - - + + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/genInfoForm.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/genInfoForm.vue index a75e3b8..926d268 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/gen/genInfoForm.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/genInfoForm.vue @@ -1,297 +1,306 @@ - - - + + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/importTable.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/importTable.vue index 33b5633..49d96c3 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/gen/importTable.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/importTable.vue @@ -1,118 +1,126 @@ - - - + + + diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/index.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/index.vue index 06e2d4c..de5fc75 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/gen/index.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/index.vue @@ -1,284 +1,310 @@ - - - + + + -- Gitee From fe41a207fa96b887a3a3824bc25c41a3ee0ef60c Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Sun, 16 Feb 2025 23:29:15 +0800 Subject: [PATCH 16/34] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_generator/dao/gen_dao.py | 70 ++- .../module_generator/service/gen_service.py | 112 ++--- .../templates/python/controller.py.jinja2 | 24 +- .../templates/python/dao.py.jinja2 | 14 +- .../templates/python/do.py.jinja2 | 17 +- .../templates/python/service.py.jinja2 | 11 +- .../templates/python/vo.py.jinja2 | 9 +- .../templates/vue/v3/index-tree.vue.jinja2 | 454 ++++++++++++++++++ .../templates/vue/v3/index.vue.jinja2 | 11 +- ruoyi-fastapi-backend/utils/gen_util.py | 92 +++- ruoyi-fastapi-backend/utils/template_util.py | 216 +++++++-- 11 files changed, 854 insertions(+), 176 deletions(-) diff --git a/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py index 8844ab2..c125741 100644 --- a/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py +++ b/ruoyi-fastapi-backend/module_generator/dao/gen_dao.py @@ -16,17 +16,17 @@ from utils.page_util import PageUtil class GenTableDao: """ - 代码生成表模块数据库操作层 + 代码生成业务表模块数据库操作层 """ @classmethod async def get_gen_table_by_id(cls, db: AsyncSession, table_id: int): """ - 根据表格id获取需要生成的表格信息 + 根据业务表id获取需要生成的业务表信息 :param db: orm对象 - :param table_id: 岗位id - :return: 需要生成的表格信息对象 + :param table_id: 业务表id + :return: 需要生成的业务表信息对象 """ gen_table_info = ( ( @@ -43,11 +43,11 @@ class GenTableDao: @classmethod async def get_gen_table_by_name(cls, db: AsyncSession, table_name: str): """ - 根据表格名称获取需要生成的表格信息 + 根据业务表名称获取需要生成的业务表信息 :param db: orm对象 - :param table_name: 表格名称 - :return: 需要生成的表格信息对象 + :param table_name: 业务表名称 + :return: 需要生成的业务表信息对象 """ gen_table_info = ( ( @@ -64,10 +64,10 @@ class GenTableDao: @classmethod async def get_gen_table_all(cls, db: AsyncSession): """ - 根据表格id获取需要生成的表格详细信息 + 获取所有业务表信息 :param db: orm对象 - :return: 需要生成的表格信息对象 + :return: 所有业务表信息 """ gen_table_all = (await db.execute(select(GenTable).options(selectinload(GenTable.columns)))).scalars().all() @@ -87,12 +87,12 @@ class GenTableDao: @classmethod async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): """ - 根据查询参数获取代码生成列表信息 + 根据查询参数获取代码生成业务表列表信息 :param db: orm对象 :param query_object: 查询参数对象 :param is_page: 是否开启分页 - :return: 代码生成列表信息对象 + :return: 代码生成业务表列表信息对象 """ query = ( select(GenTable) @@ -164,11 +164,10 @@ class GenTableDao: @classmethod async def get_gen_db_table_list_by_names(cls, db: AsyncSession, table_names: List[str]): """ - 根据查询参数获取数据库列表信息 + 根据业务表名称组获取数据库列表信息 :param db: orm对象 - :param query_object: 查询参数对象 - :param is_page: 是否开启分页 + :param table_names: 业务表名称组 :return: 数据库列表信息对象 """ query_sql = """ @@ -193,10 +192,10 @@ class GenTableDao: @classmethod async def add_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): """ - 新增岗位数据库操作 + 新增业务表数据库操作 :param db: orm对象 - :param post: 岗位对象 + :param gen_table: 业务表对象 :return: """ db_gen_table = GenTable(**GenTableBaseModel(**gen_table.model_dump(by_alias=True)).model_dump()) @@ -208,10 +207,10 @@ class GenTableDao: @classmethod async def edit_gen_table_dao(cls, db: AsyncSession, gen_table: dict): """ - 编辑岗位数据库操作 + 编辑业务表数据库操作 :param db: orm对象 - :param post: 需要更新的岗位字典 + :param gen_table: 需要更新的业务表字典 :return: """ await db.execute(update(GenTable), [GenTableBaseModel(**gen_table).model_dump()]) @@ -219,10 +218,10 @@ class GenTableDao: @classmethod async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): """ - 删除岗位数据库操作 + 删除业务表数据库操作 :param db: orm对象 - :param post: 岗位对象 + :param gen_table: 业务表对象 :return: """ await db.execute(delete(GenTable).where(GenTable.table_id.in_([gen_table.table_id]))) @@ -230,17 +229,17 @@ class GenTableDao: class GenTableColumnDao: """ - 代码生成列模块数据库操作层 + 代码生成业务表字段模块数据库操作层 """ @classmethod async def get_gen_table_column_list_by_table_id(cls, db: AsyncSession, table_id: int): """ - 根据表格id获取需要生成的列列表信息 + 根据业务表id获取需要生成的业务表字段列表信息 :param db: orm对象 - :param table_id: 表格id - :return: 需要生成的列列表信息对象 + :param table_id: 业务表id + :return: 需要生成的业务表字段列表信息对象 """ gen_table_column_list = ( (await db.execute(select(GenTableColumn).where(GenTableColumn.table_id == table_id))).scalars().all() @@ -251,12 +250,11 @@ class GenTableColumnDao: @classmethod async def get_gen_db_table_columns_by_name(cls, db: AsyncSession, table_name: str): """ - 根据查询参数获取数据库列表信息 + 根据业务表名称获取业务表字段列表信息 :param db: orm对象 - :param query_object: 查询参数对象 - :param is_page: 是否开启分页 - :return: 数据库列表信息对象 + :param table_name: 业务表名称 + :return: 业务表字段列表信息对象 """ query_sql = """ select @@ -292,10 +290,10 @@ class GenTableColumnDao: @classmethod async def add_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): """ - 新增岗位数据库操作 + 新增业务表字段数据库操作 :param db: orm对象 - :param post: 岗位对象 + :param gen_table_column: 岗位对象 :return: """ db_gen_table_column = GenTableColumn( @@ -309,10 +307,10 @@ class GenTableColumnDao: @classmethod async def edit_gen_table_column_dao(cls, db: AsyncSession, gen_table_column: dict): """ - 编辑岗位数据库操作 + 编辑业务表字段数据库操作 :param db: orm对象 - :param post: 需要更新的岗位字典 + :param gen_table_column: 需要更新的业务表字段字典 :return: """ await db.execute(update(GenTableColumn), [GenTableColumnBaseModel(**gen_table_column).model_dump()]) @@ -320,10 +318,10 @@ class GenTableColumnDao: @classmethod async def delete_gen_table_column_by_table_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): """ - 删除岗位数据库操作 + 通过业务表id删除业务表字段数据库操作 :param db: orm对象 - :param post: 岗位对象 + :param gen_table_column: 业务表字段对象 :return: """ await db.execute(delete(GenTableColumn).where(GenTableColumn.table_id.in_([gen_table_column.table_id]))) @@ -331,10 +329,10 @@ class GenTableColumnDao: @classmethod async def delete_gen_table_column_by_column_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): """ - 删除岗位数据库操作 + 通过业务字段id删除业务表字段数据库操作 :param db: orm对象 - :param post: 岗位对象 + :param post: 业务表字段对象 :return: """ await db.execute(delete(GenTableColumn).where(GenTableColumn.column_id.in_([gen_table_column.column_id]))) diff --git a/ruoyi-fastapi-backend/module_generator/service/gen_service.py b/ruoyi-fastapi-backend/module_generator/service/gen_service.py index 0879d31..9132e91 100644 --- a/ruoyi-fastapi-backend/module_generator/service/gen_service.py +++ b/ruoyi-fastapi-backend/module_generator/service/gen_service.py @@ -19,14 +19,13 @@ from module_generator.entity.vo.gen_vo import ( ) from module_generator.dao.gen_dao import GenTableColumnDao, GenTableDao from utils.common_util import CamelCaseUtil -from utils.excel_util import ExcelUtil from utils.gen_util import GenUtils from utils.template_util import TemplateInitializer, TemplateUtils class GenTableService: """ - 岗位管理模块服务层 + 代码生成业务表服务层 """ @classmethod @@ -34,12 +33,12 @@ class GenTableService: cls, query_db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False ): """ - 获取代码生成列表信息service + 获取代码生成业务表列表信息service :param query_db: orm对象 :param query_object: 查询参数对象 :param is_page: 是否开启分页 - :return: 代码生成列表信息对象 + :return: 代码生成业务列表信息对象 """ gen_table_list_result = await GenTableDao.get_gen_table_list(query_db, query_object, is_page) @@ -64,11 +63,10 @@ class GenTableService: @classmethod async def get_gen_db_table_list_by_name_services(cls, query_db: AsyncSession, table_names: List[str]): """ - 获取数据库列表信息service + 根据表名称组获取数据库列表信息service :param query_db: orm对象 - :param query_object: 查询参数对象 - :param is_page: 是否开启分页 + :param table_names: 表名称组 :return: 数据库列表信息对象 """ gen_db_table_list_result = await GenTableDao.get_gen_db_table_list_by_names(query_db, table_names) @@ -79,6 +77,14 @@ class GenTableService: async def import_gen_table_services( cls, query_db: AsyncSession, gen_table_list: List[GenTableModel], current_user: CurrentUserModel ): + """ + 导入表结构service + + :param query_db: orm对象 + :param gen_table_list: 导入表列表 + :param current_user: 当前用户信息对象 + :return: 导入结果 + """ try: for table in gen_table_list: table_name = table.table_name @@ -102,11 +108,11 @@ class GenTableService: @classmethod async def edit_gen_table_services(cls, query_db: AsyncSession, page_object: EditGenTableModel): """ - 编辑岗位信息service + 编辑业务表信息service :param query_db: orm对象 - :param page_object: 编辑岗位对象 - :return: 编辑岗位校验结果 + :param page_object: 编辑业务表对象 + :return: 编辑业务表校验结果 """ edit_gen_table = page_object.model_dump(exclude_unset=True, by_alias=True) gen_table_info = await cls.get_gen_table_by_id_services(query_db, page_object.table_id) @@ -129,11 +135,11 @@ class GenTableService: @classmethod async def delete_gen_table_services(cls, query_db: AsyncSession, page_object: DeleteGenTableModel): """ - 删除岗位信息service + 删除业务表信息service :param query_db: orm对象 - :param page_object: 删除岗位对象 - :return: 删除岗位校验结果 + :param page_object: 删除业务表对象 + :return: 删除业务表校验结果 """ if page_object.table_ids: table_id_list = page_object.table_ids.split(',') @@ -154,25 +160,24 @@ class GenTableService: @classmethod async def get_gen_table_by_id_services(cls, query_db: AsyncSession, table_id: int): """ - 获取需要生成的表格详细信息service + 获取需要生成的业务表详细信息service :param query_db: orm对象 - :param table_id: 需要生成的表格id - :return: 需要生成的表格id对应的信息 + :param table_id: 需要生成的业务表id + :return: 需要生成的业务表id对应的信息 """ gen_table = await GenTableDao.get_gen_table_by_id(query_db, table_id) - result = await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table))) + await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table))) - return result + return gen_table @classmethod async def get_gen_table_all_services(cls, query_db: AsyncSession): """ - 获取需要生成的表格详细信息service + 获取所有业务表信息service :param query_db: orm对象 - :param table_id: 需要生成的表格id - :return: 需要生成的表格id对应的信息 + :return: 所有业务表信息 """ gen_table_all = await GenTableDao.get_gen_table_all(query_db) result = [GenTableModel(**gen_table) for gen_table in CamelCaseUtil.transform_result(gen_table_all)] @@ -225,7 +230,7 @@ class GenTableService: 预览代码service :param query_db: orm对象 - :param table_id: 表格id + :param table_id: 业务表id :return: 预览数据列表 """ gen_table = GenTableModel( @@ -248,7 +253,7 @@ class GenTableService: 生成代码至指定路径service :param query_db: orm对象 - :param table_name: 表格名称 + :param table_name: 业务表名称 :return: 生成代码结果 """ env = TemplateInitializer.init_jinja2() @@ -273,7 +278,7 @@ class GenTableService: 批量生成代码service :param query_db: orm对象 - :param table_names: 表格名称 + :param table_names: 业务表名称组 :return: 下载代码结果 """ zip_buffer = io.BytesIO() @@ -295,7 +300,7 @@ class GenTableService: 获取生成代码渲染模板相关信息 :param query_db: orm对象 - :param table_name: 表格名称 + :param table_name: 业务表名称 :return: 生成代码渲染模板相关信息 """ gen_table = GenTableModel( @@ -327,11 +332,11 @@ class GenTableService: @classmethod async def sync_db_services(cls, query_db: AsyncSession, table_name: str): """ - 获取需要生成的表格详细信息service + 同步数据库service :param query_db: orm对象 - :param table_name: 表格名称 - :return: 需要生成的表格名称对应的信息 + :param table_name: 业务表名称 + :return: 同步数据库结果 """ gen_table = await GenTableDao.get_gen_table_by_name(query_db, table_name) table = GenTableModel(**CamelCaseUtil.transform_result(gen_table)) @@ -381,6 +386,7 @@ class GenTableService: :param query_db: orm对象 :param gen_table: 业务表信息 + :return: """ if gen_table.sub_table_name: sub_table = await GenTableDao.get_gen_table_by_name(query_db, gen_table.sub_table_name) @@ -392,6 +398,7 @@ class GenTableService: 设置主键列信息 :param gen_table: 业务表信息 + :return: """ for column in gen_table.columns: if column.pk: @@ -407,43 +414,13 @@ class GenTableService: if gen_table.sub_table.columns is None: gen_table.sub_table.pk_column = gen_table.sub_table.columns[0] - @staticmethod - async def export_post_list_services(post_list: List): - """ - 导出岗位信息service - - :param post_list: 岗位信息列表 - :return: 岗位信息对应excel的二进制数据 - """ - # 创建一个映射字典,将英文键映射到中文键 - mapping_dict = { - 'postId': '岗位编号', - 'postCode': '岗位编码', - 'postName': '岗位名称', - 'postSort': '显示顺序', - 'status': '状态', - 'createBy': '创建者', - 'createTime': '创建时间', - 'updateBy': '更新者', - 'updateTime': '更新时间', - 'remark': '备注', - } - - for item in post_list: - if item.get('status') == '0': - item['status'] = '正常' - else: - item['status'] = '停用' - binary_data = ExcelUtil.export_list2excel(post_list, mapping_dict) - - return binary_data - @classmethod async def set_table_from_options(cls, gen_table: GenTableModel): """ 设置代码生成其他选项值 :param gen_table: 设置后的生成对象 + :return: """ params_obj = json.loads(gen_table.options) if gen_table.options else None if params_obj: @@ -453,10 +430,13 @@ class GenTableService: gen_table.parent_menu_id = params_obj.get(GenConstant.PARENT_MENU_ID) gen_table.parent_menu_name = params_obj.get(GenConstant.PARENT_MENU_NAME) - return gen_table - @classmethod async def validate_edit(cls, edit_gen_table: EditGenTableModel): + """ + 编辑保存参数校验 + + :param edit_gen_table: 编辑业务表对象 + """ if edit_gen_table.tpl_category == GenConstant.TPL_TREE: params_obj = edit_gen_table.params @@ -474,14 +454,18 @@ class GenTableService: class GenTableColumnService: + """ + 代码生成业务表字段服务层 + """ + @classmethod async def get_gen_table_column_list_by_table_id_services(cls, query_db: AsyncSession, table_id: int): """ - 获取代码生成列列表信息service + 获取业务表字段列表信息service :param query_db: orm对象 - :param table_id: 表格id - :return: 代码生成列列表信息对象 + :param table_id: 业务表格id + :return: 业务表字段列表信息对象 """ gen_table_column_list_result = await GenTableColumnDao.get_gen_table_column_list_by_table_id(query_db, table_id) diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 index 5f6c3d1..6236836 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 @@ -1,6 +1,10 @@ {% set pkField = pkColumn.python_field %} {% set pk_field = pkColumn.python_field | camel_to_snake %} +{% for column in columns %} +{% if column.python_field == "createTime" %} from datetime import datetime +{% endif %} +{% endfor %} from fastapi import APIRouter, Depends, Form, Request from pydantic_validation_decorator import ValidateFields from sqlalchemy.ext.asyncio import AsyncSession @@ -29,18 +33,18 @@ async def get_{{ moduleName }}_{{ businessName }}_list( {{ businessName }}_page_query: {{ BusinessName }}PageQueryModel = Depends({{ BusinessName }}PageQueryModel.as_query), query_db: AsyncSession = Depends(get_db), ): -{% if table.crud or table.sub %} + {% if table.crud or table.sub %} # 获取分页数据 {{ businessName }}_page_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=True) logger.info('获取成功') return ResponseUtil.success(model_content={{ businessName }}_page_query_result) -{% elif table.tree %} + {% elif table.tree %} {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_query) logger.info('获取成功') return ResponseUtil.success(data={{ businessName }}_query_result) -{% endif %} + {% endif %} @{{ businessName }}Controller.post('', dependencies=[Depends(CheckUserInterfaceAuth('{{ permissionPrefix }}:add'))]) @@ -52,17 +56,17 @@ async def add_{{ moduleName }}_{{ businessName }}( query_db: AsyncSession = Depends(get_db), current_user: CurrentUserModel = Depends(LoginService.get_current_user), ): -{% for column in columns %} -{% if column.python_field == "createBy" %} + {% for column in columns %} + {% if column.python_field == "createBy" %} add_{{ businessName }}.create_by = current_user.user.user_name -{% elif column.python_field == "createTime" %} + {% elif column.python_field == "createTime" %} add_{{ businessName }}.create_time = datetime.now() -{% elif column.python_field == "updateBy" %} + {% elif column.python_field == "updateBy" %} add_{{ businessName }}.update_by = current_user.user.user_name -{% elif column.python_field == "updateTime" %} + {% elif column.python_field == "updateTime" %} add_{{ businessName }}.update_time = datetime.now() -{% endif %} -{% endfor %} + {% endif %} + {% endfor %} add_{{ businessName }}_result = await {{ BusinessName }}Service.add_{{ businessName }}_services(query_db, add_{{ businessName }}) logger.info(add_{{ businessName }}_result.message) diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 index b1b99f5..cd2cfdb 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 @@ -1,8 +1,13 @@ {% set pkField = pkColumn.python_field %} {% set pk_field = pkColumn.python_field | camel_to_snake %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} from datetime import datetime, time from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession +{% if table.sub %} +from sqlalchemy.orm import selectinload +{% endif %} from {{ packageName }}.entity.do.{{ businessName }}_do import {{ ClassName }} from {{ packageName }}.entity.vo.{{ businessName }}_vo import {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel from utils.page_util import PageUtil @@ -16,10 +21,10 @@ class {{ BusinessName }}Dao: @classmethod async def get_{{ businessName }}_detail_by_id(cls, db: AsyncSession, {{ pk_field }}: int): """ - 根据{{ functionName }}id获取{{ functionName }}详细信息 + 根据{{ pk_field_comment }}获取{{ functionName }}详细信息 :param db: orm对象 - :param {{ pk_field }}: 通知公告id + :param {{ pk_field }}: {{ pk_field_comment }} :return: {{ functionName }}信息对象 """ {{ businessName }}_info = (await db.execute(select({{ ClassName }}).where({{ ClassName }}.{{ pk_field }} == {{ pk_field }}))).scalars().first() @@ -64,7 +69,12 @@ class {{ BusinessName }}Dao: :return: {{ functionName }}列表信息对象 """ query = ( + {% if table.sub %%} select({{ ClassName }}) + .options(selectinload{{ ClassName }}.{{ subTable.business_name }}) + {% else %} + select({{ ClassName }}) + {% endif %} .where( {% for column in columns %} {% set field = column.python_field | camel_to_snake %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 index 404d76d..398159b 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 @@ -1,5 +1,6 @@ -from datetime import datetime -from sqlalchemy import Column, DateTime, Integer, String +{% for do_import in doImportList %} +{{ do_import }} +{% endfor %} {% if table.sub %} from sqlalchemy.orm import relationship {% endif %} @@ -18,23 +19,23 @@ class {{ ClassName }}(Base): {% endfor %} {% if table.sub %} - {{ table.sub_table.business_name }} = relationship('{{ table.sub_table.class_name }}', back_populates='{{ businessName }}') + {{ subTable.business_name }} = relationship('{{ subTable.class_name }}', back_populates='{{ businessName }}') {% endif %} {% if table.sub %} -class {{ table.sub_table.class_name }}(Base): +class {{ subTable.class_name }}(Base): """ - {{ table.sub_table.function_name }}表 + {{ subTable.function_name }}表 """ - __tablename__ = '{{ table.sub_table.table_name }}' + __tablename__ = '{{ subTableName }}' - {% for column in table.sub_table.columns %} + {% for column in subTable.columns %} {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required %}nullable=True{% else %}nullable=False{% endif %}, comment='{{ column.column_comment }}') {% endfor %} {% if table.sub %} - {{ businessName }} = relationship('{{ ClassName }}', back_populates='{{ table.sub_table.business_name }}') + {{ businessName }} = relationship('{{ ClassName }}', back_populates='{{ subTable.business_name }}') {% endif %} {% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 index 1a2ad71..4020cd7 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 @@ -1,6 +1,7 @@ {% set pkField = pkColumn.python_field %} {% set pk_field = pkColumn.python_field | camel_to_snake %} -{% set pk_field_comment = pkColumn.comment %} +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} from sqlalchemy.ext.asyncio import AsyncSession from typing import List from config.constant import CommonConstant @@ -87,8 +88,8 @@ class {{ BusinessName }}Service: :return: 编辑{{ functionName }}校验结果 """ edit_{{ businessName }} = page_object.model_dump(exclude_unset=True) - {{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.post_id) - if {{ businessName }}_info.post_id: + {{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.{{ pk_field }}) + if {{ businessName }}_info.{{ pk_field }}: if not await cls.check_post_name_unique_services(query_db, page_object): raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位名称已存在') elif not await cls.check_post_code_unique_services(query_db, page_object): @@ -133,8 +134,8 @@ class {{ BusinessName }}Service: 获取{{ functionName }}详细信息service :param query_db: orm对象 - :param post_id: 岗位id - :return: 岗位id对应的信息 + :param {{ pk_field }}: {{ pk_field_comment }} + :return: {{ pk_field_comment }}对应的信息 """ {{ businessName }} = await {{ BusinessName }}Dao.get_{{ businessName }}_detail_by_id(query_db, {{ pk_field }}={{ pk_field }}) if {{ businessName }}: diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 index cd07326..c29a0cf 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 @@ -1,11 +1,14 @@ {% set pkField = pkColumn.python_field %} {% set pk_field = pkColumn.python_field | camel_to_snake %} -{% set pk_field_comment = pkColumn.comment %} -from datetime import datetime +{% set pkParentheseIndex = pkColumn.column_comment.find("(") %} +{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% for vo_import in voImportList %} +{{ vo_import }} +{% endfor %} from pydantic import BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel from pydantic_validation_decorator import NotBlank -from typing import Literal, Optional +from typing import Optional from module_admin.annotation.pydantic_annotation import as_query diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 index e69de29..b33a65b 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 @@ -0,0 +1,454 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 index 31b3cf8..a6d80f6 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 @@ -7,7 +7,6 @@ {% set AttrName = column.python_field[0] | upper + column.python_field[1:] %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} - {% if column.html_type == "input" %} None: - """初始化表信息""" + """ + 初始化表信息 + + param gen_table: 业务表对象 + param oper_name: 操作人 + :return: + """ gen_table.class_name = cls.convert_class_name(gen_table.table_name) gen_table.package_name = GenConfig.package_name gen_table.module_name = cls.get_module_name(GenConfig.package_name) @@ -26,7 +32,13 @@ class GenUtils: @classmethod def init_column_field(cls, column: GenTableColumnModel, table: GenTableModel) -> None: - """初始化列属性字段""" + """ + 初始化列属性字段 + + param column: 业务表字段对象 + param table: 业务表对象 + :return: + """ data_type = cls.get_db_type(column.column_type) column_name = column.column_name column.table_id = table.table_id @@ -34,7 +46,9 @@ class GenUtils: # 设置Python字段名 column.python_field = cls.to_camel_case(column_name) # 设置默认类型 - column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.MYSQL_TO_PYTHON_TYPE_MAPPING, data_type) + column.python_type = StringUtil.get_mapping_value_by_key_ignore_case( + GenConstant.MYSQL_TO_PYTHON_TYPE_MAPPING, data_type + ) column.query_type = GenConstant.QUERY_EQ if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains( @@ -87,22 +101,43 @@ class GenUtils: @classmethod def arrays_contains(cls, arr: List[str], target_value: str) -> bool: - """校验数组是否包含指定值""" + """ + 校验数组是否包含指定值 + + param arr: 数组 + param target_value: 需要校验的值 + :return: 校验结果 + """ return target_value in arr @classmethod def get_module_name(cls, package_name: str) -> str: - """获取模块名""" + """ + 获取模块名 + + param package_name: 包名 + :return: 模块名 + """ return package_name.split('.')[-1] @classmethod def get_business_name(cls, table_name: str) -> str: - """获取业务名""" + """ + 获取业务名 + + param table_name: 业务表名 + :return: 业务名 + """ return table_name.split('_')[-1] @classmethod def convert_class_name(cls, table_name: str) -> str: - """表名转换成Python类名""" + """ + 表名转换成Python类名 + + param table_name: 业务表名 + :return: Python类名 + """ auto_remove_pre = GenConfig.auto_remove_pre table_prefix = GenConfig.table_prefix if auto_remove_pre and table_prefix: @@ -112,7 +147,13 @@ class GenUtils: @classmethod def replace_first(cls, replacement: str, search_list: List[str]) -> str: - """批量替换前缀""" + """ + 批量替换前缀 + + param replacement: 需要被替换的字符串 + param search_list: 可替换的字符串列表 + :return: 替换后的字符串 + """ for search_string in search_list: if replacement.startswith(search_string): return replacement.replace(search_string, '', 1) @@ -120,19 +161,34 @@ class GenUtils: @classmethod def replace_text(cls, text: str) -> str: - """关键字替换""" + """ + 关键字替换 + + param text: 需要被替换的字符串 + :return: 替换后的字符串 + """ return re.sub(r'(?:表|若依)', '', text) @classmethod def get_db_type(cls, column_type: str) -> str: - """获取数据库类型字段""" + """ + 获取数据库类型字段 + + param column_type: 字段类型 + :return: 数据库类型 + """ if '(' in column_type: return column_type.split('(')[0] return column_type @classmethod def get_column_length(cls, column_type: str) -> int: - """获取字段长度""" + """ + 获取字段长度 + + param column_type: 字段类型 + :return: 字段长度 + """ if '(' in column_type: length = len(column_type.split('(')[1].split(')')[0]) return length @@ -140,13 +196,23 @@ class GenUtils: @classmethod def split_column_type(cls, column_type: str) -> List[str]: - """拆分列类型""" + """ + 拆分列类型 + + param column_type: 字段类型 + :return: 拆分结果 + """ if '(' in column_type and ')' in column_type: return column_type.split('(')[1].split(')')[0].split(',') return [] @classmethod def to_camel_case(cls, text: str) -> str: - """将字符串转换为驼峰命名""" + """ + 将字符串转换为驼峰命名 + + param text: 需要转换的字符串 + :return: 驼峰命名 + """ parts = text.split('_') return parts[0] + ''.join(word.capitalize() for word in parts[1:]) diff --git a/ruoyi-fastapi-backend/utils/template_util.py b/ruoyi-fastapi-backend/utils/template_util.py index 47c4ac3..19c8573 100644 --- a/ruoyi-fastapi-backend/utils/template_util.py +++ b/ruoyi-fastapi-backend/utils/template_util.py @@ -55,6 +55,7 @@ class TemplateUtils: def prepare_context(cls, gen_table: GenTableModel): """ 准备模板变量 + :param gen_table: 生成表的配置信息 :return: 模板上下文字典 """ @@ -79,7 +80,8 @@ class TemplateUtils: 'author': gen_table.function_author, 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'pkColumn': gen_table.pk_column, - 'importList': cls.get_import_list(gen_table), + 'doImportList': cls.get_do_import_list(gen_table), + 'voImportList': cls.get_vo_import_list(gen_table), 'permissionPrefix': cls.get_permission_prefix(module_name, business_name), 'columns': gen_table.columns, 'table': gen_table, @@ -97,14 +99,26 @@ class TemplateUtils: @classmethod def set_menu_context(cls, context: Dict, gen_table: GenTableModel): - """设置菜单上下文""" + """ + 设置菜单上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ options = gen_table.options params_obj = json.loads(options) context['parentMenuId'] = cls.get_parent_menu_id(params_obj) @classmethod def set_tree_context(cls, context: Dict, gen_table: GenTableModel): - """设置树形结构上下文""" + """ + 设置树形结构上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ options = gen_table.options params_obj = json.loads(options) context['treeCode'] = cls.get_tree_code(params_obj) @@ -114,7 +128,13 @@ class TemplateUtils: @classmethod def set_sub_context(cls, context: Dict, gen_table: GenTableModel): - """设置子表上下文""" + """ + 设置子表上下文 + + :param context: 模板上下文字典 + :param gen_table: 生成表的配置信息 + :return: 新的模板上下文字典 + """ sub_table = gen_table.sub_table sub_table_name = gen_table.sub_table_name sub_table_fk_name = gen_table.sub_table_fk_name @@ -127,11 +147,16 @@ class TemplateUtils: context['subTableFkclassName'] = sub_table_fk_class_name.lower() context['subClassName'] = sub_class_name context['subclassName'] = sub_class_name.lower() - context['subImportList'] = cls.get_import_list(sub_table) @classmethod - def get_template_list(cls, tpl_category, tpl_web_type): - """获取模板列表""" + def get_template_list(cls, tpl_category: str, tpl_web_type: str): + """ + 获取模板列表 + + :param tpl_category: 生成模板类型 + :param tpl_web_type: 前端类型 + :return: 模板列表 + """ use_web_type = 'vue' if tpl_web_type == 'element-plus': use_web_type = 'vue/v3' @@ -154,14 +179,20 @@ class TemplateUtils: return templates @classmethod - def get_file_name(cls, template, gen_table: GenTableModel): - """根据模板生成文件名""" + def get_file_name(cls, template: List[str], gen_table: GenTableModel): + """ + 根据模板生成文件名 + + :param template: 模板列表 + :param gen_table: 生成表的配置信息 + :return: 模板生成文件名 + """ package_name = gen_table.package_name module_name = gen_table.module_name business_name = gen_table.business_name vue_path = cls.FRONTEND_PROJECT_PATH - python_path = f"{cls.BACKEND_PROJECT_PATH}/{package_name.replace('.', '/')}" + python_path = f'{cls.BACKEND_PROJECT_PATH}/{package_name.replace(".", "/")}' if 'controller.py.jinja2' in template: return f'{python_path}/controller/{business_name}_controller.py' @@ -183,27 +214,106 @@ class TemplateUtils: @classmethod def get_package_prefix(cls, package_name: str): - """获取包前缀""" + """ + 获取包前缀 + + :param package_name: 包名 + :return: 包前缀 + """ return package_name[: package_name.rfind('.')] @classmethod - def get_import_list(cls, gen_table: GenTableModel): - """获取导入包列表""" + def get_vo_import_list(cls, gen_table: GenTableModel): + """ + 获取vo模板导入包列表 + + :param gen_table: 生成表的配置信息 + :return: 导入包列表 + """ columns = gen_table.columns or [] - sub_gen_table = gen_table.sub_table import_list = set() - if sub_gen_table is not None: - import_list.add('python.util.List') for column in columns: - if not column.super_column and column.python_type in GenConstant.TYPE_DATE: + if column.python_type in GenConstant.TYPE_DATE: import_list.add(f'from datetime import {column.python_type}') - elif not column.super_column and column.python_type == GenConstant.TYPE_DECIMAL: + elif column.python_type == GenConstant.TYPE_DECIMAL: import_list.add('from decimal import Decimal') - return list(import_list) + if gen_table.sub: + sub_columns = gen_table.sub_table.columns or [] + for sub_column in sub_columns: + if sub_column.python_type in GenConstant.TYPE_DATE: + import_list.add(f'from datetime import {column.python_type}') + elif sub_column.python_type == GenConstant.TYPE_DECIMAL: + import_list.add('from decimal import Decimal') + return cls.merge_same_imports(list(import_list), 'from datetime import') + + @classmethod + def get_do_import_list(cls, gen_table: GenTableModel): + """ + 获取do模板导入包列表 + + :param gen_table: 生成表的配置信息 + :return: 导入包列表 + """ + columns = gen_table.columns or [] + import_list = set() + for column in columns: + data_type = cls.get_db_type(column.column_type) + import_list.add( + f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.MYSQL_TO_SQLALCHEMY_TYPE_MAPPING, data_type)}' + ) + if gen_table.sub: + sub_columns = gen_table.sub_table.columns or [] + for sub_column in sub_columns: + if sub_column.python_type in GenConstant.TYPE_DATE: + import_list.add(f'from datetime import {column.python_type}') + elif sub_column.python_type == GenConstant.TYPE_DECIMAL: + import_list.add('from decimal import Decimal') + return cls.merge_same_imports(list(import_list), 'from sqlalchemy import') + + @classmethod + def get_db_type(cls, column_type: str) -> str: + """ + 获取数据库类型字段 + + param column_type: 字段类型 + :return: 数据库类型 + """ + if '(' in column_type: + return column_type.split('(')[0] + return column_type + + @classmethod + def merge_same_imports(cls, imports: List[str], import_start: str) -> List[str]: + """ + 合并相同的导入语句 + + :param imports: 导入语句列表 + :param import_start: 导入语句的起始字符串 + :return: 合并后的导入语句列表 + """ + merged_imports = [] + _imports = [] + for import_stmt in imports: + if import_stmt.startswith(import_start): + imported_items = import_stmt.split('import')[1].strip() + _imports.extend(imported_items.split(', ')) + else: + merged_imports.append(import_stmt) + + if _imports: + merged_datetime_import = f'{import_start} {", ".join(_imports)}' + merged_imports.append(merged_datetime_import) + + return merged_imports @classmethod def get_dicts(cls, gen_table: GenTableModel): - """获取字典列表""" + """ + 获取字典列表 + + :param gen_table: 生成表的配置信息 + :return: 字典列表 + """ columns = gen_table.columns or [] dicts = set() cls.add_dicts(dicts, columns) @@ -213,7 +323,13 @@ class TemplateUtils: @classmethod def add_dicts(cls, dicts: Set[str], columns: List[GenTableColumnModel]): - """添加字典列表""" + """ + 添加字典列表 + + :param dicts: 字典列表 + :param columns: 字段列表 + :return: 新的字典列表 + """ for column in columns: if ( column.super_column @@ -226,40 +342,71 @@ class TemplateUtils: @classmethod def get_permission_prefix(cls, module_name: str, business_name: str): - """获取权限前缀""" + """ + 获取权限前缀 + + :param module_name: 模块名 + :param business_name: 业务名 + :return: 权限前缀 + """ return f'{module_name}:{business_name}' @classmethod - def get_parent_menu_id(cls, params_obj): - """获取上级菜单ID""" + def get_parent_menu_id(cls, params_obj: Dict): + """ + 获取上级菜单ID + + :param params_obj: 菜单参数字典 + :return: 上级菜单ID + """ if params_obj and params_obj.get(GenConstant.PARENT_MENU_ID): return params_obj.get(GenConstant.PARENT_MENU_ID) return cls.DEFAULT_PARENT_MENU_ID @classmethod def get_tree_code(cls, params_obj: Dict): - """获取树编码""" + """ + 获取树编码 + + :param params_obj: 菜单参数字典 + :return: 树编码 + """ if GenConstant.TREE_CODE in params_obj: return cls.to_camel_case(params_obj.get(GenConstant.TREE_CODE)) return '' @classmethod def get_tree_parent_code(cls, params_obj: Dict): - """获取树父编码""" + """ + 获取树父编码 + + :param params_obj: 菜单参数字典 + :return: 树父编码 + """ if GenConstant.TREE_PARENT_CODE in params_obj: return cls.to_camel_case(params_obj.get(GenConstant.TREE_PARENT_CODE)) return '' @classmethod def get_tree_name(cls, params_obj: Dict): - """获取树名称""" + """ + 获取树名称 + + :param params_obj: 菜单参数字典 + :return: 树名称 + """ if GenConstant.TREE_NAME in params_obj: return cls.to_camel_case(params_obj.get(GenConstant.TREE_NAME)) return '' @classmethod def get_expand_column(cls, gen_table: GenTableModel): - """获取展开列""" + """ + 获取展开列 + + :param gen_table: 生成表的配置信息 + :return: 展开列 + """ options = gen_table.options params_obj = json.loads(options) tree_name = params_obj.get(GenConstant.TREE_NAME) @@ -273,12 +420,23 @@ class TemplateUtils: @classmethod def to_camel_case(cls, text: str) -> str: - """将字符串转换为驼峰命名""" + """ + 将字符串转换为驼峰命名 + + :param text: 待转换的字符串 + :return: 转换后的驼峰命名字符串 + """ parts = text.split('_') return parts[0] + ''.join(word.capitalize() for word in parts[1:]) @classmethod def get_sqlalchemy_type(cls, column_type: str): + """ + 获取SQLAlchemy类型 + + :param column_type: 列类型 + :return: SQLAlchemy类型 + """ if '(' in column_type: column_type_list = column_type.split('(') sqlalchemy_type = ( -- Gitee From 59f6e0091fa7f6bd36fa491bc908e1a289487a5c Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Sun, 16 Feb 2025 23:29:44 +0800 Subject: [PATCH 17/34] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90vue2=E5=89=8D=E7=AB=AF=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../templates/vue/index-tree.vue.jinja2 | 486 +++++++++++++++ .../templates/vue/index.vue.jinja2 | 586 ++++++++++++++++++ 2 files changed, 1072 insertions(+) create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 create mode 100644 ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 new file mode 100644 index 0000000..67cb845 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 @@ -0,0 +1,486 @@ + + + \ No newline at end of file diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 new file mode 100644 index 0000000..e783e46 --- /dev/null +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 @@ -0,0 +1,586 @@ + + + \ No newline at end of file -- Gitee From 25e2a1e9312a6c5e9c879d49eef48af186032a4e Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Sun, 16 Feb 2025 23:30:28 +0800 Subject: [PATCH 18/34] =?UTF-8?q?feat:=20postgresql=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93sql=E8=AF=AD=E5=8F=A5=E6=96=B0=E5=A2=9E=E8=A7=86?= =?UTF-8?q?=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sql/ruoyi-fastapi-pg.sql | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql index 3b33738..4ef7154 100644 --- a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql @@ -975,3 +975,71 @@ END; $BODY$ LANGUAGE plpgsql VOLATILE COST 100; + +create view list_column as +SELECT c.relname AS table_name, + a.attname AS column_name, + d.description AS column_comment, + CASE + WHEN a.attnotnull AND con.conname IS NULL THEN 1 + ELSE 0 + END AS is_required, + CASE + WHEN con.conname IS NOT NULL THEN 1 + ELSE 0 + END AS is_pk, + a.attnum AS sort, + CASE + WHEN "position"(pg_get_expr(ad.adbin, ad.adrelid), ((c.relname::text || '_'::text) || a.attname + ::text) || '_seq'::text) > 0 THEN 1 + ELSE 0 + END AS is_increment, + btrim( + CASE + WHEN t.typelem <> 0::oid AND t.typlen = '-1'::integer THEN 'ARRAY'::text + ELSE + CASE + WHEN t.typtype = 'd'::"char" THEN format_type(t.typbasetype, NULL::integer) + ELSE format_type(a.atttypid, NULL::integer) + END + END, '"'::text) AS column_type +FROM pg_attribute a + JOIN (pg_class c + JOIN pg_namespace n ON c.relnamespace = n.oid) ON a.attrelid = c.oid + LEFT JOIN pg_description d ON d.objoid = c.oid AND a.attnum = d.objsubid + LEFT JOIN pg_constraint con ON con.conrelid = c.oid AND (a.attnum = ANY (con.conkey)) + LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum + LEFT JOIN pg_type t ON a.atttypid = t.oid +WHERE (c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"])) + AND a.attnum > 0 + AND n.nspname = 'public'::name + AND not a.attisdropped + ORDER BY c.relname, a.attnum; + +create view list_table as +SELECT c.relname AS table_name, + obj_description(c.oid) AS table_comment, + CURRENT_TIMESTAMP AS create_time, + CURRENT_TIMESTAMP AS update_time +FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace +WHERE (c.relkind = ANY (ARRAY['r'::"char", 'p'::"char"])) + AND c.relname !~~ 'spatial_%'::text AND n.nspname = 'public'::name AND n.nspname <> ''::name; + +CREATE OR REPLACE FUNCTION substring_index(varchar, varchar, integer) +RETURNS varchar AS $$ +DECLARE +tokens varchar[]; +length integer ; +indexnum integer; +BEGIN +tokens := pg_catalog.string_to_array($1, $2); +length := pg_catalog.array_upper(tokens, 1); +indexnum := length - ($3 * -1) + 1; +IF $3 >= 0 THEN +RETURN pg_catalog.array_to_string(tokens[1:$3], $2); +ELSE +RETURN pg_catalog.array_to_string(tokens[indexnum:length], $2); +END IF; +END; +$$ IMMUTABLE STRICT LANGUAGE PLPGSQL; -- Gitee From 2b474ddb35b124a2a0688c99ca0eb410750758a6 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Mon, 17 Feb 2025 17:43:29 +0800 Subject: [PATCH 19/34] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E7=94=9F=E6=88=90=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/gen_controller.py | 2 +- .../module_generator/entity/vo/gen_vo.py | 31 +++--- .../module_generator/service/gen_service.py | 33 +++--- .../templates/python/controller.py.jinja2 | 2 +- .../templates/python/dao.py.jinja2 | 73 +++++++++++-- .../templates/python/do.py.jinja2 | 6 +- .../templates/python/service.py.jinja2 | 103 ++++++++++++------ .../templates/python/vo.py.jinja2 | 61 ++++++++++- .../templates/vue/index-tree.vue.jinja2 | 3 +- ruoyi-fastapi-backend/utils/gen_util.py | 5 + ruoyi-fastapi-backend/utils/template_util.py | 3 +- 11 files changed, 239 insertions(+), 83 deletions(-) diff --git a/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py index 8396f1b..4e227c1 100644 --- a/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py +++ b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py @@ -67,7 +67,7 @@ async def import_gen_table( @genController.put('', dependencies=[Depends(CheckUserInterfaceAuth('tool:gen:edit'))]) -@ValidateFields(validate_model='edit_post') +@ValidateFields(validate_model='edit_gen_table') @Log(title='代码生成', business_type=BusinessType.UPDATE) async def edit_gen_table( request: Request, diff --git a/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py index a28d3b0..34d8bbc 100644 --- a/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py +++ b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py @@ -5,6 +5,7 @@ from pydantic_validation_decorator import NotBlank from typing import List, Literal, Optional from config.constant import GenConstant from module_admin.annotation.pydantic_annotation import as_query +from utils.string_util import StringUtil class GenTableBaseModel(BaseModel): @@ -183,6 +184,8 @@ class GenTableColumnBaseModel(BaseModel): sort: Optional[int] = Field(default=None, description='排序') create_by: Optional[str] = Field(default=None, description='创建者') create_time: Optional[datetime] = Field(default=None, description='创建时间') + update_by: Optional[str] = Field(default=None, description='更新者') + update_time: Optional[datetime] = Field(default=None, description='更新时间') @NotBlank(field_name='python_field', message='Python属性不能为空') def get_python_field(self): @@ -198,15 +201,15 @@ class GenTableColumnModel(GenTableColumnBaseModel): """ cap_python_field: Optional[str] = Field(default=None, description='字段大写形式') - pk: Optional[bool] = Field(default=None, description='是否为子表') - increment: Optional[bool] = Field(default=None, description='是否为树表') - required: Optional[bool] = Field(default=None, description='是否为必填字段') - insert: Optional[bool] = Field(default=None, description='是否为必填字段') - edit: Optional[bool] = Field(default=None, description='是否为必填字段') - list: Optional[bool] = Field(default=None, description='是否为必填字段') - query: Optional[bool] = Field(default=None, description='是否为必填字段') + pk: Optional[bool] = Field(default=None, description='是否主键') + increment: Optional[bool] = Field(default=None, description='是否自增') + required: Optional[bool] = Field(default=None, description='是否必填') + insert: Optional[bool] = Field(default=None, description='是否为插入字段') + edit: Optional[bool] = Field(default=None, description='是否编辑字段') + list: Optional[bool] = Field(default=None, description='是否列表字段') + query: Optional[bool] = Field(default=None, description='是否查询字段') super_column: Optional[bool] = Field(default=None, description='是否为基类字段') - usable_column: Optional[bool] = Field(default=None, description='是否为基类字段') + usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单') @model_validator(mode='after') def check_some_is(self) -> 'GenTableModel': @@ -220,19 +223,11 @@ class GenTableColumnModel(GenTableColumnBaseModel): self.query = True if self.is_query and self.is_query == '1' else False self.super_column = ( True - if any( - self.python_field and self.python_field.lower() == field.lower() - for field in GenConstant.TREE_ENTITY + GenConstant.BASE_ENTITY - ) + if StringUtil.equals_any_ignore_case(self.python_field, GenConstant.TREE_ENTITY + GenConstant.BASE_ENTITY) else False ) self.usable_column = ( - True - if any( - self.python_field and self.python_field.lower() == field.lower() - for field in ['parentId', 'orderNum', 'remark'] - ) - else False + True if StringUtil.equals_any_ignore_case(self.python_field, ['parentId', 'orderNum', 'remark']) else False ) return self diff --git a/ruoyi-fastapi-backend/module_generator/service/gen_service.py b/ruoyi-fastapi-backend/module_generator/service/gen_service.py index 9132e91..c22019d 100644 --- a/ruoyi-fastapi-backend/module_generator/service/gen_service.py +++ b/ruoyi-fastapi-backend/module_generator/service/gen_service.py @@ -3,6 +3,7 @@ import json import os import re import zipfile +from datetime import datetime from sqlalchemy.ext.asyncio import AsyncSession from typing import List from config.constant import GenConstant @@ -121,6 +122,8 @@ class GenTableService: edit_gen_table['options'] = json.dumps(edit_gen_table.get('params')) await GenTableDao.edit_gen_table_dao(query_db, edit_gen_table) for gen_table_column in page_object.columns: + gen_table_column.update_by = page_object.update_by + gen_table_column.update_time = datetime.now() await GenTableColumnDao.edit_gen_table_column_dao( query_db, gen_table_column.model_dump(by_alias=True) ) @@ -167,9 +170,9 @@ class GenTableService: :return: 需要生成的业务表id对应的信息 """ gen_table = await GenTableDao.get_gen_table_by_id(query_db, table_id) - await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table))) + result = await cls.set_table_from_options(GenTableModel(**CamelCaseUtil.transform_result(gen_table))) - return gen_table + return result @classmethod async def get_gen_table_all_services(cls, query_db: AsyncSession): @@ -419,8 +422,8 @@ class GenTableService: """ 设置代码生成其他选项值 - :param gen_table: 设置后的生成对象 - :return: + :param gen_table: 生成对象 + :return: 设置后的生成对象 """ params_obj = json.loads(gen_table.options) if gen_table.options else None if params_obj: @@ -430,6 +433,8 @@ class GenTableService: gen_table.parent_menu_id = params_obj.get(GenConstant.PARENT_MENU_ID) gen_table.parent_menu_name = params_obj.get(GenConstant.PARENT_MENU_NAME) + return gen_table + @classmethod async def validate_edit(cls, edit_gen_table: EditGenTableModel): """ @@ -438,19 +443,19 @@ class GenTableService: :param edit_gen_table: 编辑业务表对象 """ if edit_gen_table.tpl_category == GenConstant.TPL_TREE: - params_obj = edit_gen_table.params - - if not getattr(params_obj, GenConstant.TREE_CODE): - raise ServiceException('树编码字段不能为空') - elif not getattr(params_obj, GenConstant.TREE_PARENT_CODE): - raise ServiceException('树父编码字段不能为空') - elif not getattr(params_obj, GenConstant.TREE_NAME): - raise ServiceException('树名称字段不能为空') + params_obj = edit_gen_table.params.model_dump(by_alias=True) + + if GenConstant.TREE_CODE not in params_obj: + raise ServiceException(message='树编码字段不能为空') + elif GenConstant.TREE_PARENT_CODE not in params_obj: + raise ServiceException(message='树父编码字段不能为空') + elif GenConstant.TREE_NAME not in params_obj: + raise ServiceException(message='树名称字段不能为空') elif edit_gen_table.tpl_category == GenConstant.TPL_SUB: if not edit_gen_table.sub_table_name: - raise ServiceException('关联子表的表名不能为空') + raise ServiceException(message='关联子表的表名不能为空') elif not edit_gen_table.sub_table_fk_name: - raise ServiceException('子表关联的外键名不能为空') + raise ServiceException(message='子表关联的外键名不能为空') class GenTableColumnService: diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 index 6236836..e1a1146 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/controller.py.jinja2 @@ -119,7 +119,7 @@ async def export_{{ moduleName }}_{{ businessName }}_list( ): # 获取全量数据 {{ businessName }}_query_result = await {{ BusinessName }}Service.get_{{ businessName }}_list_services(query_db, {{ businessName }}_page_query, is_page=False) - {{ businessName }}_export_result = await {{ BusinessName }}Service.export_{{ businessName }}_list_services({{ businessName }}_query_result) + {{ businessName }}_export_result = await {{ BusinessName }}Service.export_{{ businessName }}_list_services({% if dicts %}request, {% endif %}{{ businessName }}_query_result) logger.info('导出成功') return ResponseUtil.streaming(data=bytes2file_response({{ businessName }}_export_result)) diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 index cd2cfdb..8ea556f 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 @@ -2,7 +2,11 @@ {% set pk_field = pkColumn.python_field | camel_to_snake %} {% set pkParentheseIndex = pkColumn.column_comment.find("(") %} {% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% for column in columns %} +{% if column.python_field == "createTime" %} from datetime import datetime, time +{% endif %} +{% endfor %} from sqlalchemy import delete, select, update from sqlalchemy.ext.asyncio import AsyncSession {% if table.sub %} @@ -27,7 +31,23 @@ class {{ BusinessName }}Dao: :param {{ pk_field }}: {{ pk_field_comment }} :return: {{ functionName }}信息对象 """ - {{ businessName }}_info = (await db.execute(select({{ ClassName }}).where({{ ClassName }}.{{ pk_field }} == {{ pk_field }}))).scalars().first() + {{ businessName }}_info = ( + ( + await db.execute( + {% if table.sub %} + select({{ ClassName }}) + .options(selectinload({{ ClassName }}.{{ subclassName }}_list)) + {% else %} + select({{ ClassName }}) + {% endif %} + .where( + {{ ClassName }}.{{ pk_field }} == {{ pk_field }} + ) + ) + ) + .scalars() + .first() + ) return {{ businessName }}_info @@ -69,9 +89,9 @@ class {{ BusinessName }}Dao: :return: {{ functionName }}列表信息对象 """ query = ( - {% if table.sub %%} + {% if table.sub %} select({{ ClassName }}) - .options(selectinload{{ ClassName }}.{{ subTable.business_name }}) + .options(selectinload({{ ClassName }}.{{ subclassName }}_list)) {% else %} select({{ ClassName }}) {% endif %} @@ -117,10 +137,10 @@ class {{ BusinessName }}Dao: 新增{{ functionName }}数据库操作 :param db: orm对象 - :param notice: {{ functionName }}对象 + :param {{ businessName }}: {{ functionName }}对象 :return: """ - db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump()) + db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump({%if table.sub %}exclude={'{{ subclassName }}_list'}{% endif %})) db.add(db_{{ businessName }}) await db.flush() @@ -132,7 +152,7 @@ class {{ BusinessName }}Dao: 编辑{{ functionName }}数据库操作 :param db: orm对象 - :param notice: 需要更新的{{ functionName }}字典 + :param {{ businessName }}: 需要更新的{{ functionName }}字典 :return: """ await db.execute(update({{ ClassName }}), [{{ businessName }}]) @@ -143,7 +163,46 @@ class {{ BusinessName }}Dao: 删除{{ functionName }}数据库操作 :param db: orm对象 - :param notice: {{ functionName }}对象 + :param {{ businessName }}: {{ functionName }}对象 :return: """ await db.execute(delete({{ ClassName }}).where({{ ClassName }}.{{ pk_field }}.in_([{{ businessName }}.{{ pk_field }}]))) + + {% if table.sub %} + @classmethod + async def add_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: {{ subTable.business_name | capitalize }}Model): + """ + 新增{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: {{ subTable.function_name }}对象 + :return: + """ + db_{{ subTable.business_name }} = {{ subClassName }}(**{{ subTable.business_name }}.model_dump()) + db.add(db_{{ subTable.business_name }}) + await db.flush() + + return db_{{ subTable.business_name }} + + @classmethod + async def edit_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: dict): + """ + 编辑{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: 需要更新的{{ subTable.function_name }}字典 + :return: + """ + await db.execute(update({{ subClassName }}), [{{ subTable.business_name }}]) + + @classmethod + async def delete_{{ subTable.business_name }}_dao(cls, db: AsyncSession, {{ subTable.business_name }}: {{ subTable.business_name | capitalize }}Model): + """ + 删除{{ subTable.function_name }}数据库操作 + + :param db: orm对象 + :param {{ subTable.business_name }}: {{ subTable.function_name }}对象 + :return: + """ + await db.execute(delete({{ subClassName }}).where({{ subClassName }}.{{ subTable.pk_column.python_field | camel_to_snake }}.in_([{{ subTable.business_name }}.{{ subTable.pk_column.python_field | camel_to_snake }}]))) + {% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 index 398159b..a3775d8 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 @@ -19,12 +19,12 @@ class {{ ClassName }}(Base): {% endfor %} {% if table.sub %} - {{ subTable.business_name }} = relationship('{{ subTable.class_name }}', back_populates='{{ businessName }}') + {{ subclassName }}_list = relationship('{{ subClassName }}', back_populates='{{ businessName }}') {% endif %} {% if table.sub %} -class {{ subTable.class_name }}(Base): +class {{ subClassName }}(Base): """ {{ subTable.function_name }}表 """ @@ -36,6 +36,6 @@ class {{ subTable.class_name }}(Base): {% endfor %} {% if table.sub %} - {{ businessName }} = relationship('{{ ClassName }}', back_populates='{{ subTable.business_name }}') + {{ businessName }} = relationship('{{ ClassName }}', back_populates='{{ subclassName }}_list') {% endif %} {% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 index 4020cd7..eeda5cf 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 @@ -2,12 +2,18 @@ {% set pk_field = pkColumn.python_field | camel_to_snake %} {% set pkParentheseIndex = pkColumn.column_comment.find("(") %} {% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% if dicts %} +from fastapi import Request +{% endif %} from sqlalchemy.ext.asyncio import AsyncSession from typing import List from config.constant import CommonConstant from exceptions.exception import ServiceException -from {{ packageName }}.dao.{{ businessName }}_dao import {{ BusinessName }}Dao from module_admin.entity.vo.common_vo import CrudResponseModel +{% if dicts %} +from module_admin.service.dict_service import DictDataService +{% endif %} +from {{ packageName }}.dao.{{ businessName }}_dao import {{ BusinessName }}Dao from {{ packageName }}.entity.vo.{{ businessName }}_vo import Delete{{ BusinessName }}Model, {{ BusinessName }}Model, {{ BusinessName }}PageQueryModel from utils.common_util import CamelCaseUtil from utils.excel_util import ExcelUtil @@ -65,18 +71,28 @@ class {{ BusinessName }}Service: :param page_object: 新增{{ functionName }}对象 :return: 新增{{ functionName }}校验结果 """ - if not await cls.check_post_name_unique_services(query_db, page_object): - raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位名称已存在') - elif not await cls.check_post_code_unique_services(query_db, page_object): - raise ServiceException(message=f'新增岗位{page_object.post_name}失败,岗位编码已存在') - else: - try: - await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) - await query_db.commit() - return CrudResponseModel(is_success=True, message='新增成功') - except Exception as e: - await query_db.rollback() - raise e + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.required %} + if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object) + raise ServiceException(message=f'新增{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') + {% endif %} + {% endfor %} + try: + {% if table.sub %} + add_{{ businessName }} = await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) + if add_{{ businessName }}: + for sub_table in page_object.{{ subclassName }}_list: + await {{ BusinessName }}Dao.add_{{ subTable.business_name }}_dao(query_db, sub_table) + {% else %} + await {{ BusinessName }}Dao.add_{{ businessName }}_dao(query_db, page_object) + {% endif %} + await query_db.commit() + return CrudResponseModel(is_success=True, message='新增成功') + except Exception as e: + await query_db.rollback() + raise e @classmethod async def edit_{{ businessName }}_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): @@ -87,21 +103,28 @@ class {{ BusinessName }}Service: :param page_object: 编辑{{ functionName }}对象 :return: 编辑{{ functionName }}校验结果 """ - edit_{{ businessName }} = page_object.model_dump(exclude_unset=True) + edit_{{ businessName }} = page_object.model_dump(exclude_unset=True{% if table.sub %}, exclude={'{{ subclassName }}_list'}{% endif %}) {{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.{{ pk_field }}) if {{ businessName }}_info.{{ pk_field }}: - if not await cls.check_post_name_unique_services(query_db, page_object): - raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位名称已存在') - elif not await cls.check_post_code_unique_services(query_db, page_object): - raise ServiceException(message=f'修改岗位{page_object.post_name}失败,岗位编码已存在') - else: - try: - await {{ BusinessName }}Dao.edit_{{ businessName }}_dao(query_db, edit_{{ businessName }}) - await query_db.commit() - return CrudResponseModel(is_success=True, message='更新成功') - except Exception as e: - await query_db.rollback() - raise e + {% for column in columns %} + {% set parentheseIndex = column.column_comment.find("(") %} + {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} + {% if column.required %} + if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object) + raise ServiceException(message=f'修改{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') + {% endif %} + {% endfor %} + try: + await {{ BusinessName }}Dao.edit_{{ businessName }}_dao(query_db, edit_{{ businessName }}) + {% if table.sub %} + for sub_table in page_object.{{ subclassName }}_list: + await {{ BusinessName }}Dao.edit_{{ subTable.business_name }}_dao(query_db, sub_table) + {% endif %} + await query_db.commit() + return CrudResponseModel(is_success=True, message='更新成功') + except Exception as e: + await query_db.rollback() + raise e else: raise ServiceException(message='{{ functionName }}不存在') @@ -118,8 +141,12 @@ class {{ BusinessName }}Service: {{ pk_field }}_list = page_object.{{ pk_field }}s.split(',') try: for {{ pk_field }} in {{ pk_field }}_list: - {{ businessName }} = await cls.{{ businessName }}_detail_services(query_db, int({{ pk_field }})) await {{ BusinessName }}Dao.delete_{{ businessName }}_dao(query_db, {{ BusinessName }}Model({{ pkField }}={{ pk_field }})) + {% if table.sub %} + {{ businessName }} = await cls.{{ businessName }}_detail_services(query_db, int({{ pk_field }})) + for sub_table in {{ businessName }}.{{ subclassName }}_list: + await {{ BusinessName }}Dao.delete_{{ subTable.business_name }}_dao(query_db, sub_table) + {% endif %} await query_db.commit() return CrudResponseModel(is_success=True, message='删除成功') except Exception as e: @@ -146,7 +173,7 @@ class {{ BusinessName }}Service: return result @staticmethod - async def export_{{ businessName }}_list_services({{ businessName }}_list: List): + async def export_{{ businessName }}_list_services({% if dicts %}request: Request, {% endif %}{{ businessName }}_list: List): """ 导出{{ functionName }}信息service @@ -161,12 +188,22 @@ class {{ BusinessName }}Service: '{{ column.python_field }}': '{{ comment }}', {% endfor %} } - + {% if dicts %} + {% for dict_type in dicts.split(", ") %} + {{ dict_type[1:-1] }}_list = await DictDataService.query_dict_data_list_from_cache_services( + request.app.state.redis, dict_type={{ dict_type }} + ) + {{ dict_type[1:-1] }}_option = [dict(label=item.get('dictLabel'), value=item.get('dictValue')) for item in {{ dict_type[1:-1] }}_list] + {{ dict_type[1:-1] }}_option_dict = {item.get('value'): item for item in {{ dict_type[1:-1] }}_option} + {% endfor %} for item in {{ businessName }}_list: - if item.get('status') == '0': - item['status'] = '正常' - else: - item['status'] = '停用' + {% for column in columns %} + {% if column.dict_type %} + if str(item.get('{{ column.python_field }}')) in {{ column.dict_type }}_option_dict.keys(): + item['{{ column.python_field }}'] = {{ column.dict_type }}_option_dict.get(str(item.get('{{ column.python_field }}'))).get('label') + {% endif %} + {% endfor %} + {% endif %} binary_data = ExcelUtil.export_list2excel({{ businessName }}_list, mapping_dict) return binary_data diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 index c29a0cf..39b8421 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/vo.py.jinja2 @@ -2,13 +2,33 @@ {% set pk_field = pkColumn.python_field | camel_to_snake %} {% set pkParentheseIndex = pkColumn.column_comment.find("(") %} {% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %} +{% set vo_field_required = namespace(has_required=False) %} +{% for column in columns %} +{% if column.required %} + {% set vo_field_required.has_required = True %} +{% endif %} +{% endfor %} +{% if table.sub %} +{% set sub_vo_field_required = namespace(has_required=False) %} +{% for sub_column in subTable.columns %} +{% if sub_column.required %} + {% set sub_vo_field_required.has_required = True %} +{% endif %} +{% endfor %} +{% endif %} {% for vo_import in voImportList %} {{ vo_import }} {% endfor %} from pydantic import BaseModel, ConfigDict, Field from pydantic.alias_generators import to_camel +{% if vo_field_required.has_required %} from pydantic_validation_decorator import NotBlank +{% endif %} +{% if table.sub %} +from typing import List, Optional +{% else %} from typing import Optional +{% endif %} from module_admin.annotation.pydantic_annotation import as_query @@ -22,6 +42,9 @@ class {{ BusinessName }}Model(BaseModel): {% for column in columns %} {{ column.column_name }}: Optional[{{ column.python_type }}] = Field(default=None, description='{{ column.column_comment }}') {% endfor %} + {% if table.sub %} + {{ subclassName }}_list: Optional[List['{{ subTable.business_name | capitalize }}Model']] = Field(default=None, description='子表列信息') + {% endif %} {% for column in columns %} {% if column.required %} @@ -33,19 +56,49 @@ class {{ BusinessName }}Model(BaseModel): {% endif %} {% endfor %} + {% if vo_field_required.has_required %} def validate_fields(self): - {% set vo_field_required = namespace(has_required=False) %} {% for column in columns %} {% if column.required %} self.get_{{ column.column_name }}() - {% set vo_field_required.has_required = True %} {% endif %} {% endfor %} - {% if not vo_field_required.has_required %} - pass {% endif %} +{% if table.sub %} +class {{ subTable.business_name | capitalize }}Model(BaseModel): + """ + {{ subTable.function_name }}表对应pydantic模型 + """ + + model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) + + {% for sub_column in subTable.columns %} + {{ sub_column.column_name }}: Optional[{{ sub_column.python_type }}] = Field(default=None, description='{{ sub_column.column_comment}}') + {% endfor %} + + {% for sub_column in subTable.columns %} + {% if sub_column.required %} + {% set parentheseIndex = sub_column.column_comment.find("(") %} + {% set comment = sub_column.column_comment[:parentheseIndex] if parentheseIndex != -1 else sub_column.column_comment %} + @NotBlank(field_name='{{ sub_column.column_name }}', message='{{ comment }}不能为空') + def get_{{ sub_column.column_name }}(self): + return self.{{ sub_column.column_name }} + {% endif %} + {% endfor %} + + {% if sub_vo_field_required.has_required %} + def validate_fields(self): + {% for sub_column in subTable.columns %} + {% if sub_column.required %} + self.get_{{ sub_column.column_name }}() + {% endif %} + {% endfor %} + {% endif %} +{% endif %} + + class {{ BusinessName }}QueryModel({{ BusinessName }}Model): """ {{ functionName }}不分页查询模型 diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 index 67cb845..6a1af7d 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 @@ -127,6 +127,7 @@ {% else %} {% endif %} + {% endif %} {% endfor %} -- Gitee From fd07ad088ccb124fcf1c2b4c41b068ef73f4205e Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Tue, 18 Feb 2025 16:10:57 +0800 Subject: [PATCH 22/34] =?UTF-8?q?feat:=20=E4=BB=A3=E7=A0=81=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=96=B0=E5=A2=9E=E5=AD=97=E6=AE=B5=E5=94=AF=E4=B8=80?= =?UTF-8?q?=E6=80=A7=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module_generator/entity/do/gen_do.py | 1 + .../module_generator/entity/vo/gen_vo.py | 3 +++ .../module_generator/templates/python/dao.py.jinja2 | 4 ++-- .../module_generator/templates/python/do.py.jinja2 | 2 +- .../module_generator/templates/python/service.py.jinja2 | 8 ++++---- .../module_generator/templates/vue/index-tree.vue.jinja2 | 2 +- .../module_generator/templates/vue/index.vue.jinja2 | 2 +- .../templates/vue/v3/index-tree.vue.jinja2 | 2 +- .../module_generator/templates/vue/v3/index.vue.jinja2 | 2 +- ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql | 2 ++ ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql | 1 + ruoyi-fastapi-backend/utils/gen_util.py | 6 +++--- ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue | 5 +++++ 13 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py index bcc3c87..e64d0bf 100644 --- a/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py +++ b/ruoyi-fastapi-backend/module_generator/entity/do/gen_do.py @@ -55,6 +55,7 @@ class GenTableColumn(Base): is_pk = Column(String(1), nullable=True, comment='是否主键(1是)') is_increment = Column(String(1), nullable=True, comment='是否自增(1是)') is_required = Column(String(1), nullable=True, comment='是否必填(1是)') + is_unique = Column(String(1), nullable=True, comment='是否唯一(1是)') is_insert = Column(String(1), nullable=True, comment='是否为插入字段(1是)') is_edit = Column(String(1), nullable=True, comment='是否编辑字段(1是)') is_list = Column(String(1), nullable=True, comment='是否列表字段(1是)') diff --git a/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py index 34d8bbc..e5d7917 100644 --- a/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py +++ b/ruoyi-fastapi-backend/module_generator/entity/vo/gen_vo.py @@ -172,6 +172,7 @@ class GenTableColumnBaseModel(BaseModel): is_pk: Optional[str] = Field(default=None, description='是否主键(1是)') is_increment: Optional[str] = Field(default=None, description='是否自增(1是)') is_required: Optional[str] = Field(default=None, description='是否必填(1是)') + is_unique: Optional[str] = Field(default=None, description='是否唯一(1是)') is_insert: Optional[str] = Field(default=None, description='是否为插入字段(1是)') is_edit: Optional[str] = Field(default=None, description='是否编辑字段(1是)') is_list: Optional[str] = Field(default=None, description='是否列表字段(1是)') @@ -204,6 +205,7 @@ class GenTableColumnModel(GenTableColumnBaseModel): pk: Optional[bool] = Field(default=None, description='是否主键') increment: Optional[bool] = Field(default=None, description='是否自增') required: Optional[bool] = Field(default=None, description='是否必填') + unique: Optional[bool] = Field(default=None, description='是否唯一') insert: Optional[bool] = Field(default=None, description='是否为插入字段') edit: Optional[bool] = Field(default=None, description='是否编辑字段') list: Optional[bool] = Field(default=None, description='是否列表字段') @@ -217,6 +219,7 @@ class GenTableColumnModel(GenTableColumnBaseModel): self.pk = True if self.is_pk and self.is_pk == '1' else False self.increment = True if self.is_increment and self.is_increment == '1' else False self.required = True if self.is_required and self.is_required == '1' else False + self.unique = True if self.is_unique and self.is_unique == '1' else False self.insert = True if self.is_insert and self.is_insert == '1' else False self.edit = True if self.is_edit and self.is_edit == '1' else False self.list = True if self.is_list and self.is_list == '1' else False diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 index 25f902a..949683b 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/dao.py.jinja2 @@ -71,7 +71,7 @@ class {{ BusinessName }}Dao: select({{ ClassName }}).where( {% for column in columns %} {% if column.required %} - {{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }}, + {{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }} if {{ businessName }}.{{ column.python_field | camel_to_snake }} else True, {% endif %} {% endfor %} ) @@ -145,7 +145,7 @@ class {{ BusinessName }}Dao: :param {{ businessName }}: {{ functionName }}对象 :return: """ - db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump({%if table.sub %}exclude={'{{ subclassName }}_list'}{% endif %})) + db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump(exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list, '{% endif %}{% for column in columns %}{% if not column.insert %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %})) db.add(db_{{ businessName }}) await db.flush() diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 index a3775d8..2b34300 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/do.py.jinja2 @@ -15,7 +15,7 @@ class {{ ClassName }}(Base): __tablename__ = '{{ tableName }}' {% for column in columns %} - {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required %}nullable=True{% else %}nullable=False{% endif %}, comment='{{ column.column_comment }}') + {{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required or column.pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}') {% endfor %} {% if table.sub %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 index e291be8..53a97b9 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/python/service.py.jinja2 @@ -43,7 +43,7 @@ class {{ BusinessName }}Service: {% for column in columns %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} - {% if column.required %} + {% if column.unique %} @classmethod async def check_{{ column.python_field | camel_to_snake }}_unique_services(cls, query_db: AsyncSession, page_object: {{ BusinessName }}Model): """ @@ -74,7 +74,7 @@ class {{ BusinessName }}Service: {% for column in columns %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} - {% if column.required %} + {% if column.unique %} if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object): raise ServiceException(message=f'新增{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') {% endif %} @@ -103,13 +103,13 @@ class {{ BusinessName }}Service: :param page_object: 编辑{{ functionName }}对象 :return: 编辑{{ functionName }}校验结果 """ - edit_{{ businessName }} = page_object.model_dump(exclude_unset=True{% if table.sub %}, exclude={'{{ subclassName }}_list'}{% endif %}) + edit_{{ businessName }} = page_object.model_dump(exclude_unset=True, exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list, '{% endif %}{% for column in columns %}{% if not column.edit and not column.pk %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %}) {{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.{{ pk_field }}) if {{ businessName }}_info.{{ pk_field }}: {% for column in columns %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} - {% if column.required %} + {% if column.unique %} if not await cls.check_{{ column.python_field | camel_to_snake }}_unique_services(query_db, page_object): raise ServiceException(message=f'修改{{ functionName }}{page_object.{{ column.python_field | camel_to_snake }}}失败,{{ comment }}已存在') {% endif %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 index 6a1af7d..3297279 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index-tree.vue.jinja2 @@ -162,7 +162,7 @@ {% for column in columns %} {% set field = column.python_field %} {% if column.insert and not column.pk %} - {% if column.usable_column or column.super_column %} + {% if column.usable_column or not column.super_column %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} {% set dictType = column.dict_type %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 index e783e46..8076de2 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/index.vue.jinja2 @@ -174,7 +174,7 @@ {% for column in columns %} {% set field = column.python_field %} {% if column.insert and not column.pk %} - {% if column.usable_column or column.super_column %} + {% if column.usable_column or not column.super_column %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} {% set dictType = column.dict_type %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 index 64e8c8f..1488ea0 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index-tree.vue.jinja2 @@ -137,7 +137,7 @@ {% for column in columns %} {% set field = column.python_field %} {% if column.insert and not column.pk %} - {% if column.usable_column or column.super_column %} + {% if column.usable_column or not column.super_column %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} {% set dictType = column.dict_type %} diff --git a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 index a6d80f6..2a22216 100644 --- a/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 +++ b/ruoyi-fastapi-backend/module_generator/templates/vue/v3/index.vue.jinja2 @@ -160,7 +160,7 @@ {% for column in columns %} {% set field = column.python_field %} {% if column.insert and not column.pk %} - {% if column.usable_column or column.super_column %} + {% if column.usable_column or not column.super_column %} {% set parentheseIndex = column.column_comment.find("(") %} {% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %} {% set dictType = column.dict_type %} diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql index 70ee0c1..b3731aa 100644 --- a/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi-pg.sql @@ -918,6 +918,7 @@ create table gen_table_column ( is_pk char(1), is_increment char(1), is_required char(1), + is_unique char(1), is_insert char(1), is_edit char(1), is_list char(1), @@ -942,6 +943,7 @@ comment on column gen_table_column.python_field is 'PYTHON字段名'; comment on column gen_table_column.is_pk is '是否主键(1是)'; comment on column gen_table_column.is_increment is '是否自增(1是)'; comment on column gen_table_column.is_required is '是否必填(1是)'; +comment on column gen_table_column.is_unique is '是否唯一(1是)'; comment on column gen_table_column.is_insert is '是否为插入字段(1是)'; comment on column gen_table_column.is_edit is '是否编辑字段(1是)'; comment on column gen_table_column.is_list is '是否列表字段(1是)'; diff --git a/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql index 294e21c..d0ce11a 100644 --- a/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql +++ b/ruoyi-fastapi-backend/sql/ruoyi-fastapi.sql @@ -696,6 +696,7 @@ create table gen_table_column ( is_pk char(1) comment '是否主键(1是)', is_increment char(1) comment '是否自增(1是)', is_required char(1) comment '是否必填(1是)', + is_unique char(1) comment '是否唯一(1是)', is_insert char(1) comment '是否为插入字段(1是)', is_edit char(1) comment '是否编辑字段(1是)', is_list char(1) comment '是否列表字段(1是)', diff --git a/ruoyi-fastapi-backend/utils/gen_util.py b/ruoyi-fastapi-backend/utils/gen_util.py index 3f1e392..355e5d0 100644 --- a/ruoyi-fastapi-backend/utils/gen_util.py +++ b/ruoyi-fastapi-backend/utils/gen_util.py @@ -71,13 +71,13 @@ class GenUtils: column.is_insert = GenConstant.REQUIRE # 编辑字段 - if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.is_pk: + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not column.pk: column.is_edit = GenConstant.REQUIRE # 列表字段 - if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.is_pk: + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not column.pk: column.is_list = GenConstant.REQUIRE # 查询字段 - if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.is_pk: + if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not column.pk: column.is_query = GenConstant.REQUIRE # 查询字段类型 diff --git a/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue b/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue index 8739e0e..8c71824 100644 --- a/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue +++ b/ruoyi-fastapi-frontend/src/views/tool/gen/editTable.vue @@ -85,6 +85,11 @@ + + +