Compare commits

..

4 Commits
v1.10.7 ... dev

Author SHA1 Message Date
meilin.huang
400db0402a refactor: 包优化&其他问题修复 2026-01-25 14:16:16 +08:00
fanzhouqi
f0ae178183 !143 fix:mysql查询时,如果出现列名一样,会覆盖数据,github issues #124
* fix:mysql查询时,如果出现列名一样,会覆盖数据,github issues #124
2026-01-23 09:13:14 +00:00
meilin.huang
4641e448d2 fix: 数据库迁移、同步保存时定时任务未清除问题 2026-01-21 12:22:54 +08:00
meilin.huang
f0de65b7ce refactor: 协程启动优化、tagviews调整 2026-01-20 19:45:46 +08:00
87 changed files with 1272 additions and 957 deletions

View File

@@ -57,7 +57,7 @@ function build() {
execFileName="${execFileName}.exe"
fi
go mod tidy
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -ldflags=-w -o ${execFileName} main.go
CGO_ENABLE=0 GOOS=${os} GOARCH=${arch} go build -trimpath -ldflags=-w -o ${execFileName} main.go
if [ -d ${toFolder} ] ; then
echo_green "The desired folder already exists. Clear the folder"

View File

@@ -13,9 +13,6 @@ export function getBaseApiUrl() {
const config = {
baseApiUrl: `${(window as any).globalConfig.BaseApiUrl || location.protocol + '//' + getBaseApiUrl()}/api`,
baseWsUrl: `${(window as any).globalConfig.BaseWsUrl || `${location.protocol == 'https:' ? 'wss:' : 'ws:'}//${getBaseApiUrl()}`}/api`,
// 系统版本
version: 'v1.10.7',
};
export default config;

View File

@@ -4,7 +4,7 @@
<span class="logo-title">
{{ `${themeConfig.globalTitle}` }}
<sub
><span style="font-size: 10px; color: goldenrod">{{ ` ${config.version}` }}</span></sub
><span style="font-size: 10px; color: goldenrod">{{ ` ${themeConfig.version}` }}</span></sub
>
</span>
</div>
@@ -17,7 +17,6 @@
import { computed } from 'vue';
import { storeToRefs } from 'pinia';
import { useThemeConfig } from '@/store/themeConfig';
import config from '@/common/config';
const { themeConfig } = storeToRefs(useThemeConfig());

View File

@@ -308,17 +308,7 @@
<!-- 其它设置 -->
<el-divider content-position="left">{{ $t('layout.config.otherSetting') }}</el-divider>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.tagsStyle') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select v-model="themeConfig.tagsStyle" placeholder="请选择" size="small" style="width: 90px">
<el-option label="风格1" value="tags-style-one"></el-option>
<el-option label="风格2" value="tags-style-two"></el-option>
<el-option label="风格3" value="tags-style-three"></el-option>
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5">
<div class="layout-breadcrumb-seting-bar-flex mt-3.5!">
<div class="layout-breadcrumb-seting-bar-flex-label">{{ $t('layout.config.animation') }}</div>
<div class="layout-breadcrumb-seting-bar-flex-value">
<el-select v-model="themeConfig.animation" size="small" style="width: 90px">
@@ -328,7 +318,7 @@
</el-select>
</div>
</div>
<div class="layout-breadcrumb-seting-bar-flex !mt-3.5 !mb-5.5">
<div class="layout-breadcrumb-seting-bar-flex mt-3.5! mb-5.5!">
<div class="layout-breadcrumb-seting-bar-flex-label">
{{ $t('layout.config.columnsAsideStyle') }}
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div class="layout-navbars-tagsview" :class="{ 'layout-navbars-tagsview-shadow': themeConfig.layout === 'classic' }">
<el-scrollbar ref="scrollbarRef" @wheel.prevent="onHandleScroll">
<ul class="layout-navbars-tagsview-ul" :class="setTagsStyle" ref="tagsUlRef">
<ul class="layout-navbars-tagsview-ul" ref="tagsUlRef">
<li
v-for="(v, k) in tagsViews"
:key="k"
@@ -18,26 +18,20 @@
>
<SvgIcon :name="v.icon" class="layout-navbars-tagsview-ul-li-iconfont" v-if="themeConfig.isTagsviewIcon" />
<span>{{ $t(v.title) }}</span>
<template v-if="isActive(v)">
<SvgIcon
name="RefreshRight"
class="!text-[14px] ml-1 layout-navbars-tagsview-ul-li-refresh"
class="text-[14px]! ml-1 layout-navbars-tagsview-ul-li-icon layout-navbars-tagsview-ul-li-refresh"
@click.stop="refreshCurrentTagsView($route.fullPath)"
/>
<SvgIcon
name="Close"
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-active"
class="text-[14px]! layout-navbars-tagsview-ul-li-icon layout-navbars-tagsview-ul-li-close layout-icon-active"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</template>
<SvgIcon
name="Close"
class="!text-[14px] layout-navbars-tagsview-ul-li-icon layout-icon-three"
v-if="!v.isAffix"
@click.stop="closeCurrentTagsView(themeConfig.isShareTagsView ? v.path : v.path)"
/>
</li>
</ul>
</el-scrollbar>
@@ -46,7 +40,7 @@
</template>
<script lang="ts" setup name="layoutTagsView">
import { reactive, onMounted, computed, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
import { reactive, onMounted, ref, nextTick, onBeforeUpdate, getCurrentInstance, watch } from 'vue';
import { useRoute, useRouter, onBeforeRouteUpdate } from 'vue-router';
import screenfull from 'screenfull';
import { storeToRefs } from 'pinia';
@@ -105,11 +99,6 @@ const state = reactive({
},
});
// 动态设置 tagsView 风格样式
const setTagsStyle = computed(() => {
return themeConfig.value.tagsStyle;
});
// 存储 tagsViewList 到浏览器临时缓存中,页面刷新时,保留记录
const addBrowserSetSession = (tagsViewList: Array<object>) => {
setTagViews(tagsViewList);
@@ -403,163 +392,120 @@ onBeforeRouteUpdate((to) => {
});
</script>
<style scoped lang="scss">
<style scoped lang="css">
.layout-navbars-tagsview {
background-color: var(--bg-main-color);
border-bottom: 1px solid var(--el-border-color-light, #ebeef5);
position: relative;
z-index: 4;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
:deep(.el-scrollbar__wrap) {
overflow-x: auto !important;
}
.layout-navbars-tagsview :deep(.el-scrollbar__wrap) {
overflow-x: auto !important;
}
&-ul {
list-style: none;
margin: 0;
padding: 0;
height: 34px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
font-size: 12px;
white-space: nowrap;
padding: 0 15px;
.layout-navbars-tagsview-ul {
list-style: none;
margin: 0;
padding: 0;
height: 38px;
display: flex;
align-items: center;
color: var(--el-text-color-regular);
font-size: 13px;
white-space: nowrap;
padding: 0 15px;
}
&-li {
height: 26px;
line-height: 26px;
display: flex;
align-items: center;
border: 1px solid var(--el-border-color-lighter);
padding: 0 15px;
margin-right: 5px;
border-radius: 2px;
position: relative;
z-index: 0;
cursor: pointer;
justify-content: space-between;
.layout-navbars-tagsview-ul-li {
height: 30px;
line-height: 30px;
display: flex;
align-items: center;
border-radius: 6px;
padding: 0 12px;
margin-right: 8px;
position: relative;
z-index: 0;
cursor: pointer;
justify-content: space-between;
transition: all 0.3s ease;
border: 1px solid var(--el-border-color, #dcdfe6);
box-sizing: border-box;
background-color: var(--el-bg-color, #fafafa);
color: var(--el-text-color-regular, #606266);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
&:hover {
background-color: var(--el-color-primary-light-9);
color: var(--el-color-primary);
border-color: var(--el-color-primary-light-5);
}
.layout-navbars-tagsview-ul-li:not(.is-active):hover {
background-color: var(--el-fill-color-blank, #f5f7fa);
color: var(--el-text-color-primary, #303133);
border-color: var(--el-color-primary-light-7, #c6e2ff);
transform: translateY(-1px);
}
&-iconfont {
position: relative;
left: -5px;
font-size: 12px;
}
.layout-navbars-tagsview-ul-li-iconfont {
position: relative;
left: -3px;
font-size: 12px;
margin-right: 4px;
}
&-icon {
border-radius: 100%;
position: relative;
height: 14px;
width: 14px;
text-align: center;
line-height: 14px;
right: -5px;
.layout-navbars-tagsview-ul-li-icon {
border-radius: 4px;
position: relative;
height: 18px;
width: 18px;
text-align: center;
line-height: 18px;
right: -3px;
margin-left: 4px;
transition: all 0.25s ease;
color: var(--el-text-color-secondary, #909399);
display: flex;
align-items: center;
justify-content: center;
}
&:hover {
color: var(--el-color-white);
background-color: var(--el-color-primary-light-3);
}
}
.layout-navbars-tagsview-ul-li-icon:hover {
background-color: var(--el-color-info-light-7);
border-radius: 4px;
}
.layout-icon-active {
display: block;
}
.layout-icon-active {
display: flex;
align-items: center;
justify-content: center;
}
.layout-icon-three {
display: none;
}
}
.layout-navbars-tagsview-ul .is-active {
color: var(--el-color-primary, #409eff);
background: var(--el-color-primary-light-9, #ecf5ff);
border-color: var(--el-color-primary-light-5, #409eff);
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.2);
}
.is-active {
color: var(--el-color-white);
background: var(--el-color-primary);
border-color: var(--el-color-primary);
transition: border-color 3s ease;
}
}
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-icon {
color: var(--el-color-primary, #409eff);
}
// 风格2
.tags-style-two {
.layout-navbars-tagsview-ul-li {
margin-right: 0 !important;
border: none !important;
position: relative;
border-radius: 3px !important;
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-icon:hover {
background-color: var(--el-color-primary);
color: var(--el-color-white);
transform: scale(1.1);
}
.layout-icon-active {
display: none;
}
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-close:hover {
background-color: var(--el-color-danger);
color: var(--el-color-white);
border-radius: 4px;
}
.layout-icon-three {
display: block;
}
&:hover {
background: none !important;
}
}
.is-active {
background: none !important;
color: var(--el-color-primary) !important;
}
}
// 风格3
.tags-style-three {
align-items: flex-end;
.tgs-style-three-svg {
-webkit-mask-image:
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CgogPGc+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxwYXRoIHRyYW5zZm9ybT0icm90YXRlKC0wLjEzMzUwNiA1MC4xMTkyIDUwKSIgaWQ9InN2Z18xIiBkPSJtMTAwLjExOTE5LDEwMGMtNTUuMjI4LDAgLTEwMCwtNDQuNzcyIC0xMDAsLTEwMGwwLDEwMGwxMDAsMHoiIG9wYWNpdHk9InVuZGVmaW5lZCIgc3Ryb2tlPSJudWxsIiBmaWxsPSIjRjhFQUU3Ii8+CiAgPHBhdGggZD0ibS0wLjYzNzY2LDcuMzEyMjhjMC4xMTkxOSwwIDAuMjE3MzcsMC4wNTc5NiAwLjQ3Njc2LDAuMTE5MTljMC4yMzIsMC4wNTQ3NyAwLjI3MzI5LDAuMDM0OTEgMC4zNTc1NywwLjExOTE5YzAuMDg0MjgsMC4wODQyOCAwLjM1NzU3LDAgMC40NzY3NiwwbDAuMTE5MTksMGwwLjIzODM4LDAiIGlkPSJzdmdfMiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHBhdGggZD0ibTI4LjkyMTM0LDY5LjA1MjQ0YzAsMC4xMTkxOSAwLDAuMjM4MzggMCwwLjM1NzU3bDAsMC4xMTkxOWwwLDAuMTE5MTkiIGlkPSJzdmdfMyIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z180IiBoZWlnaHQ9IjAiIHdpZHRoPSIxLjMxMTA4IiB5PSI2LjgzNTUyIiB4PSItMC4wNDE3MSIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z181IiBoZWlnaHQ9IjEuNzg3ODQiIHdpZHRoPSIwLjExOTE5IiB5PSI2OC40NTY1IiB4PSIyOC45MjEzNCIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiAgPHJlY3QgaWQ9InN2Z182IiBoZWlnaHQ9IjQuODg2NzciIHdpZHRoPSIxOS4wNzAzMiIgeT0iNTEuMjkzMjEiIHg9IjM2LjY2ODY2IiBzdHJva2U9Im51bGwiIGZpbGw9Im5vbmUiLz4KIDwvZz4KPC9zdmc+'),
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNzAiIGhlaWdodD0iNzAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0ibm9uZSI+CiA8Zz4KICA8dGl0bGU+TGF5ZXIgMTwvdGl0bGU+CiAgPHBhdGggdHJhbnNmb3JtPSJyb3RhdGUoLTg5Ljc2MjQgNy4zMzAxNCA1NS4xMjUyKSIgc3Ryb2tlPSJudWxsIiBpZD0ic3ZnXzEiIGZpbGw9IiNGOEVBRTciIGQ9Im02Mi41NzQ0OSwxMTcuNTIwODZjLTU1LjIyOCwwIC0xMDAsLTQ0Ljc3MiAtMTAwLC0xMDBsMCwxMDBsMTAwLDB6IiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgogIDxwYXRoIGQ9Im0tMC42Mzc2Niw3LjMxMjI4YzAuMTE5MTksMCAwLjIxNzM3LDAuMDU3OTYgMC40NzY3NiwwLjExOTE5YzAuMjMyLDAuMDU0NzcgMC4yNzMyOSwwLjAzNDkxIDAuMzU3NTcsMC4xMTkxOWMwLjA4NDI4LDAuMDg0MjggMC4zNTc1NywwIDAuNDc2NzYsMGwwLjExOTE5LDBsMC4yMzgzOCwwIiBpZD0ic3ZnXzIiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxwYXRoIGQ9Im0yOC45MjEzNCw2OS4wNTI0NGMwLDAuMTE5MTkgMCwwLjIzODM4IDAsMC4zNTc1N2wwLDAuMTE5MTlsMCwwLjExOTE5IiBpZD0ic3ZnXzMiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNCIgaGVpZ2h0PSIwIiB3aWR0aD0iMS4zMTEwOCIgeT0iNi44MzU1MiIgeD0iLTAuMDQxNzEiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNSIgaGVpZ2h0PSIxLjc4Nzg0IiB3aWR0aD0iMC4xMTkxOSIgeT0iNjguNDU2NSIgeD0iMjguOTIxMzQiIHN0cm9rZT0ibnVsbCIgZmlsbD0ibm9uZSIvPgogIDxyZWN0IGlkPSJzdmdfNiIgaGVpZ2h0PSI0Ljg4Njc3IiB3aWR0aD0iMTkuMDcwMzIiIHk9IjUxLjI5MzIxIiB4PSIzNi42Njg2NiIgc3Ryb2tlPSJudWxsIiBmaWxsPSJub25lIi8+CiA8L2c+Cjwvc3ZnPg=='),
url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg'><rect rx='8' width='100%' height='100%' fill='%23F8EAE7'/></svg>");
-webkit-mask-size:
18px 30px,
20px 30px,
calc(100% - 30px) calc(100% + 17px);
-webkit-mask-position:
right bottom,
left bottom,
center top;
-webkit-mask-repeat: no-repeat;
}
.layout-navbars-tagsview-ul-li {
padding: 0 5px;
border-width: 15px 27px 15px;
border-style: solid;
border-color: transparent;
margin: 0 -15px;
.layout-icon-active {
display: none;
}
.layout-icon-three {
display: block;
}
&:hover {
@extend .tgs-style-three-svg;
background: var(--tagsview3-active-background-color);
color: unset;
}
}
.is-active {
@extend .tgs-style-three-svg;
background: var(--tagsview3-active-background-color) !important;
color: var(--el-color-primary) !important;
z-index: 1;
}
}
.layout-navbars-tagsview-ul .is-active .layout-navbars-tagsview-ul-li-refresh:hover {
background-color: var(--el-color-primary);
color: var(--el-color-white);
border-radius: 4px;
}
.layout-navbars-tagsview-shadow {

View File

@@ -98,8 +98,6 @@ export const useThemeConfig = defineStore('themeConfig', {
/* 其它设置
------------------------------- */
// 默认 Tagsview 风格,可选 1、 tags-style-one 2、 tags-style-two 3、 tags-style-three
tagsStyle: 'tags-style-three',
// 默认主页面切换动画,可选 1、 slide-right 2、 slide-left 3、 opacitys
animation: 'slide-right',
// 默认分栏高亮风格,可选 1、 圆角 columns-round 2、 卡片 columns-card
@@ -137,6 +135,7 @@ export const useThemeConfig = defineStore('themeConfig', {
appSlogan: 'common.appSlogan',
// 网站logo icon, base64编码内容
logoIcon: logoIcon,
version: 'latest',
// 默认初始语言,可选值"<zh-cn|en|zh-tw>",默认 zh-cn
globalI18n: 'zh-cn',
// 默认全局组件大小,可选值"<|large|default|small>",默认 ''
@@ -155,12 +154,13 @@ export const useThemeConfig = defineStore('themeConfig', {
if (tc) {
this.themeConfig = tc;
document.documentElement.style.cssText = getLocal('themeConfigStyle');
} else {
getServerConf().then((res) => {
this.themeConfig.globalI18n = res.i18n;
});
}
getServerConf().then((res) => {
this.themeConfig.globalI18n = res.i18n;
this.themeConfig.version = res.version;
});
// 根据后台系统配置初始化
getSysStyleConfig().then((res) => {
if (res?.title) {

View File

@@ -40,7 +40,6 @@ declare interface ThemeConfigState {
isInvert: boolean;
isWatermark: boolean;
watermarkText: Array<string>;
tagsStyle: string;
animation: string;
columnsAsideStyle: string;
layout: string;
@@ -49,6 +48,7 @@ declare interface ThemeConfigState {
globalViceTitle: string;
appSlogan: string;
logoIcon: string;
version: string;
globalI18n: string;
globalComponentSize: string;
terminalTheme: string;

View File

@@ -39,7 +39,7 @@
</div>
</div>
<el-splitter style="height: calc(100vh - 215px)" layout="vertical" @resize-end="onResizeTableHeight">
<el-splitter style="height: calc(100vh - 220px)" layout="vertical" @resize-end="onResizeTableHeight">
<el-splitter-panel :size="state.editorSize" max="80%">
<MonacoEditor ref="monacoEditorRef" class="mt-1" v-model="state.sql" language="sql" height="100%" :id="'MonacoTextarea-' + getKey()" />
</el-splitter-panel>
@@ -289,7 +289,7 @@ const onResizeTableHeight = (index: number, sizes: number[]) => {
editorHeight = plitpaneHeight / 2;
}
let tableDataHeight = plitpaneHeight - editorHeight - 43;
let tableDataHeight = plitpaneHeight - editorHeight - 47;
state.editorSize = editorHeight;
state.tableDataHeight = tableDataHeight + 'px';
@@ -486,6 +486,7 @@ const runSql = async (sql: string, remark = '', newTab = false) => {
state.execResTabs[i].tableColumn = colAndData.columns.map((x: any) => {
return {
columnName: x.name,
key: x.key,
columnType: x.type,
show: true,
};

View File

@@ -77,9 +77,7 @@
<!-- 排序箭头图标 -->
<SvgIcon
v-if="
column.title == nowSortColumn?.columnName &&
!showColumnActions[column.key] &&
!columnActionVisible[column.key]
column.key == nowSortColumn?.key && !showColumnActions[column.key] && !columnActionVisible[column.key]
"
:color="'var(--el-color-primary)'"
:name="nowSortColumn?.order == 'asc' ? 'top' : 'bottom'"
@@ -135,7 +133,7 @@
<div v-else @dblclick="onEnterEditMode(rowData, column, rowIndex, columnIndex)">
<div v-if="canEdit(rowIndex, columnIndex)">
<ColumnFormItem
v-model="rowData[column.dataKey!]"
v-model="rowData[column.key!]"
:data-type="column.dataType"
@blur="onExitEditMode(rowData, column, rowIndex)"
:column-name="column.columnName"
@@ -143,11 +141,11 @@
/>
</div>
<div v-else :class="isUpdated(rowIndex, column.dataKey) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
<span v-if="rowData[column.dataKey!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<div v-else :class="isUpdated(rowIndex, column.key) ? 'update_field_active ml-0.5 mr-0.5' : 'ml-0.5 mr-0.5'">
<span v-if="rowData[column.key!] === null" style="color: var(--el-color-info-light-5)"> NULL </span>
<span v-else :title="rowData[column.dataKey!]" class="el-text el-text--small is-truncated">
{{ rowData[column.dataKey!] }}
<span v-else :title="rowData[column.key!]" class="el-text el-text--small is-truncated">
{{ rowData[column.key!] }}
</span>
</div>
</div>
@@ -275,7 +273,7 @@ const columnActionVisible = ref({} as any);
const cmDataCopyCell = new ContextmenuItem('copyValue', 'common.copy')
.withIcon('CopyDocument')
.withOnClick(async (data: any) => {
await copyToClipboard(data.rowData[data.column.dataKey]);
await copyToClipboard(data.rowData[data.column.key]);
})
.withHideFunc(() => {
// 选中多条则隐藏该复制按钮
@@ -409,7 +407,6 @@ const dbConfig = useStorage('dbConfig', DbThemeConfig);
const rowNoColumn = {
title: 'No.',
key: 'tableDataRowNo',
dataKey: 'tableDataRowNo',
width: 45,
fixed: true,
align: 'center',
@@ -515,8 +512,6 @@ const setTableColumns = (columns: any) => {
x.remark = `${x.columnType} ${x.columnComment ? ' | ' + x.columnComment : ''}`;
return {
...x,
key: columnName,
dataKey: columnName,
width: DbInst.flexColumnWidth(columnName, state.datas),
title: columnName,
align: x.dataType == DataType.Number ? 'right' : 'left',
@@ -565,21 +560,21 @@ const hideColumnAction = () => {
const handleColumnCommand = (column: any, command: string) => {
switch (command) {
case 'sort-asc':
onTableSortChange({ columnName: column.dataKey, order: 'asc' });
onTableSortChange({ key: column.key, order: 'asc' });
break;
case 'sort-desc':
onTableSortChange({ columnName: column.dataKey, order: 'desc' });
onTableSortChange({ key: column.key, order: 'desc' });
break;
case 'fix':
state.columns.forEach((col: any) => {
if (col.dataKey == column.dataKey) {
if (col.key == column.key) {
col.fixed = true;
}
});
break;
case 'unfix':
state.columns.forEach((col: any) => {
if (col.dataKey == column.dataKey) {
if (col.key == column.key) {
col.fixed = false;
}
});
@@ -718,7 +713,7 @@ const onGenerateJson = async () => {
let obj: any = {};
for (let column of state.columns) {
if (column.show) {
obj[column.title] = selectionData[column.dataKey];
obj[column.title] = selectionData[column.key];
}
}
jsonObj.push(obj);
@@ -775,7 +770,7 @@ const onEnterEditMode = (rowData: any, column: any, rowIndex = 0, columnIndex =
nowUpdateCell.value = {
rowIndex: rowIndex,
colIndex: columnIndex,
oldValue: rowData[column.dataKey],
oldValue: rowData[column.key],
dataType: column.dataType,
};
};
@@ -785,7 +780,7 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
return;
}
const oldValue = nowUpdateCell.value.oldValue;
const newValue = rowData[column.dataKey];
const newValue = rowData[column.key];
// 未改变单元格值
if (oldValue == newValue) {
@@ -800,7 +795,7 @@ const onExitEditMode = (rowData: any, column: any, rowIndex = 0) => {
cellUpdateMap.value.set(rowIndex, updatedRow);
}
const columnName = column.dataKey;
const columnName = column.key;
let cellData = updatedRow.columnsMap.get(columnName);
if (cellData) {
// 多次修改情况,可能又修改回原值,则移除该修改单元格

View File

@@ -152,7 +152,7 @@
<el-text
id="copyValue"
style="color: var(--el-color-info-light-3)"
class="is-truncated !text-[12px] mt-1"
class="is-truncated text-[12px]! mt-1"
@click="copyToClipboard(sql)"
:title="sql"
>{{ sql }}</el-text
@@ -392,6 +392,7 @@ const selectData = async () => {
const columns = await getNowDbInst().loadColumns(props.dbName, props.tableName);
columns.forEach((x: any) => {
x.show = true;
x.key = x.columnName;
});
state.columns = columns;
}
@@ -592,7 +593,7 @@ const onSelectByCondition = async () => {
*/
const onTableSortChange = async (sort: any) => {
const sortType = sort.order == 'desc' ? 'DESC' : 'ASC';
state.orderBy = `ORDER BY ${state.dbDialect.quoteIdentifier(sort.columnName)} ${sortType}`;
state.orderBy = `ORDER BY ${state.dbDialect.quoteIdentifier(sort.key)} ${sortType}`;
await onRefresh();
};

View File

@@ -316,7 +316,7 @@ watch(dialogVisible, async (newValue: boolean) => {
srcTreeRef.value.setCheckedKeys(form.checkedKeys.split(','));
// 初始化默认值
form.cronAble = state.form.cronAble || 0;
form.cronAble = form.cronAble || -1;
form.mode = form.mode || 1;
form.extra = form.extra || { fileType: fileTypeOptions[0].value };

View File

@@ -85,12 +85,18 @@ const NodeTypeAuthCert = new NodeType(12)
(await node.ctx?.addResourceComponent(MachineOpComp)).openTerminal(node.params);
})
.withContextMenuItems([
new ContextmenuItem('term', 'machine.openTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
}),
new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal').withIcon('Monitor').withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params, true);
}),
new ContextmenuItem('term', 'machine.openTerminal')
.withIcon('Monitor')
.withPermission('machine:terminal')
.withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params);
}),
new ContextmenuItem('term-ex', 'machine.newTabOpenTerminal')
.withIcon('Monitor')
.withPermission('machine:terminal')
.withOnClick(async (node: TagTreeNode) => {
(await node.ctx?.addResourceComponent(MachineOpComp))?.openTerminal(node.params, true);
}),
new ContextmenuItem('files', 'machine.fileManage').withIcon('FolderOpened').withOnClick(async (node: any) => {
(await node.ctx?.addResourceComponent(MachineOpComp)).showFileManage(node.params);
}),

View File

@@ -32,7 +32,7 @@
<template #dropdown>
<el-dropdown-menu>
<template v-for="item in contextMenuItems" :key="item.clickId">
<el-dropdown-item v-if="!item.isHide(props.data)" :command="item">
<el-dropdown-item v-if="!item.isHide(props.data) && hasPerm(item.permission)" :command="item">
<SvgIcon v-if="item.icon" :name="item.icon" class="mr-1" />{{ $t(item.txt) }}
</el-dropdown-item>
</template>
@@ -54,6 +54,7 @@ import SvgIcon from '@/components/svgIcon/index.vue';
import { ContextmenuItem } from '@/components/contextmenu';
import { ResourceOpCtx, TagTreeNode } from '@/views/ops/component/tag';
import { ResourceOpCtxKey } from '@/views/ops/resource/resource';
import { hasPerm } from '@/components/auth/auth';
const resourceOpCtx: ResourceOpCtx | undefined = inject(ResourceOpCtxKey, undefined);

View File

@@ -18,20 +18,19 @@ jwt:
expire-time: 720
# refreshToken过期时间单位分钟
refresh-token-expire-time: 4320
# 资源密码aes加密key
aes:
key: 1111111111111111
# 若存在mysql配置优先使用mysql
mysql:
host: mysql:3306
# 数据库配置dialect支持mysql、sqlite
db:
dialect: mysql
address: mysql:3306
name: mayfly-go
username: root
password: 111049
db-name: mayfly-go
config: charset=utf8&loc=Local&parseTime=true
max-idle-conns: 5
sqlite:
path: ./mayfly-go.sqlite
max-idle-conns: 5
# db:
# dialect: sqlite
# address: ./mayfly-go.db
# max-idle-conns: 5
# 若同时部署多台机器则需要配置redis信息用于缓存权限码、验证码、公私钥等
# redis:
# host: localhost
@@ -55,3 +54,6 @@ log:
# max-age: 60
# # 是否使用 gzip 压缩方式压缩轮转后的日志文件
# compress: true
# 资源密码aes加密key
aes:
key: 1111111111111111

View File

@@ -76,9 +76,12 @@ func (aiAgent *AiAgent) Chat(ctx context.Context, sysPrompt string, question str
agentOption := []agent.AgentOption{}
go func() {
defer gox.RecoverPanic()
defer close(ch)
defer close(errCh)
defer gox.Recover(func(err error) {
errCh <- err
})
sr, err := aiAgent.Stream(ctx, []*schema.Message{
{
Role: schema.System,

View File

@@ -1,13 +1,13 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/ai/api"
"mayfly-go/pkg/starter"
)
func init() {
// 注册AI模块的IoC组件
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
api.InitIoc()
})
}

View File

@@ -13,6 +13,7 @@ import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/global"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/req"
"mayfly-go/pkg/utils/collx"
"mayfly-go/pkg/utils/netx"
@@ -57,7 +58,9 @@ func LastLoginCheck(ctx context.Context, account *sysentity.Account, accountLogi
res["refresh_token"] = refreshToken
// 不进行otp二次校验则直接返回accessToken
// 保存登录消息
go saveLogin(ctx, account, loginIp)
gox.Go(func() {
saveLogin(ctx, account, loginIp)
})
}
// 赋值otp状态

View File

@@ -1,14 +1,14 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/auth/api"
"mayfly-go/internal/auth/application"
"mayfly-go/internal/auth/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()

View File

@@ -1,12 +1,12 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/common/api"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
api.InitIoc()
})
}

View File

@@ -151,13 +151,13 @@ func (d *DbTransferTask) FileRun(rc *req.Ctx) {
filename, reader, err := d.fileApp.GetReader(context.TODO(), tFile.FileKey)
biz.ErrIsNil(err)
go func() {
defer gox.RecoverPanic()
biz.ErrIsNil(d.dbSqlExecApp.ExecReader(rc.MetaCtx, &dto.SqlReaderExec{
gox.GoCtx(rc.MetaCtx, func(ctx context.Context) {
biz.ErrIsNil(d.dbSqlExecApp.ExecReader(ctx, &dto.SqlReaderExec{
Reader: reader,
Filename: filename,
DbConn: targetDbConn,
ClientId: fm.ClientId,
}))
}()
})
}

View File

@@ -4,7 +4,6 @@ type DataSyncTaskForm struct {
Id uint64 `json:"id"`
TaskName string `binding:"required" json:"taskName"`
TaskCron string `binding:"required" json:"taskCron"`
TaskKey string `json:"taskKey"`
Status int `binding:"required" json:"status"`
SrcDbId int64 `binding:"required" json:"srcDbId"`

View File

@@ -71,7 +71,13 @@ func (app *dataSyncAppImpl) Save(ctx context.Context, taskEntity *entity.DataSyn
taskEntity.TaskKey = uuid.New().String()
err = app.Insert(ctx, taskEntity)
} else {
taskEntity.TaskKey = ""
if taskEntity.TaskKey == "" {
task, err := app.GetById(taskEntity.Id)
if err != nil {
return errorx.NewBiz("db sync task not found")
}
taskEntity.TaskKey = task.TaskKey
}
err = app.UpdateById(ctx, taskEntity)
}
if err != nil {
@@ -129,7 +135,7 @@ func (app *dataSyncAppImpl) Run(ctx context.Context, id uint64) error {
}
defer app.endRunning(task, syncLog)
defer gox.RecoverPanic(func(err error) {
defer gox.Recover(func(err error) {
syncLog.ErrText = i18n.T(imsg.DataSyncFailMsg, "msg", err.Error())
logx.ErrorContext(ctx, syncLog.ErrText)
syncLog.Status = entity.DataSyncTaskStateFail

View File

@@ -77,11 +77,19 @@ func (app *dbTransferAppImpl) Save(ctx context.Context, taskEntity *entity.DbTra
taskEntity.TaskKey = uuid.New().String()
err = app.Insert(ctx, taskEntity)
} else {
if taskEntity.TaskKey == "" {
task, err := app.GetById(taskEntity.Id)
if err != nil {
return errorx.NewBiz("db transfer task not found")
}
taskEntity.TaskKey = task.TaskKey
}
err = app.UpdateById(ctx, taskEntity)
}
if err != nil {
return err
}
app.addCronJob(ctx, taskEntity)
return nil
}
@@ -143,8 +151,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, e
// 标记该任务开始执行
app.MarkRunning(taskId)
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
// 获取源库连接、目标库连接判断连接可用性否则记录日志xx连接不可用
// 获取源库表信息
srcConn, err := app.dbApp.GetDbConn(ctx, uint64(task.SrcDbId), task.SrcDbName)
@@ -180,7 +187,7 @@ func (app *dbTransferAppImpl) Run(ctx context.Context, taskId uint64) (uint64, e
app.EndTransfer(ctx, logId, taskId, "error in transfer mode, only migrating to files or databases is currently supported", err, nil)
return
}
}()
})
return logId, nil
}
@@ -207,7 +214,7 @@ func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, logId uint64, tas
for _, tables := range tableGroups {
errGroup.Go(func() error {
defer gox.RecoverPanic()
defer gox.Recover()
if !app.IsRunning(taskId) {
return errorx.NewBiz("transfer stopped")
@@ -217,8 +224,7 @@ func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, logId uint64, tas
pr, pw := io.Pipe()
defer pr.Close()
go func() {
defer gox.RecoverPanic()
gox.Go(func () {
defer pw.Close()
err := app.dbApp.DumpDb(ctx, &dto.DumpDb{
LogId: logId,
@@ -254,7 +260,7 @@ func (app *dbTransferAppImpl) transfer2Db(ctx context.Context, logId uint64, tas
pr.CloseWithError(err)
return
}
}()
})
tx, _ := targetConn.Begin()
err = sqlparser.SQLSplit(pr, ';', func(stmt string) error {
@@ -317,7 +323,7 @@ func (app *dbTransferAppImpl) transfer2File(ctx context.Context, logId uint64, t
defer closeFunc(&err)
defer app.MarkStop(taskId)
defer app.logApp.Flush(logId, true)
defer gox.RecoverPanic(func(e error) {
defer gox.Recover(func(e error) {
err = e
app.EndTransfer(ctx, logId, taskId, "transfer to file panic", e, nil)
tFile.Status = entity.DbTransferFileStatusFail
@@ -385,7 +391,7 @@ func (app *dbTransferAppImpl) Stop(ctx context.Context, taskId uint64) error {
func (d *dbTransferAppImpl) TimerDeleteTransferFile() {
logx.Debug("start deleting transfer files periodically...")
scheduler.AddFun("@every 100m", func() {
defer gox.RecoverPanic()
defer gox.Recover()
dts, err := d.ListByCond(model.NewCond().Eq("mode", entity.DbTransferTaskModeFile).Ge("file_save_days", 1))
if err != nil {
logx.Errorf("the task to periodically get database transfer to file failed: %s", err.Error())
@@ -413,12 +419,6 @@ func (app *dbTransferAppImpl) addCronJob(ctx context.Context, taskEntity *entity
// 根据状态添加新的任务
if taskEntity.Status == entity.DbTransferTaskStatusEnable && taskEntity.CronAble == entity.DbTransferTaskCronAbleEnable {
if key == "" {
taskEntity.TaskKey = uuid.New().String()
key = taskEntity.TaskKey
_ = app.UpdateById(ctx, taskEntity)
}
taskId := taskEntity.Id
if err := scheduler.AddFunByKey(key, taskEntity.Cron, func() {
logx.Infof("start the transfer task: %d", taskId)

View File

@@ -5,6 +5,7 @@ import (
"database/sql"
"errors"
"fmt"
"strconv"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/logx"
@@ -49,7 +50,7 @@ func (d *DbConn) Ping() error {
stats := d.db.Stats()
logx.Debugf("[%s] db stats -> open: %d, idle: %d, inUse: %d, maxOpen: %d", d.Info.Name, stats.OpenConnections, stats.Idle, stats.InUse, stats.MaxOpenConnections)
if stats.OpenConnections == 0 {
logx.Infof("[%s] db stats: no open connections", d.Info.Name)
logx.Infof("[%s]-[%s] db stats: no open connections", d.Info.Name, d.Info.Database)
}
return d.db.Ping()
@@ -58,6 +59,7 @@ func (d *DbConn) Ping() error {
// 执行数据库查询返回的列信息
type QueryColumn struct {
Name string `json:"name"` // 列名
Key string `json:"key"` // 列唯一标识
Type string `json:"type"` // 数据类型
DbDataType *DbDataType `json:"-"`
@@ -67,6 +69,7 @@ type QueryColumn struct {
func NewQueryColumn(colName string, columnType *DbDataType) *QueryColumn {
return &QueryColumn{
Name: colName,
Key: colName,
Type: columnType.DataType.Name,
DbDataType: columnType,
valuer: columnType.DataType.Valuer(),
@@ -245,7 +248,12 @@ func (d *DbConn) walkQueryRows(ctx context.Context, selectSql string, walkFn Wal
rowData := make(map[string]any, lenCols)
// 把values中的数据复制到row中
for i := range scans {
rowData[cols[i].Name] = cols[i].value()
colname := cols[i].Name
if _, e := rowData[colname]; e {
colname = colname + strconv.Itoa(i)
cols[i].Key = colname
}
rowData[colname] = cols[i].value()
}
if err = walkFn(rowData, cols); err != nil {
logx.ErrorfContext(ctx, "[%s] cursor traversal query result set error, exit traversal: %s", selectSql, err.Error())

View File

@@ -39,8 +39,7 @@ func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 复制数据
if copy.CopyData {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
// 设置允许填充自增列之后,显示指定列名可以插入自增列\
identityInsert := fmt.Sprintf("set identity_insert \"%s\" on", newTableName)
// 获取列名
@@ -52,8 +51,7 @@ func (dd *DMDialect) CopyTable(copy *dbi.DbCopyTable) error {
columnStr := strings.Join(columnArr, ",")
// 插入新数据并显示指定列
_, _ = dd.dc.Exec(fmt.Sprintf("%s insert into \"%s\" (%s) select %s from \"%s\"", identityInsert, newTableName, columnStr, columnStr, tableName))
}()
})
}
return err
}

View File

@@ -43,8 +43,7 @@ func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
}
// 复制数据
if copy.CopyData {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
// 查询所有的列
columns, err := msMetadata.GetColumns(copy.TableName)
if err != nil {
@@ -73,7 +72,7 @@ func (md *MssqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
if err != nil {
logx.Warnf("复制表[%s]数据失败: %s", copy.TableName, err.Error())
}
}()
})
}
return err

View File

@@ -43,10 +43,9 @@ func (md *MysqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 复制数据
if copy.CopyData {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
_, _ = md.dc.Exec(fmt.Sprintf("insert into %s select * from %s", newTableName, tableName))
}()
})
}
return err
}

View File

@@ -27,10 +27,9 @@ func (pd *PgsqlDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 复制数据
if copy.CopyData {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
_, _ = pd.dc.Exec(fmt.Sprintf("insert into %s select * from %s", newTableName, tableName))
}()
})
}
// 查询旧表的自增字段名 重新设置新表的序列序列器

View File

@@ -37,11 +37,10 @@ func (sd *SqliteDialect) CopyTable(copy *dbi.DbCopyTable) error {
// 使用异步线程插入数据
if copy.CopyData {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
// 执行插入语句
_, _ = sd.dc.Exec(fmt.Sprintf("INSERT INTO \"%s\" SELECT * FROM \"%s\"", newTableName, tableName))
}()
})
}
return err

View File

@@ -1,22 +1,22 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/db/ai/tools"
"mayfly-go/internal/db/api"
"mayfly-go/internal/db/application"
"mayfly-go/internal/db/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(application.Init)
initialize.AddTerminateFunc(Terminate)
starter.AddInitFunc(application.Init)
starter.AddTerminateFunc(Terminate)
// 注册AI数据库工具
tools.Init()
}

View File

@@ -98,10 +98,9 @@ func (d *Container) GetContainersStats(rc *req.Ctx) {
var mu sync.Mutex
allStats := make([]vo.ContainerStats, 0)
for _, c := range cs {
go func(item container.Summary) {
defer gox.RecoverPanic()
gox.Go(func() {
defer wg.Done()
if item.State != "running" {
if c.State != "running" {
return
}
@@ -127,7 +126,7 @@ func (d *Container) GetContainersStats(rc *req.Ctx) {
mu.Lock()
allStats = append(allStats, cs)
mu.Unlock()
}(c)
})
}
wg.Wait()
@@ -224,8 +223,7 @@ func (d *Container) ContainerLogs(rc *req.Ctx) {
biz.ErrIsNil(err)
defer logs.Close()
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
for {
select {
case <-ctx.Done():
@@ -239,7 +237,7 @@ func (d *Container) ContainerLogs(rc *req.Ctx) {
}
}
}
}()
})
buf := make([]byte, 1024)
for {

View File

@@ -150,8 +150,7 @@ func (c Client) ContainerAttach(containerID string, wsConn *websocket.Conn, rows
wsConn.WriteMessage(websocket.TextMessage, []byte("\033[2J\033[3J\033[1;1H")) // 清屏
// 转发容器输出到前端
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
buf := make([]byte, 1024)
for {
select {
@@ -171,7 +170,7 @@ func (c Client) ContainerAttach(containerID string, wsConn *websocket.Conn, rows
wsConn.WriteMessage(websocket.TextMessage, buf[:n])
}
}
}()
})
for {
select {

View File

@@ -1,14 +1,14 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/docker/api"
"mayfly-go/internal/docker/application"
"mayfly-go/internal/docker/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()

View File

@@ -1,18 +1,18 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/es/api"
"mayfly-go/internal/es/application"
"mayfly-go/internal/es/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(application.Init)
starter.AddInitFunc(application.Init)
}

View File

@@ -1,14 +1,14 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/file/api"
"mayfly-go/internal/file/application"
"mayfly-go/internal/file/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()

View File

@@ -184,12 +184,11 @@ func (e *executionAppImpl) executeNode(ctx *ExecutionCtx) error {
// 执行节点逻辑
if node.IsAsync() {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
if err := node.Execute(ctx); err != nil {
logx.Errorf("async execute node error: %v, procinst_id: %d, node_key: %s", err, ctx.Procinst.Id, flowNode.Key)
}
}()
})
return nil
}

View File

@@ -1,18 +1,18 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/flow/api"
"mayfly-go/internal/flow/application"
"mayfly-go/internal/flow/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(application.Init)
starter.AddInitFunc(application.Init)
}

View File

@@ -16,6 +16,7 @@ import (
tagentity "mayfly-go/internal/tag/domain/entity"
"mayfly-go/pkg/biz"
"mayfly-go/pkg/global"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/model"
"mayfly-go/pkg/req"
@@ -378,7 +379,9 @@ func (m *Machine) WsGuacamole(rc *req.Ctx) {
defer tunnel.ReleaseWriter()
defer tunnel.ReleaseReader()
go guac.WsToGuacd(wsConn, tunnel, writer)
gox.Go(func() {
guac.WsToGuacd(wsConn, tunnel, writer)
})
guac.GuacdToWs(wsConn, tunnel, reader)
//OnConnect

View File

@@ -256,13 +256,11 @@ func (m *machineAppImpl) GetSshTunnelMachine(ctx context.Context, machineId int)
func (m *machineAppImpl) TimerUpdateStats() {
logx.Debug("start collecting and caching machine state information periodically...")
scheduler.AddFun("@every 2m", func() {
defer gox.RecoverPanic()
defer gox.Recover()
machineIds, _ := m.ListByCond(model.NewModelCond(&entity.Machine{Status: entity.MachineStatusEnable, Protocol: entity.MachineProtocolSsh}).Columns("id"))
for _, ma := range machineIds {
go func(mid uint64) {
defer gox.RecoverPanic(func(err error) {
logx.ErrorTrace(fmt.Sprintf("failed to get machine [id=%d] status information on time", mid), err)
})
gox.Go(func() {
mid := ma.Id
logx.Debugf("time to get machine [id=%d] status information start", mid)
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
@@ -273,7 +271,9 @@ func (m *machineAppImpl) TimerUpdateStats() {
}
cache.SaveMachineStats(mid, cli.GetAllStats())
logx.Debugf("time to get the machine [id=%d] status information end", mid)
}(ma.Id)
}, func(err error) {
logx.ErrorTrace(fmt.Sprintf("failed to get machine [id=%d] status information on time", ma.Id), err)
})
}
})
}

View File

@@ -136,7 +136,9 @@ func (m *machineCronJobAppImpl) RunCronJob(key string) {
})), "id")
for _, machine := range machines {
go m.runCronJob0(machine.Id, cronJob)
gox.Go(func() {
m.runCronJob0(machine.Id, cronJob)
})
}
}
@@ -150,7 +152,7 @@ func (m *machineCronJobAppImpl) addCronJob(mcj *entity.MachineCronJob) {
}
if err := scheduler.AddFunByKey(key, mcj.Cron, func() {
defer gox.RecoverPanic()
defer gox.Recover()
m.RunCronJob(key)
}); err != nil {
logx.ErrorTrace("add machine cron job failed", err)

View File

@@ -118,7 +118,7 @@ func (m *machineTermOpAppImpl) GetPageList(condition *entity.MachineTermOp, page
func (m *machineTermOpAppImpl) TimerDeleteTermOp() {
logx.Debug("start deleting machine terminal playback records every hour...")
scheduler.AddFun("@every 60m", func() {
defer gox.RecoverPanic()
defer gox.Recover()
startDate := time.Now().AddDate(0, 0, -config.GetMachine().TermOpSaveDays)
cond := &entity.MachineTermOpQuery{
StartCreateTime: &startDate,

View File

@@ -1,6 +1,7 @@
package guac
import (
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"sync"
"time"
@@ -72,7 +73,9 @@ func NewTunnelMap() *TunnelMap {
tunnelMap: make(map[string]*LastAccessedTunnel),
tunnelTimeout: TunnelTimeout,
}
go tunnelMap.tunnelTimeoutTask()
gox.Go(func() {
tunnelMap.tunnelTimeoutTask()
})
return tunnelMap
}
@@ -116,7 +119,6 @@ func (m *TunnelMap) tunnelTimeoutTaskRun() {
}
}
m.Unlock()
return
}
// Get returns the Tunnel having the given UUID, wrapped within a LastAccessedTunnel.

View File

@@ -1,18 +1,18 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/machine/api"
"mayfly-go/internal/machine/application"
"mayfly-go/internal/machine/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(application.Init)
starter.AddInitFunc(application.Init)
}

View File

@@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/pool"
"mayfly-go/pkg/utils/netx"
@@ -107,7 +108,9 @@ func (stm *SshTunnelMachine) OpenSshTunnel(id string, ip string, port int) (expo
remotePort: port,
listener: listener,
}
go tunnel.Open(stm.SshClient)
gox.Go(func() {
tunnel.Open(stm.SshClient)
})
stm.tunnels[tunnel.id] = tunnel
return localHost, localPort, nil

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"github.com/spf13/cast"
@@ -95,8 +96,12 @@ func NewTerminalSession(param *CreateTerminalSessionParam) (*TerminalSession, er
}
func (r TerminalSession) Start() {
go r.readFromTerminal()
go r.writeToWebsocket()
gox.Go(func() {
r.readFromTerminal()
})
gox.Go(func() {
r.writeToWebsocket()
})
r.receiveWsMsg()
}

View File

@@ -1,14 +1,14 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/mongo/api"
"mayfly-go/internal/mongo/application"
"mayfly-go/internal/mongo/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()

View File

@@ -195,9 +195,9 @@ func (m *msgTmplAppImpl) SendMsg(ctx context.Context, mts *dto.MsgTmplSend) erro
logx.Warnf("channel is disabled => %s", channel.Code)
continue
}
go func(ch entity.MsgChannel) {
defer gox.RecoverPanic()
gox.Go(func() {
ch := *channel
if err := msgx.Send(ctx, &msgx.Channel{
Type: ch.Type,
Name: ch.Name,
@@ -206,7 +206,7 @@ func (m *msgTmplAppImpl) SendMsg(ctx context.Context, mts *dto.MsgTmplSend) erro
}, msg); err != nil {
logx.Errorf("send msg error => channel=%s, msg=%s, err -> %v", ch.Code, msg.Content, err)
}
}(*channel)
})
}
return nil

View File

@@ -2,7 +2,6 @@ package init
import (
"context"
"mayfly-go/initialize"
"mayfly-go/internal/msg/api"
"mayfly-go/internal/msg/application"
"mayfly-go/internal/msg/application/dto"
@@ -13,16 +12,17 @@ import (
"mayfly-go/pkg/global"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(Init)
starter.AddInitFunc(Init)
}
func Init() {

View File

@@ -2,7 +2,7 @@ package config
import (
"fmt"
"mayfly-go/pkg/utils/assert"
"mayfly-go/pkg/starter"
"mayfly-go/pkg/utils/cryptox"
)
@@ -20,11 +20,15 @@ func (a *Aes) DecryptBase64(data string) ([]byte, error) {
return cryptox.AesDecryptBase64(data, []byte(a.Key))
}
func (a *Aes) Valid() {
func (a *Aes) ApplyDefaults() error {
if a.Key == "" {
return
return nil
}
aesKeyLen := len(a.Key)
assert.IsTrue(aesKeyLen == 16 || aesKeyLen == 24 || aesKeyLen == 32,
fmt.Sprintf("config.yml之 [aes.key] 长度需为16、24、32位长度, 当前为%d位", aesKeyLen))
if aesKeyLen != 16 && aesKeyLen != 24 && aesKeyLen != 32 {
return starter.NewConfigError("aes.key", fmt.Sprintf("长度需为16、24、32位长度, 当前为%d位", aesKeyLen))
}
return nil
}

View File

@@ -1,12 +1,5 @@
package config
import "fmt"
const (
AppName = "mayfly-go"
Version = "v1.10.7"
Version = "v1.10.9"
)
func GetAppInfo() string {
return fmt.Sprintf("[%s:%s]", AppName, Version)
}

View File

@@ -4,24 +4,17 @@ import (
"flag"
"fmt"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/starter"
"mayfly-go/pkg/utils/ymlx"
"os"
"path/filepath"
"strconv"
)
type ConfigItem interface {
// 验证配置
Valid()
// 如果不存在配置值,则设置默认值
Default()
}
// 配置文件映射对象
var Conf *Config
func Init() {
func Init() (*Config, error) {
configFilePath := flag.String("e", "./config.yml", "配置文件路径,默认为可执行文件目录")
flag.Parse()
// 获取启动参数中,配置文件的绝对路径
@@ -35,10 +28,12 @@ func Init() {
// 尝试使用系统环境变量替换配置信息
yc.ReplaceOsEnv()
yc.IfBlankDefaultValue()
// 校验配置文件内容信息
yc.Valid()
if err := yc.ApplyDefaults(); err != nil {
return nil, err
}
Conf = yc
return yc, nil
}
// 启动配置参数
@@ -48,39 +43,23 @@ type CmdConfigParam struct {
// yaml配置文件映射对象
type Config struct {
Server Server `yaml:"server"`
Jwt Jwt `yaml:"jwt"`
Aes Aes `yaml:"aes"`
Mysql Mysql `yaml:"mysql"`
Sqlite Sqlite `yaml:"sqlite"`
Redis Redis `yaml:"redis"`
Log Log `yaml:"log"`
starter.Conf `yaml:",inline"`
Aes Aes `yaml:"aes"`
}
func (c *Config) IfBlankDefaultValue() {
c.Log.Default()
// 优先初始化log因为后续的一些default方法中会需要用到。统一日志输出
logx.Init(logx.Config{
Level: c.Log.Level,
Type: c.Log.Type,
AddSource: c.Log.AddSource,
Filename: c.Log.File.Name,
Filepath: c.Log.File.Path,
MaxSize: c.Log.File.MaxSize,
MaxAge: c.Log.File.MaxAge,
Compress: c.Log.File.Compress,
})
var _ starter.ConfigItem = (*Config)(nil)
c.Server.Default()
c.Jwt.Default()
c.Mysql.Default()
c.Sqlite.Default()
}
func (c *Config) ApplyDefaults() error {
if err := c.Conf.ApplyDefaults(); err != nil {
return err
}
// 配置文件内容校验
func (c *Config) Valid() {
c.Jwt.Valid()
c.Aes.Valid()
if err := c.Aes.ApplyDefaults(); err != nil {
return err
}
return nil
}
// 替换系统环境变量,如果环境变量中存在该值,则优先使用环境变量设定的值
@@ -96,27 +75,29 @@ func (c *Config) ReplaceOsEnv() {
dbHost := os.Getenv("MAYFLY_DB_HOST")
if dbHost != "" {
c.Mysql.Host = dbHost
c.DB.Address = dbHost
c.DB.Dialect = starter.DialectMySQL
}
dbName := os.Getenv("MAYFLY_DB_NAME")
if dbName != "" {
c.Mysql.Dbname = dbName
c.DB.Name = dbName
}
dbUser := os.Getenv("MAYFLY_DB_USER")
if dbUser != "" {
c.Mysql.Username = dbUser
c.DB.Username = dbUser
}
dbPwd := os.Getenv("MAYFLY_DB_PASS")
if dbPwd != "" {
c.Mysql.Password = dbPwd
c.DB.Password = dbPwd
}
sqlitePath := os.Getenv("MAYFLY_SQLITE_PATH")
if sqlitePath != "" {
c.Sqlite.Path = sqlitePath
c.DB.Address = sqlitePath
c.DB.Dialect = starter.DialectSQLite
}
aesKey := os.Getenv("MAYFLY_AES_KEY")

View File

@@ -1,34 +0,0 @@
package config
import (
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/assert"
"mayfly-go/pkg/utils/stringx"
)
type Jwt struct {
Key string `yaml:"key"`
ExpireTime uint64 `yaml:"expire-time"` // 过期时间,单位分钟
RefreshTokenExpireTime uint64 `yaml:"refresh-token-expire-time"` // 刷新token的过期时间单位分钟
}
func (j *Jwt) Default() {
if j.Key == "" {
// 如果配置文件中的jwt key为空则随机生成字符串
j.Key = stringx.Rand(32)
logx.Warnf("未配置jwt.key, 随机生成key为: %s", j.Key)
}
if j.ExpireTime == 0 {
j.ExpireTime = 1440
logx.Warnf("未配置jwt.expire-time, 默认值: %d", j.ExpireTime)
}
if j.RefreshTokenExpireTime == 0 {
j.RefreshTokenExpireTime = j.ExpireTime * 5
logx.Warnf("未配置jwt.refresh-token-expire-time, 默认值: %d", j.RefreshTokenExpireTime)
}
}
func (j *Jwt) Valid() {
assert.IsTrue(j.ExpireTime != 0, "config.yml之[jwt.expire-time] 不能为空")
}

View File

@@ -1,33 +0,0 @@
package config
import (
"mayfly-go/pkg/logx"
)
type Log struct {
Level string `yaml:"level"`
Type string `yaml:"type"`
AddSource bool `yaml:"add-source"`
File LogFile `yaml:"file"`
}
func (l *Log) Default() {
if l.Level == "" {
l.Level = "info"
logx.Warnf("未配置log.level, 默认值: %s", l.Level)
}
if l.Type == "" {
l.Type = "text"
}
if l.File.Name == "" {
l.File.Name = "mayfly-go.log"
}
}
type LogFile struct {
Name string `yaml:"name"`
Path string `yaml:"path"`
MaxSize int `yaml:"max-size"`
MaxAge int `yaml:"max-age"`
Compress bool `yaml:"compress"`
}

View File

@@ -1,35 +0,0 @@
package config
import "mayfly-go/pkg/logx"
type Mysql struct {
Host string `mapstructure:"path" json:"host" yaml:"host"`
Config string `mapstructure:"config" json:"config" yaml:"config"`
Dbname string `mapstructure:"db-name" json:"dbname" yaml:"db-name"`
Username string `mapstructure:"username" json:"username" yaml:"username"`
Password string `mapstructure:"password" json:"password" yaml:"password"`
MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"`
MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"`
LogMode bool `mapstructure:"log-mode" json:"logMode" yaml:"log-mode"`
LogZap string `mapstructure:"log-zap" json:"logZap" yaml:"log-zap"`
}
func (m *Mysql) Default() {
if m.Host == "" {
m.Host = "localhost:3306"
logx.Warnf("[使用sqlite可忽略]未配置mysql.host, 默认值: %s", m.Host)
}
if m.Config == "" {
m.Config = "charset=utf8&loc=Local&parseTime=true"
}
if m.MaxIdleConns == 0 {
m.MaxIdleConns = 5
}
if m.MaxOpenConns == 0 {
m.MaxOpenConns = m.MaxIdleConns
}
}
func (m *Mysql) Dsn() string {
return m.Username + ":" + m.Password + "@tcp(" + m.Host + ")/" + m.Dbname + "?" + m.Config
}

View File

@@ -1,8 +0,0 @@
package config
type Redis struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Password string `yaml:"password"`
Db int `yaml:"db"`
}

View File

@@ -1,49 +0,0 @@
package config
import (
"fmt"
"mayfly-go/pkg/i18n"
)
type Server struct {
Lang string `yaml:"lang"`
Port int `yaml:"port"`
Model string `yaml:"model"`
ContextPath string `yaml:"context-path"` // 请求路径上下文
Cors bool `yaml:"cors"`
Tls *Tls `yaml:"tls"`
Static *[]*Static `yaml:"static"`
StaticFile *[]*StaticFile `yaml:"static-file"`
}
func (s *Server) Default() {
if s.Lang == "" {
s.Lang = i18n.Zh_CN
}
if s.Model == "" {
s.Model = "release"
}
if s.Port == 0 {
s.Port = 18888
}
}
func (s *Server) GetPort() string {
return fmt.Sprintf(":%d", s.Port)
}
type Static struct {
RelativePath string `yaml:"relative-path"`
Root string `yaml:"root"`
}
type StaticFile struct {
RelativePath string `yaml:"relative-path"`
Filepath string `yaml:"filepath"`
}
type Tls struct {
Enable bool `yaml:"enable"` // 是否启用tls
KeyFile string `yaml:"key-file"` // 私钥文件路径
CertFile string `yaml:"cert-file"` // 证书文件路径
}

View File

@@ -1,22 +0,0 @@
package config
import "mayfly-go/pkg/logx"
type Sqlite struct {
Path string `mapstructure:"path" json:"path" yaml:"path"`
MaxIdleConns int `mapstructure:"max-idle-conns" json:"maxIdleConns" yaml:"max-idle-conns"`
MaxOpenConns int `mapstructure:"max-open-conns" json:"maxOpenConns" yaml:"max-open-conns"`
}
func (m *Sqlite) Default() {
if m.Path == "" {
m.Path = "./mayfly-go.sqlite"
logx.Warnf("[使用mysql可忽略]未配置sqlite.path, 默认值: %s", m.Path)
}
if m.MaxIdleConns == 0 {
m.MaxIdleConns = 5
}
if m.MaxOpenConns == 0 {
m.MaxOpenConns = m.MaxIdleConns
}
}

View File

@@ -1,20 +0,0 @@
package starter
import (
"fmt"
"mayfly-go/internal/pkg/config"
"mayfly-go/pkg/logx"
"os"
"runtime/debug"
)
func printBanner() {
buildInfo, _ := debug.ReadBuildInfo()
logx.Print(fmt.Sprintf(`
__ _
_ __ ___ __ _ _ _ / _| |_ _ __ _ ___
| '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \
| | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) | version: %s | go_version: %s | pid: %d
|_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/
|___/ |___/ |___/ `, config.Version, buildInfo.GoVersion, os.Getpid()))
}

View File

@@ -1,47 +0,0 @@
package starter
import (
"context"
"fmt"
"mayfly-go/internal/pkg/config"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/rediscli"
"github.com/redis/go-redis/v9"
)
// 有配置redis信息则初始化redis。多台机器部署需要使用redis存储验证码、权限、公私钥等信息
func initCache() {
redisCli := connRedis()
if redisCli == nil {
logx.Info("no redis configuration, using local cache")
return
}
logx.Info("redis connected successfully, using Redis for caching")
rediscli.SetCli(connRedis())
cache.SetCache(cache.NewRedisCache(redisCli))
}
func connRedis() *redis.Client {
// 设置redis客户端
redisConf := config.Conf.Redis
if redisConf.Host == "" {
// logx.Panic("未找到redis配置信息")
return nil
}
logx.Infof("redis connecting [%s:%d]", redisConf.Host, redisConf.Port)
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", redisConf.Host, redisConf.Port),
Password: redisConf.Password, // no password set
DB: redisConf.Db, // use default DB
})
// 测试连接
_, e := rdb.Ping(context.TODO()).Result()
if e != nil {
logx.Panicf("redis connection faild! [%s:%d][%s]", redisConf.Host, redisConf.Port, e.Error())
}
return rdb
}

View File

@@ -1,93 +0,0 @@
package starter
import (
"log"
"mayfly-go/internal/pkg/config"
"mayfly-go/pkg/global"
"mayfly-go/pkg/logx"
"time"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
func initDb() {
global.Db = initGormDb()
}
func initGormDb() *gorm.DB {
m := config.Conf.Mysql
// 存在msyql数据库名则优先使用mysql
if m.Dbname != "" {
return initMysql(m)
}
return initSqlite(config.Conf.Sqlite)
}
func initMysql(m config.Mysql) *gorm.DB {
logx.Infof("connecting to mysql [%s]", m.Host)
mysqlConfig := mysql.Config{
DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据版本自动配置
}
if db, err := gorm.Open(mysql.New(mysqlConfig), getGormConfig()); err != nil {
logx.Panicf("failed to connect to mysql! [%s]", err.Error())
return nil
} else {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(m.MaxIdleConns)
sqlDB.SetMaxOpenConns(m.MaxOpenConns)
// 如果是开发环境时打印sql语句
if logx.GetConfig().IsDebug() {
db = db.Debug()
}
return db
}
}
func initSqlite(sc config.Sqlite) *gorm.DB {
logx.Infof("connecting to sqlite [%s]", sc.Path)
if db, err := gorm.Open(sqlite.Open(sc.Path), getGormConfig()); err != nil {
logx.Panicf("failed to connect to sqlite! [%s]", err.Error())
return nil
} else {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(sc.MaxIdleConns)
sqlDB.SetMaxOpenConns(sc.MaxOpenConns)
return db
}
}
func getGormConfig() *gorm.Config {
sqlLogLevel := logger.Error
logConf := logx.GetConfig()
// 如果为配置文件中配置的系统日志级别为debug则打印gorm执行的sql信息
if logConf.IsDebug() {
sqlLogLevel = logger.Info
}
gormLogger := logger.New(
log.New(logConf.GetLogOut(), "\r\n", log.LstdFlags), // io writer日志输出的目标前缀和日志包含的内容——译者注
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: sqlLogLevel, // 日志级别, 改为logger.Info即可显示sql语句
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound记录未找到错误
Colorful: false, // 禁用彩色打印
},
)
return &gorm.Config{NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_",
SingularTable: true,
}, Logger: gormLogger}
}

View File

@@ -1,61 +0,0 @@
package starter
import (
"context"
"mayfly-go/initialize"
"mayfly-go/internal/pkg/config"
"mayfly-go/migration"
"mayfly-go/pkg/global"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/validatorx"
"os"
"os/signal"
"syscall"
)
func RunWebServer() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
cancel()
}()
// 初始化config.yml配置文件映射信息或使用环境变量。并初始化系统日志相关配置
config.Init()
// 打印banner
printBanner()
// 初始化并赋值数据库全局变量
initDb()
// 初始化缓存
initCache()
// 数据库升级操作
if err := migration.RunMigrations(global.Db); err != nil {
logx.Panicf("db migration failed: %v", err)
}
// 参数校验器初始化、如错误提示中文转译等
validatorx.Init()
// 注册自定义正则表达式校验规则
RegisterCustomPatterns()
// 初始化其他需要启动时运行的方法
initialize.InitOther()
// 运行web服务
runWebServer(ctx)
}
// 注册自定义正则表达式校验规则
func RegisterCustomPatterns() {
// 账号用户名校验
validatorx.RegisterPattern("account_username", "^[a-zA-Z0-9_]{5,20}$", "只允许输入5-20位大小写字母、数字、下划线")
validatorx.RegisterPattern("resource_code", "^[a-zA-Z0-9_\\-.:]{1,32}$", "只允许输入1-32位大小写字母、数字、_-.:")
}

View File

@@ -1,128 +0,0 @@
package starter
import (
"context"
"errors"
"io/fs"
"mayfly-go/initialize"
"mayfly-go/internal/pkg/config"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/middleware"
"mayfly-go/pkg/req"
"mayfly-go/static"
"net/http"
"time"
sysapp "mayfly-go/internal/sys/application"
"github.com/gin-gonic/gin"
)
func runWebServer(ctx context.Context) {
// 设置gin日志输出器
logOut := logx.GetConfig().GetLogOut()
gin.DefaultErrorWriter = logOut
gin.DefaultWriter = logOut
// 权限处理器
req.UseBeforeHandlerInterceptor(req.PermissionHandler)
// 日志处理器
req.UseAfterHandlerInterceptor(req.LogHandler)
// 设置日志保存函数
req.SetSaveLogFunc(sysapp.GetSyslogApp().SaveFromReq)
// jwt配置
jwtConf := config.Conf.Jwt
req.SetJwtConf(req.JwtConf{
Key: jwtConf.Key,
ExpireTime: jwtConf.ExpireTime,
RefreshTokenExpireTime: jwtConf.RefreshTokenExpireTime,
})
// i18n配置
i18n.SetLang(config.Conf.Server.Lang)
// server配置
serverConfig := config.Conf.Server
gin.SetMode(serverConfig.Model)
var router = gin.New()
router.MaxMultipartMemory = 8 << 20
// 初始化接口路由
initialize.InitRouter(router, initialize.RouterConfig{ContextPath: serverConfig.ContextPath})
// 设置静态资源
setStatic(serverConfig.ContextPath, router)
// 是否允许跨域
if serverConfig.Cors {
router.Use(middleware.Cors())
}
srv := http.Server{
Addr: config.Conf.Server.GetPort(),
// 注册路由
Handler: router,
}
go func() {
defer gox.RecoverPanic()
<-ctx.Done()
logx.Info("Shutdown HTTP Server ...")
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := srv.Shutdown(timeout)
if err != nil {
logx.Errorf("failed to Shutdown HTTP Server: %v", err)
}
initialize.Terminate()
}()
confSrv := config.Conf.Server
logx.Infof("Listening and serving HTTP on %s", srv.Addr+confSrv.ContextPath)
var err error
if confSrv.Tls != nil && confSrv.Tls.Enable {
err = srv.ListenAndServeTLS(confSrv.Tls.CertFile, confSrv.Tls.KeyFile)
} else {
err = srv.ListenAndServe()
}
if errors.Is(err, http.ErrServerClosed) {
logx.Info("HTTP Server Shutdown")
} else if err != nil {
logx.Errorf("Failed to Start HTTP Server: %v", err)
}
}
func setStatic(contextPath string, router *gin.Engine) {
// 使用embed打包静态资源至二进制文件中
fsys, _ := fs.Sub(static.Static, "static")
fileServer := http.FileServer(http.FS(fsys))
handler := WrapStaticHandler(http.StripPrefix(contextPath, fileServer))
router.GET(contextPath+"/", handler)
router.GET(contextPath+"/favicon.ico", handler)
router.GET(contextPath+"/config.js", handler)
// 所有/assets/**开头的都是静态资源文件
router.GET(contextPath+"/assets/*file", handler)
// 设置静态资源
if staticConfs := config.Conf.Server.Static; staticConfs != nil {
for _, scs := range *staticConfs {
router.StaticFS(scs.RelativePath, http.Dir(scs.Root))
}
}
// 设置静态文件
if staticFileConfs := config.Conf.Server.StaticFile; staticFileConfs != nil {
for _, sfs := range *staticFileConfs {
router.StaticFile(sfs.RelativePath, sfs.Filepath)
}
}
}
func WrapStaticHandler(h http.Handler) gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Cache-Control", `public, max-age=31536000`)
h.ServeHTTP(c.Writer, c.Request)
}
}

View File

@@ -1,18 +1,18 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/redis/api"
"mayfly-go/internal/redis/application"
"mayfly-go/internal/redis/infra/persistence"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(application.Init)
starter.AddInitFunc(application.Init)
}

View File

@@ -63,5 +63,5 @@ func (c *Config) SaveConfig(rc *req.Ctx) {
// GetServerConfig 获取当前系统启动配置
func (c *Config) GetServerConfig(rc *req.Ctx) {
conf := config.Conf
rc.ResData = collx.Kvs("i18n", conf.Server.Lang)
rc.ResData = collx.Kvs("i18n", conf.Server.Lang, "version", config.Version)
}

View File

@@ -1,16 +1,20 @@
package init
import (
"mayfly-go/initialize"
"mayfly-go/internal/sys/api"
"mayfly-go/internal/sys/application"
"mayfly-go/internal/sys/infra/persistence"
"mayfly-go/pkg/starter"
"mayfly-go/pkg/validatorx"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
// 账号用户名校验
validatorx.RegisterPattern("account_username", "^[a-zA-Z0-9_]{5,20}$", "只允许输入5-20位大小写字母、数字、下划线")
}

View File

@@ -2,23 +2,23 @@ package init
import (
"context"
"mayfly-go/initialize"
"mayfly-go/internal/pkg/event"
"mayfly-go/internal/tag/api"
"mayfly-go/internal/tag/application"
"mayfly-go/internal/tag/infra/persistence"
"mayfly-go/pkg/eventbus"
"mayfly-go/pkg/global"
"mayfly-go/pkg/starter"
)
func init() {
initialize.AddInitIocFunc(func() {
starter.AddInitIocFunc(func() {
persistence.InitIoc()
application.InitIoc()
api.InitIoc()
})
initialize.AddInitFunc(Init)
starter.AddInitFunc(Init)
}
func Init() {

View File

@@ -1,6 +1,7 @@
package main
import (
"fmt"
_ "mayfly-go/internal/ai/init"
_ "mayfly-go/internal/auth/init"
_ "mayfly-go/internal/common/init"
@@ -12,12 +13,53 @@ import (
_ "mayfly-go/internal/machine/init"
_ "mayfly-go/internal/mongo/init"
_ "mayfly-go/internal/msg/init"
"mayfly-go/internal/pkg/starter"
"mayfly-go/internal/pkg/config"
_ "mayfly-go/internal/redis/init"
_ "mayfly-go/internal/sys/init"
_ "mayfly-go/internal/tag/init"
"mayfly-go/migration"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/req"
"mayfly-go/pkg/starter"
"mayfly-go/static"
"os"
"runtime/debug"
sysapp "mayfly-go/internal/sys/application"
"gorm.io/gorm"
)
func main() {
starter.RunWebServer()
// 初始化config.yml配置文件映射信息或使用环境变量
config, err := config.Init()
if err != nil {
logx.Panicf("config init failed: %v", err)
}
printBanner()
if err = starter.Run(config.Conf,
starter.WithOnDbReady(func(db *gorm.DB) error {
// 数据库升级操作
return migration.RunMigrations(db)
}),
starter.WithLogSaver(func() req.SaveLogFunc {
// 日志保存
return sysapp.GetSyslogApp().SaveFromReq
}),
starter.WithStaticRouter(static.Router()),
); err != nil {
logx.Panicf("starter server failed: %v", err)
}
}
func printBanner() {
buildInfo, _ := debug.ReadBuildInfo()
logx.Print(fmt.Sprintf(`
__ _
_ __ ___ __ _ _ _ / _| |_ _ __ _ ___
| '_ ' _ \ / _' | | | | |_| | | | |_____ / _' |/ _ \
| | | | | | (_| | |_| | _| | |_| |_____| (_| | (_) | version: %s | go_version: %s | pid: %d
|_| |_| |_|\__,_|\__, |_| |_|\__, | \__, |\___/
|___/ |___/ |___/ `, config.Version, buildInfo.GoVersion, os.Getpid()))
}

View File

@@ -24,7 +24,7 @@ func V1_9_3() []*gormigrate.Migration {
{
ID: "20250213-v1.9.3-addMachineExtra-updateMenuIcon",
Migrate: func(tx *gorm.DB) error {
tx.Migrator().AddColumn(&machineentity.Machine{}, "extra")
tx.AutoMigrate(&machineentity.Machine{})
// 更新菜单图标
resourceModel := &sysentity.Resource{}

View File

@@ -1,13 +1,47 @@
package gox
import (
"context"
"errors"
"mayfly-go/pkg/logx"
"runtime/debug"
)
// RecoverPanic 捕获panic日志, 可选传入panic时的回调函数
func RecoverPanic(onPanic ...func(error)) {
// Go 启动安全协程自动捕获并处理panic
// 该函数会在协程中执行传入的函数f并使用recover捕获可能发生的panic
// 如果发生panic会记录错误日志同时执行传入的回调函数如果有
//
// 参数:
// - f: 要在协程中执行的函数
// - onPanic: 可选的panic回调函数当发生panic时会被调用可传入多个回调函数
func Go(f func(), onPanic ...func(error)) {
go func() {
defer Recover(onPanic...)
f()
}()
}
// GoCtx 启动安全协程(带上下文)
// 该函数会在协程中执行传入的函数fn并使用recover捕获可能发生的panic
// 如果发生panic会记录错误日志同时执行传入的回调函数如果有
//
// 参数:
// - ctx: 上下文对象,用于传递取消信号、超时等
// - fn: 要在协程中执行的函数,接收上下文参数
// - onPanic: 可选的panic回调函数当发生panic时会被调用可传入多个回调函数
func GoCtx(ctx context.Context, fn func(context.Context), onPanic ...func(error)) {
go func() {
defer Recover(onPanic...)
fn(ctx)
}()
}
// Recover 捕获panic日志, 可选传入panic时的回调函数
// 该函数应作为defer调用的一部分用于捕获并处理panic
//
// 参数:
// - onPanic: 可选的panic回调函数当发生panic时会被调用可传入多个回调函数
func Recover(onPanic ...func(error)) {
if r := recover(); r != nil {
logx.ErrorTrace("PANIC: ", r)

View File

@@ -3,6 +3,7 @@ package pool
import (
"context"
"math/rand"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/stringx"
"sync"
@@ -44,7 +45,9 @@ func NewCachePool[T Conn](factory func() (T, error), opts ...Option[T]) *CachePo
closeCh: make(chan struct{}),
}
go p.backgroundMaintenance()
gox.Go(func() {
p.backgroundMaintenance()
})
return p
}
@@ -253,14 +256,14 @@ func (p *CachePool[T]) cleanupIdle() {
func (p *CachePool[T]) ping(conn T) bool {
done := make(chan struct{})
var result bool
go func() {
gox.Go(func () {
err := conn.Ping()
if err != nil {
logx.Errorf("conn pool - ping failed: %s", err.Error())
}
result = err == nil
close(done)
}()
})
select {
case <-done:
return result

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"mayfly-go/pkg/contextx"
"mayfly-go/pkg/errorx"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/anyx"
@@ -73,7 +74,9 @@ func LogHandler(rc *Ctx) error {
// 如果需要保存日志,并且保存日志处理函数存在则执行保存日志函数
if li.save && saveLog != nil {
go saveLog(rc)
gox.Go(func() {
saveLog(rc)
})
}
logMsg := li.Description

13
server/pkg/req/router.go Normal file
View File

@@ -0,0 +1,13 @@
package req
// RouterApi
// 该接口的实现类注册到ioc中则会自动将请求配置注册到路由中
type RouterApi interface {
// ReqConfs 获取请求配置信息
ReqConfs() *Confs
}
// RouterConfig 请求路由配置
type RouterConfig struct {
ContextPath string // 请求路径上下文
}

View File

@@ -0,0 +1,49 @@
package starter
import (
"context"
"fmt"
"mayfly-go/pkg/cache"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/rediscli"
"github.com/redis/go-redis/v9"
)
// 有配置redis信息则初始化redis。多台机器部署需要使用redis存储验证码、权限、公私钥等信息
func initCache(redisConfig RedisConf) error {
redisCli, err := connRedis(redisConfig)
if redisCli == nil && err == nil {
logx.Info("no redis configuration, using local cache")
return nil
}
if err != nil {
return err
}
logx.Info("redis connected successfully, using Redis for caching")
rediscli.SetCli(redisCli)
cache.SetCache(cache.NewRedisCache(redisCli))
return nil
}
func connRedis(redisConfig RedisConf) (*redis.Client, error) {
// 设置redis客户端
if redisConfig.Host == "" {
return nil, nil
}
logx.Infof("redis connecting [%s:%d]", redisConfig.Host, redisConfig.Port)
rdb := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", redisConfig.Host, redisConfig.Port),
Password: redisConfig.Password, // no password set
DB: redisConfig.Db, // use default DB
})
// 测试连接
_, e := rdb.Ping(context.TODO()).Result()
if e != nil {
logx.Errorf("redis connection failed! [%s:%d][%s]", redisConfig.Host, redisConfig.Port, e.Error())
}
return rdb, e
}

View File

@@ -0,0 +1,152 @@
package starter
import (
"fmt"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/stringx"
)
type Conf struct {
Server ServerConf `yaml:"server"`
DB DBConf `yaml:"db"`
Jwt JwtConf `yaml:"jwt"`
Log LogConf `yaml:"log"`
Redis RedisConf `yaml:"redis"`
isApplyDefaults bool
}
func (c *Conf) ApplyDefaults() error {
if c.isApplyDefaults {
return nil
}
if err := c.Log.ApplyDefaults(); err != nil {
return err
}
// 优先初始化log因为后续的一些配置ApplyDefaults方法中可能会用到
logx.Init(logx.Config{
Level: c.Log.Level,
Type: c.Log.Type,
AddSource: c.Log.AddSource,
Filename: c.Log.File.Name,
Filepath: c.Log.File.Path,
MaxSize: c.Log.File.MaxSize,
MaxAge: c.Log.File.MaxAge,
Compress: c.Log.File.Compress,
})
if err := ApplyConfigDefaults(c); err != nil {
return err
}
c.isApplyDefaults = true
return nil
}
/************************ server ************************/
// ServerConf 配置
type ServerConf struct {
Lang string `yaml:"lang" default:"zh-cn" options:"zh-cn,en"`
Port int `yaml:"port" default:"18888"`
Model string `yaml:"model" default:"release" options:"release,debug"`
ContextPath string `yaml:"context-path"` // 请求路径上下文
Cors bool `yaml:"cors"`
// TLS 配置
TLS struct {
Enable bool `yaml:"enable"` // 是否启用tls
KeyFile string `yaml:"key-file"` // 私钥文件路径
CertFile string `yaml:"cert-file"` // 证书文件路径
} `yaml:"tls"`
Statics []struct {
RelativePath string `yaml:"relative-path"`
Root string `yaml:"root"`
} `yaml:"statics"`
StaticFiles []struct {
RelativePath string `yaml:"relative-path"`
Filepath string `yaml:"filepath"`
} `yaml:"static-files"`
}
func (s *ServerConf) GetPort() string {
return fmt.Sprintf(":%d", s.Port)
}
/************************ db ************************/
type DbDialect string
const (
DialectMySQL DbDialect = "mysql"
DialectSQLite DbDialect = "sqlite"
)
// DBConf 配置
type DBConf struct {
Dialect DbDialect `yaml:"dialect" default:"sqlite" options:"mysql,sqlite"` // 数据库类型
Address string `yaml:"address" default:"mayfly-go.db"` // 地址
Name string `yaml:"name"` // 数据库名
Username string `yaml:"username"`
Password string `yaml:"password"`
Config string `yaml:"config"` // 额外配置,如 charset=utf8&loc=Local&parseTime=true
MaxIdleConns int `yaml:"max-idle-conns" default:"5"`
MaxOpenConns int `yaml:"max-open-conns"`
}
/************************ jwt ************************/
// JwtConf 配置
type JwtConf struct {
Key string `yaml:"key"`
ExpireTime uint64 `yaml:"expire-time" default:"1440"` // 过期时间,单位分钟
RefreshTokenExpireTime uint64 `yaml:"refresh-token-expire-time" default:"7200"` // 刷新token的过期时间单位分钟
}
var _ ConfigItem = (*JwtConf)(nil)
func (j *JwtConf) ApplyDefaults() error {
if j.Key == "" {
// 如果配置文件中的jwt key为空则随机生成字符串
j.Key = stringx.Rand(32)
LogDefaultValue("jwt.key", j.Key)
}
return ApplyConfigDefaults(j, "jwt")
}
/************************ log ************************/
// LogConf 配置
type LogConf struct {
Level string `yaml:"level" default:"info" options:"debug,info,warn,error"`
Type string `yaml:"type" default:"text" options:"text,json"`
AddSource bool `yaml:"add-source"`
File struct {
Name string `yaml:"name" default:"mayfly-go.log"`
Path string `yaml:"path"`
MaxSize int `yaml:"max-size"`
MaxAge int `yaml:"max-age"`
Compress bool `yaml:"compress"`
} `yaml:"file"`
}
var _ ConfigItem = (*LogConf)(nil)
func (l *LogConf) ApplyDefaults() error {
return ApplyConfigDefaults(l, "log")
}
/************************ redis ************************/
// RedisConf 配置
type RedisConf struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
Password string `yaml:"password"`
Db int `yaml:"db"`
}

View File

@@ -0,0 +1,218 @@
package starter
import (
"fmt"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/utils/structx"
"reflect"
"strconv"
"strings"
)
// ConfigItem 配置项
type ConfigItem interface {
// 如果不存在配置值,则设置默认值,并校验配置项
ApplyDefaults() error
}
// ConfigError 配置错误类型
type ConfigError struct {
Field string // 字段名
Message string // 错误描述
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("配置错误: [%s] %s", e.Field, e.Message)
}
// NewConfigError 创建配置错误
func NewConfigError(field, message string) error {
return &ConfigError{
Field: field,
Message: message,
}
}
// LogDefaultValue 记录配置默认值日志
func LogDefaultValue(field, defaultValue any) {
logx.Warnf("配置项 [%s] 未设置,使用默认值: %v", field, defaultValue)
}
// ApplyConfigDefaults 应用所有配置项的默认值支持递归处理嵌套结构体和ConfigItem接口
func ApplyConfigDefaults(obj any, parentConfigPath ...string) error {
parentPath := strings.Join(parentConfigPath, ".")
configValue := reflect.ValueOf(obj).Elem()
configType := configValue.Type()
for i := 0; i < configValue.NumField(); i++ {
field := configValue.Field(i)
fieldType := configType.Field(i)
// 检查字段是否为导出的字段
if !field.CanSet() || !field.CanAddr() {
continue
}
// 如果字段是指针类型,需要先创建实例
if field.Kind() == reflect.Ptr {
if field.IsNil() {
newValue := reflect.New(field.Type().Elem())
field.Set(newValue)
}
field = field.Elem()
}
// 获取字段地址,用于调用方法
fieldAddr := field.Addr()
// 检查字段是否实现了 ConfigItem 接口
if fieldAddr.Type().Implements(reflect.TypeOf((*ConfigItem)(nil)).Elem()) {
// 实现了接口,调用 ApplyDefaults 方法
method := fieldAddr.MethodByName("ApplyDefaults")
if method.IsValid() {
results := method.Call(nil)
if len(results) > 0 {
if err, ok := results[0].Interface().(error); ok && err != nil {
return err
}
}
}
} else if field.Kind() == reflect.Struct {
// 为嵌套结构体确定其路径组件
structPath := getFieldName(fieldType)
var currentPath string
if parentPath != "" {
currentPath = parentPath + "." + structPath
} else {
currentPath = structPath
}
if err := ApplyConfigDefaults(fieldAddr.Interface(), currentPath); err != nil {
return err
}
} else {
// 处理普通字段的默认值设置,传递父路径
if err := applyConfigFieldDefaults(field, fieldType, parentPath); err != nil {
return err
}
}
}
return nil
}
// applyConfigFieldDefaults 处理单个字段的默认值设置和选项验证等
func applyConfigFieldDefaults(field reflect.Value, fieldType reflect.StructField, parentPath string) error {
// 构建当前字段的完整配置路径
currentPath := getFieldName(fieldType)
if parentPath != "" {
currentPath = parentPath + "." + currentPath
}
// 检查是否已设置值
if structx.IsZeroValue(field) {
defaultTag := fieldType.Tag.Get("default")
if defaultTag == "" {
return nil
}
if err := setFieldValue(field, defaultTag); err != nil {
return fmt.Errorf("设置字段 [%s] 的默认值失败: %v", currentPath, err)
}
LogDefaultValue(currentPath, defaultTag)
} else {
// 检查选项验证
optionsTag := fieldType.Tag.Get("options")
if optionsTag != "" {
if err := validateOptions(currentPath, field, fieldType, optionsTag); err != nil {
return err
}
}
}
return nil
}
// setFieldValue 设置字段值
func setFieldValue(field reflect.Value, defaultValue string) error {
switch field.Kind() {
case reflect.String:
field.SetString(defaultValue)
case reflect.Bool:
val, err := strconv.ParseBool(defaultValue)
if err != nil {
return err
}
field.SetBool(val)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
val, err := strconv.ParseInt(defaultValue, 10, 64)
if err != nil {
return err
}
field.SetInt(val)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
val, err := strconv.ParseUint(defaultValue, 10, 64)
if err != nil {
return err
}
field.SetUint(val)
case reflect.Float32, reflect.Float64:
val, err := strconv.ParseFloat(defaultValue, 64)
if err != nil {
return err
}
field.SetFloat(val)
default:
return fmt.Errorf("不支持的类型: %v", field.Kind())
}
return nil
}
// getFieldName 获取字段名称,用于日志输出
func getFieldName(field reflect.StructField) string {
yamlTag := field.Tag.Get("yaml")
if yamlTag != "" && yamlTag != "-" {
return yamlTag
}
return strings.ToLower(field.Name)
}
// validateOptions 验证字段值是否在允许的选项范围内
func validateOptions(configPath string, field reflect.Value, typeField reflect.StructField, optionsTag string) error {
// 解析选项列表
options := strings.Split(optionsTag, ",")
if len(options) == 0 {
return nil
}
// 清理选项空格
for i, option := range options {
options[i] = strings.TrimSpace(option)
}
var fieldValue string
switch field.Kind() {
case reflect.String:
fieldValue = field.String()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fieldValue = strconv.FormatInt(field.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
fieldValue = strconv.FormatUint(field.Uint(), 10)
case reflect.Bool:
fieldValue = strconv.FormatBool(field.Bool())
default:
return fmt.Errorf("字段 %s 不支持选项验证,类型: %v", typeField.Name, field.Kind())
}
// 检查字段值是否在允许的选项中
for _, option := range options {
if fieldValue == option {
return nil
}
}
return NewConfigError(configPath, fmt.Sprintf("值 '%s' 不在允许的选项范围内: [%s]", fieldValue, strings.Join(options, ", ")))
}

View File

@@ -0,0 +1,85 @@
package starter
import (
"log"
"mayfly-go/pkg/logx"
"time"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
)
func initDB(dbConf DBConf) (*gorm.DB, error) {
if dbConf.Dialect == DialectMySQL {
return initMysql(dbConf)
}
return initSqlite(dbConf)
}
func initMysql(dbConf DBConf) (*gorm.DB, error) {
logx.Infof("connecting to mysql [%s]", dbConf.Address)
mysqlConfig := mysql.Config{
DSN: dbConf.Username + ":" + dbConf.Password + "@tcp(" + dbConf.Address + ")/" + dbConf.Name + "?" + dbConf.Config, // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据版本自动配置
}
if db, err := gorm.Open(mysql.New(mysqlConfig), getGormConfig()); err != nil {
logx.Errorf("failed to connect to mysql! [%s]", err.Error())
return nil, err
} else {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(dbConf.MaxIdleConns)
sqlDB.SetMaxOpenConns(dbConf.MaxOpenConns)
// 如果是开发环境时打印sql语句
if logx.GetConfig().IsDebug() {
db = db.Debug()
}
return db, nil
}
}
func initSqlite(dbConf DBConf) (*gorm.DB, error) {
logx.Infof("connecting to sqlite [%s]", dbConf.Address)
if db, err := gorm.Open(sqlite.Open(dbConf.Address), getGormConfig()); err != nil {
logx.Errorf("failed to connect to sqlite! [%s]", err.Error())
return nil, err
} else {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(dbConf.MaxIdleConns)
sqlDB.SetMaxOpenConns(dbConf.MaxOpenConns)
return db, nil
}
}
func getGormConfig() *gorm.Config {
sqlLogLevel := logger.Error
logConf := logx.GetConfig()
// 如果为配置文件中配置的系统日志级别为debug则打印gorm执行的sql信息
if logConf.IsDebug() {
sqlLogLevel = logger.Info
}
gormLogger := logger.New(
log.New(logConf.GetLogOut(), "\r\n", log.LstdFlags), // io writer日志输出的目标前缀和日志包含的内容——译者注
logger.Config{
SlowThreshold: time.Second, // 慢 SQL 阈值
LogLevel: sqlLogLevel, // 日志级别, 改为logger.Info即可显示sql语句
IgnoreRecordNotFoundError: true, // 忽略ErrRecordNotFound记录未找到错误
Colorful: false, // 禁用彩色打印
},
)
return &gorm.Config{NamingStrategy: schema.NamingStrategy{
TablePrefix: "t_",
SingularTable: true,
}, Logger: gormLogger}
}

View File

@@ -1,7 +1,6 @@
package initialize
package starter
import (
"mayfly-go/pkg/biz"
"mayfly-go/pkg/ioc"
)
@@ -27,7 +26,7 @@ func AddInitFunc(initFunc InitFunc) {
}
// 系统启动时,调用各个模块的初始化函数
func InitOther() {
func initOther() error {
// 调用各个模块ioc组件注册初始化优先调用ioc初始化注册函数和注入函数可能在后续的InitFunc中需要用到依赖实例
for _, initIocFunc := range initIocFuncs {
initIocFunc()
@@ -35,11 +34,15 @@ func InitOther() {
initIocFuncs = nil
// 为所有注册的实例注入其依赖的其他组件实例
biz.ErrIsNil(ioc.InjectComponents())
if err := ioc.InjectComponents(); err != nil {
return err
}
// 调用各个模块的初始化函数
for _, initFunc := range initFuncs {
go initFunc()
}
initFuncs = nil
return nil
}

View File

@@ -0,0 +1,73 @@
package starter
import (
"mayfly-go/pkg/req"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// Option 是用于配置启动选项的函数类型
type Option func(*Options)
// Options 包含所有启动时的配置选项
type Options struct {
// 日志保存器
LogSaver func() req.SaveLogFunc
// 数据库初始化相关回调
OnDbReady func(db *gorm.DB) error
// 路由注册完成回调
OnRoutesReady func(engine *gin.Engine)
// 服务启动相关回调
OnBeforeStart func()
// 静态资源路由配置
StaticRouter *StaticRouter
}
// WithLogSaver 设置日志保存器
func WithLogSaver(saver func() req.SaveLogFunc) Option {
return func(o *Options) {
o.LogSaver = saver
}
}
// WithOnDbReady 设置数据库准备就绪回调函数
func WithOnDbReady(fn func(db *gorm.DB) error) Option {
return func(o *Options) {
o.OnDbReady = fn
}
}
// WithOnRoutesReady 设置路由准备就绪回调函数
func WithOnRoutesReady(fn func(engine *gin.Engine)) Option {
return func(o *Options) {
o.OnRoutesReady = fn
}
}
// WithOnBeforeStart 设置服务启动前回调函数
func WithOnBeforeStart(fn func()) Option {
return func(o *Options) {
o.OnBeforeStart = fn
}
}
// WithStaticRouter 添加静态资源路由
func WithStaticRouter(staticRouter *StaticRouter) Option {
return func(o *Options) {
o.StaticRouter = staticRouter
}
}
// NewOptions 创建默认的选项配置
func NewOptions(opts ...Option) *Options {
options := &Options{}
for _, opt := range opts {
opt(options)
}
return options
}

View File

@@ -1,7 +1,8 @@
package initialize
package starter
import (
"fmt"
"io/fs"
"mayfly-go/pkg/ioc"
"mayfly-go/pkg/req"
"net/http"
@@ -9,18 +10,13 @@ import (
"github.com/gin-gonic/gin"
)
// RouterApi
// 该接口的实现类注册到ioc中则会自动将请求配置注册到路由中
type RouterApi interface {
// ReqConfs 获取请求配置信息
ReqConfs() *req.Confs
// StaticRouter 静态资源路由配置
type StaticRouter struct {
Fs fs.FS // 静态资源文件系统
Paths []string // 静态资源访问路径,如 /assets/*file
}
type RouterConfig struct {
ContextPath string // 请求路径上下文
}
func InitRouter(router *gin.Engine, conf RouterConfig) *gin.Engine {
func initRouter(router *gin.Engine, conf req.RouterConfig) *gin.Engine {
// 没有路由即 404返回
router.NoRoute(func(g *gin.Context) {
g.JSON(http.StatusNotFound, gin.H{"code": 404, "msg": fmt.Sprintf("not found '%s:%s'", g.Request.Method, g.Request.URL.Path)})
@@ -30,7 +26,7 @@ func InitRouter(router *gin.Engine, conf RouterConfig) *gin.Engine {
api := router.Group(conf.ContextPath + "/api")
// 获取所有实现了RouterApi接口的实例并注册对应路由
ras := ioc.GetBeansByType[RouterApi]()
ras := ioc.GetBeansByType[req.RouterApi]()
for _, ra := range ras {
confs := ra.ReqConfs()
if group := confs.Group; group != "" {

View File

@@ -0,0 +1,81 @@
package starter
import (
"context"
"mayfly-go/pkg/global"
"mayfly-go/pkg/req"
"mayfly-go/pkg/validatorx"
"os"
"os/signal"
"syscall"
)
func Run(config Conf, opts ...Option) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
cancel()
}()
// 应用配置默认值
if err := config.ApplyDefaults(); err != nil {
return err
}
options := NewOptions(opts...)
// 初始化并赋值数据库全局变量
db, err := initDB(config.DB)
if err != nil {
return err
}
global.Db = db
if options.OnDbReady != nil {
if err := options.OnDbReady(db); err != nil {
return err
}
}
// 初始化缓存
if err := initCache(config.Redis); err != nil {
return err
}
// 初始化其他需要启动时运行的方法
if err := initOther(); err != nil {
return err
}
// 参数校验器初始化、如错误提示中文转译等
validatorx.Init()
// jwt配置
jwtConf := config.Jwt
req.SetJwtConf(req.JwtConf{
Key: jwtConf.Key,
ExpireTime: jwtConf.ExpireTime,
RefreshTokenExpireTime: jwtConf.RefreshTokenExpireTime,
})
// 权限处理器
req.UseBeforeHandlerInterceptor(req.PermissionHandler)
// 日志处理器
req.UseAfterHandlerInterceptor(req.LogHandler)
// 设置日志保存函数
if options.LogSaver != nil {
req.SetSaveLogFunc(options.LogSaver())
}
// 启动前回调
if options.OnBeforeStart != nil {
options.OnBeforeStart()
}
// 运行web服务
return runWebServer(ctx, config.Server, options)
}

View File

@@ -1,4 +1,4 @@
package initialize
package starter
// 系统进程退出终止函数
type TerminateFunc func()

View File

@@ -0,0 +1,109 @@
package starter
import (
"context"
"errors"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/i18n"
"mayfly-go/pkg/logx"
"mayfly-go/pkg/middleware"
"mayfly-go/pkg/req"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func runWebServer(ctx context.Context, serverConfig ServerConf, options *Options) error {
// 设置gin日志输出器
logOut := logx.GetConfig().GetLogOut()
gin.DefaultErrorWriter = logOut
gin.DefaultWriter = logOut
gin.SetMode(serverConfig.Model)
// i18n配置
i18n.SetLang(serverConfig.Lang)
var router = gin.New()
router.MaxMultipartMemory = 8 << 20
// 初始化接口路由
initRouter(router, req.RouterConfig{ContextPath: serverConfig.ContextPath})
// 设置静态资源
setStatic(router, serverConfig, options.StaticRouter)
if options != nil && options.OnRoutesReady != nil {
options.OnRoutesReady(router)
}
// 是否允许跨域
if serverConfig.Cors {
router.Use(middleware.Cors())
}
srv := http.Server{
Addr: serverConfig.GetPort(),
// 注册路由
Handler: router,
}
go func() {
defer gox.Recover()
<-ctx.Done()
logx.Info("Shutdown HTTP Server ...")
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := srv.Shutdown(timeout)
if err != nil {
logx.Errorf("Failed to Shutdown HTTP Server: %v", err)
}
Terminate()
}()
logx.Infof("Listening and serving HTTP on %s", srv.Addr+serverConfig.ContextPath)
var err error
if serverConfig.TLS.Enable {
err = srv.ListenAndServeTLS(serverConfig.TLS.CertFile, serverConfig.TLS.KeyFile)
} else {
err = srv.ListenAndServe()
}
if errors.Is(err, http.ErrServerClosed) {
logx.Info("HTTP Server Shutdown")
return nil
}
if err != nil {
logx.Errorf("Failed to Start HTTP Server: %v", err)
}
return err
}
func setStatic(router *gin.Engine, serverConfig ServerConf, staticRouter *StaticRouter) {
contextPath := serverConfig.ContextPath
if staticRouter != nil {
fileServer := http.FileServer(http.FS(staticRouter.Fs))
handler := WrapStaticHandler(http.StripPrefix(contextPath, fileServer))
for _, p := range staticRouter.Paths {
router.GET(contextPath+p, handler)
}
}
// 设置静态资源
for _, scs := range serverConfig.Statics {
router.StaticFS(scs.RelativePath, http.Dir(scs.Root))
}
// 设置静态文件
for _, sfs := range serverConfig.StaticFiles {
router.StaticFile(sfs.RelativePath, sfs.Filepath)
}
}
func WrapStaticHandler(h http.Handler) gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Cache-Control", `public, max-age=31536000`)
h.ServeHTTP(c.Writer, c.Request)
}
}

View File

@@ -19,3 +19,24 @@ func NewInstance[T any]() T {
return t
}
// IsZeroValue 检查字段是否为零值
func IsZeroValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.String:
return v.String() == ""
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return v.Uint() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return v.IsNil()
default:
return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface())
}
}

View File

@@ -3,6 +3,7 @@ package ws
import (
"encoding/json"
"errors"
"mayfly-go/pkg/gox"
"mayfly-go/pkg/logx"
"time"
@@ -40,7 +41,7 @@ func (c *Client) WithReadHandlerFunc(readMsgHandlerFunc ReadMsgHandlerFunc) *Cli
// 读取ws客户端消息
func (c *Client) Read() {
go func() {
gox.Go(func() {
for {
if c.WsConn == nil {
return
@@ -59,7 +60,7 @@ func (c *Client) Read() {
c.ReadMsgHandler(data)
}
}
}()
})
}
// 向客户端写入消息

View File

@@ -97,6 +97,10 @@ func (manager *ClientManager) CloseClient(client *Client) {
// 根据用户id关闭客户端连接
func (manager *ClientManager) CloseByUid(userId UserId) {
userClient := manager.GetByUid(userId)
if userClient == nil {
return
}
for _, client := range userClient.AllClients() {
manager.CloseClient(client)
}
@@ -140,8 +144,7 @@ func (manager *ClientManager) SendJsonMsg(userId UserId, clientId string, data a
// 监听并发送给客户端信息
func (manager *ClientManager) WriteMessage() {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
for {
msg := <-manager.MsgChan
uid := msg.ToUserId
@@ -169,13 +172,12 @@ func (manager *ClientManager) WriteMessage() {
}
}
}
}()
})
}
// 启动定时器进行心跳检测
func (manager *ClientManager) HeartbeatTimer() {
go func() {
defer gox.RecoverPanic()
gox.Go(func() {
ticker := time.NewTicker(heartbeatInterval)
defer ticker.Stop()
for {
@@ -196,7 +198,7 @@ func (manager *ClientManager) HeartbeatTimer() {
return true
})
}
}()
})
}
// 处理建立连接

View File

@@ -1,9 +1,25 @@
package static
import "embed"
import (
"embed"
"io/fs"
"mayfly-go/pkg/starter"
)
// 使用1.16特性编译阶段将静态资源文件打包进编译好的程序
var (
//go:embed static/**
Static embed.FS
)
func Router() *starter.StaticRouter {
sys, _ := fs.Sub(Static, "static")
return &starter.StaticRouter{
Fs: sys,
Paths: []string{"/",
"/favicon.ico",
"/config.js",
"/assets/*file",
},
}
}