Compare commits

..

45 Commits

Author SHA1 Message Date
yvan b8447604c2 wechat 2025-09-10 23:33:54 +08:00
yvan 7455586bcf wechat 2025-09-10 22:17:58 +08:00
yvan 8991a00e17 wechat 2025-09-10 20:40:06 +08:00
yvan 73063af861 Merge remote-tracking branch 'yvan/main'
# Conflicts:
#	server/go.mod
#	server/go.sum
2025-09-10 19:49:22 +08:00
yvan 870f9fff86 wechat 2025-09-10 19:49:13 +08:00
yvan 05fba0b6a3 Merge remote-tracking branch 'gva/main'
# Conflicts:
#	server/go.mod
#	server/go.sum
#	server/mcp/api_creator.go
#	server/mcp/execution_plan_schema.md
#	server/mcp/gva_auto_generate.go
#	server/mcp/menu_creator.go
#	server/mcp/requirement_analyzer.go
#	server/utils/verify.go
2025-09-09 21:37:59 +08:00
piexlMax(奇淼 424c5b23b4 style(layout): 修复绝对定位类名缺少重要修饰符的问题 2025-09-09 11:18:11 +08:00
piexlMax(奇淼 86b420ce67 docs: 更新项目规则文档和调整代码格式 2025-09-08 10:54:44 +08:00
piexlMax(奇淼 ea5709e5b6 style(菜单组件): 调整级联选择器宽度样式为全宽 2025-09-08 10:51:13 +08:00
PiexlMax(奇淼 4e9e3570ed
fix: VersionData数据类型改为text 2025-09-07 23:58:05 +08:00
pixelmaxQM 78cdc287c5 docs: 更新项目规则文档中的角色描述为全栈专家 2025-09-07 12:46:59 +08:00
piexlMax(奇淼 58b123d316 fix: VersionData 默认使用text 考虑兼容性 2025-09-05 17:38:46 +08:00
piexlMax(奇淼 fb199aeedc fix: 修复多出来的一个/ 2025-09-04 15:16:31 +08:00
piexlMax(奇淼 364dddb70a Merge branch 'main' of github.com:flipped-aurora/gin-vue-admin 2025-09-03 16:32:46 +08:00
piexlMax(奇淼 6bf848d53f style: 统一使用!前缀替代方括号的宽度类名
修改模板文件中的宽度类名,从w-[380px]和w-40统一改为!w-380px和!w-40
修复模板中条件判断的语法格式问题
2025-09-03 16:29:52 +08:00
青菜白玉汤 3e429f4091
feat: 增加依赖,提高 unocss 相关兼容性 (#2088)
* fix: 补充部分运行时依赖

* optimize: 优化class mark.
2025-09-02 15:03:48 +08:00
piexlMax(奇淼 daabca4768 Merge branch 'main' of github.com:flipped-aurora/gin-vue-admin 2025-09-01 15:19:22 +08:00
piexlMax(奇淼 9e688c65d3 fix(richtext): 为富文本编辑器上传图片添加token认证头
添加用户token到图片上传请求头中,确保有权限的用户才能上传文件
2025-09-01 15:17:58 +08:00
pixelmaxQM 2e8f5870d8 refactor(model): 调整版本数据字段的数据库类型注释
为VersionData字段添加postgres和sqlite数据库特定的类型注释,提高跨数据库兼容性
2025-08-31 00:16:32 +08:00
Qing Liang 8b0a00b98a
Update project_rules.md (#2083) 2025-08-29 11:44:59 +08:00
piexlMax(奇淼 1ce3e11d52 fix(selectFile): 修复未初始化时fileList为undefined的问题 2025-08-28 15:34:35 +08:00
piexlMax(奇淼 11aa8c5cc2 feat(version): 添加字典数据支持到版本管理功能 2025-08-28 15:11:52 +08:00
piexlMax(奇淼 d8e6f2f5f4 fix: 同步 go.mod 2025-08-28 13:58:29 +08:00
piexlMax(奇淼 9d5c93b4c3 refactor(version): 统一版本号管理并更新至v2.8.5
将版本号从硬编码改为统一管理,使用package.json和全局变量维护版本信息
2025-08-28 13:51:54 +08:00
Qing Liang 2f7f3e5ac6
other:注释 重置用户密码 (#2081) 2025-08-28 13:37:04 +08:00
PiexlMax(奇淼 cb97d3832d
Merge pull request #2080 from flipped-aurora/v2.8.5
发布v2.8.5 beta 版本
2025-08-28 11:45:01 +08:00
piexlMax(奇淼 dd34d6e3be fix: mcp字段统一样式限制 2025-08-26 18:43:55 +08:00
PiexlMax(奇淼 1d700ad5b6
Update README.md 2025-08-19 18:26:17 +08:00
PiexlMax(奇淼 3648df9522
Merge pull request #2070 from dzwvip/main
修改gorm-oracle的驱动包为go-ora,不再依赖cgo和oracle instant  client
2025-08-14 15:33:51 +08:00
dzwvip eb2925829f 修改gorm-oracle的驱动包为go-ora,不再依赖cgo和oracle client 2025-08-12 15:05:38 +08:00
piexlMax(奇淼 1e3c968277 refactor(mcp): 更新默认值描述并移除无用工具 2025-08-11 10:52:15 +08:00
piexlMax(奇淼 339db778c3 chore: 优化引入svg方式 2025-08-08 11:46:08 +08:00
piexlMax(奇淼 8b8ae7b692 refactor(auth): 简化权限指令中的类型检查逻辑 2025-08-07 16:18:04 +08:00
piexlMax(奇淼 52be26eb43 fix: 修正未配置默认路由时的错误提示信息 2025-08-06 15:55:38 +08:00
piexlMax(奇淼 9f958addc8 fix(verify): 移除登录验证中的Captcha字段校验
refactor(validation): 强制字段名首字母大写并更新文档
2025-08-06 15:18:03 +08:00
piexlMax(奇淼 7a69b88885 refactor(config): 更新配置文件格式并添加新配置项 2025-08-06 13:47:09 +08:00
pixelmaxQM 76fe71a8dd docs(mcp): 完善工具描述并添加重要限制说明 2025-08-05 20:35:12 +08:00
piexlMax(奇淼 3f21d7f8dd docs(mcp): 更新字段类型的支持说明文档 2025-08-05 18:56:57 +08:00
PiexlMax(奇淼 1fce169616
Merge pull request #2065 from flipped-aurora/2.8.5
支持多模块mcp,精细化api和menu的创建规则。
2025-08-05 18:43:21 +08:00
piexlMax(奇淼 0d900293eb docs: 更新文档中字段类型描述并优化工具说明
更新多个文档和工具描述文件,统一字段类型的详细说明格式
优化菜单创建工具和API创建工具的描述,明确使用场景
补充执行计划schema中字段类型的详细说明

(cherry picked from commit d9448aa25cca717e01a00aec2c38a24da7b28f6f)
2025-08-05 18:39:13 +08:00
piexlMax(奇淼 b71ad7b6f4 docs(mcp): 完善模块关联配置的文档和示例
(cherry picked from commit e042d0464c47ab8441f363df1a1242e26a9c13f7)
2025-08-05 18:39:13 +08:00
PiexlMax(奇淼 2139e38147
refactor: optimize and simplify login captcha validation flow (#2063)
* Optimize captcha related fields verification

- Add `Captcha` field validation to `LoginVerify` rules, ensuring that the captcha isn't empty while prechecking the data.
- Remove redundant checks for empty values for `Captcha` and `CaptchaId`, as they are already validated in the `LoginVerify` struct.
- Move client IP key retrieval after input validation for better flow.

* refactor: simplify login captcha validation flow for cleaner code
2025-08-04 18:35:28 +08:00
Feng.YJ a033ab9abe
refactor: simplify login captcha validation flow for cleaner code 2025-08-04 13:43:03 +08:00
Feng.YJ 311e1ddb9f
Optimize captcha related fields verification
- Add `Captcha` field validation to `LoginVerify` rules, ensuring that the captcha isn't empty while prechecking the data.
- Remove redundant checks for empty values for `Captcha` and `CaptchaId`, as they are already validated in the `LoginVerify` struct.
- Move client IP key retrieval after input validation for better flow.
2025-08-04 13:39:51 +08:00
pixelmaxQM 31e6316916 feat(mcp): 实现需求分析工具并支持批量创建模块
1. 新增requirement_analyzer工具,用于将用户需求转换为结构化提示词
2. 修改gva_auto_generate工具支持批量创建多个模块
3. 更新文档示例和schema以反映批量创建功能
4. 优化执行计划验证逻辑,支持模块数组校验
5. 改进字典创建流程,避免重复创建相同字典类型
2025-08-03 23:58:47 +08:00
75 changed files with 1174 additions and 748 deletions

View File

@ -22,7 +22,8 @@
<table>
<tr>
<td width="200">
<td width="250">
<p>⭐️ <a href="https://www.bilibili.com/video/BV1B3htzqEf1/?spm_id_from=333.1387.homepage.video_card.click" target="__blank"> 高度适配AI编辑器的MCP </a></p>
<p>📄 创建基础模板</p>
<p>🤖 AI生成结构</p>
<p>⏰ 生成代码</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

View File

@ -19,60 +19,6 @@ type PetAssistantApi struct{}
var petChatService = service.ServiceGroupApp.PetServiceGroup.PetChatService
// AskPetAssistant 向宠物助手提问(非流式)
// @Tags PetAssistant
// @Summary 向宠物助手提问
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param data body petRequest.ChatRequest true "宠物助手提问请求"
// @Success 200 {object} response.Response{data=petResponse.ChatResponse,msg=string} "提问成功"
// @Router /api/v1/pet/user/assistant/ask [post]
func (p *PetAssistantApi) AskPetAssistant(ctx *gin.Context) {
// 创建业务用Context
businessCtx := ctx.Request.Context()
// 获取用户ID
userId := utils.GetAppUserID(ctx)
if userId == 0 {
global.GVA_LOG.Error("获取用户ID失败")
response.FailWithMessage("用户认证失败", ctx)
return
}
// 绑定请求参数
var req petRequest.ChatRequest
if err := ctx.ShouldBindJSON(&req); err != nil {
global.GVA_LOG.Error("参数绑定失败", zap.Error(err))
response.FailWithMessage("参数错误: "+err.Error(), ctx)
return
}
// 参数验证
if req.Message == "" {
response.FailWithMessage("消息内容不能为空", ctx)
return
}
// 设置默认参数
if req.Temperature <= 0 {
req.Temperature = 0.7
}
if req.MaxTokens <= 0 {
req.MaxTokens = 1000
}
// 调用服务层
resp, err := petChatService.SendMessage(businessCtx, userId, req)
if err != nil {
global.GVA_LOG.Error("发送消息失败", zap.Error(err), zap.Uint("userId", userId))
response.FailWithMessage("发送消息失败: "+err.Error(), ctx)
return
}
response.OkWithDetailed(resp, "发送成功", ctx)
}
// StreamAskPetAssistant 向宠物助手流式提问
// @Tags PetAssistant
// @Summary 向宠物助手流式提问接口
@ -177,14 +123,14 @@ func (p *PetAssistantApi) StreamAskPetAssistant(ctx *gin.Context) {
// GetAssistantHistory 获取宠物助手对话历史
// @Tags PetAssistant
// @Summary 获取宠物助手对话历史记录
// @Summary 获取宠物助手对话历史记录(简化版本,只返回必要字段)
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param sessionId query string false "会话ID"
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页数量" default(20)
// @Success 200 {object} response.Response{data=[]pet.PetAiAssistantConversations,msg=string} "获取成功"
// @Success 200 {object} response.Response{data=[]petResponse.ChatHistoryItem,msg=string} "获取成功"
// @Router /api/v1/pet/user/assistant/history [get]
func (p *PetAssistantApi) GetAssistantHistory(ctx *gin.Context) {
// 创建业务用Context

View File

@ -27,8 +27,6 @@ import (
func (b *BaseApi) Login(c *gin.Context) {
var l systemReq.Login
err := c.ShouldBindJSON(&l)
key := c.ClientIP()
if err != nil {
response.FailWithMessage(err.Error(), c)
return
@ -39,6 +37,7 @@ func (b *BaseApi) Login(c *gin.Context) {
return
}
key := c.ClientIP()
// 判断验证码是否开启
openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数
openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间
@ -48,30 +47,30 @@ func (b *BaseApi) Login(c *gin.Context) {
}
var oc bool = openCaptcha == 0 || openCaptcha < interfaceToInt(v)
if !oc || (l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true)) {
u := &system.SysUser{Username: l.Username, Password: l.Password}
user, err := userService.Login(u)
if err != nil {
global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err))
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户名不存在或者密码错误", c)
return
}
if user.Enable != 1 {
global.GVA_LOG.Error("登陆失败! 用户被禁止登录!")
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户被禁止登录", c)
return
}
b.TokenNext(c, *user)
if oc && (l.Captcha == "" || l.CaptchaId == "" || !store.Verify(l.CaptchaId, l.Captcha, true)) {
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("验证码错误", c)
return
}
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("验证码错误", c)
u := &system.SysUser{Username: l.Username, Password: l.Password}
user, err := userService.Login(u)
if err != nil {
global.GVA_LOG.Error("登陆失败! 用户名不存在或者密码错误!", zap.Error(err))
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户名不存在或者密码错误", c)
return
}
if user.Enable != 1 {
global.GVA_LOG.Error("登陆失败! 用户被禁止登录!")
// 验证码次数+1
global.BlackCache.Increment(key, 1)
response.FailWithMessage("用户被禁止登录", c)
return
}
b.TokenNext(c, *user)
}
// TokenNext 登录以后签发jwt

View File

@ -267,6 +267,17 @@ func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
}
}
// 获取选中的字典数据
var dictData []system.SysDictionary
if len(req.DictIds) > 0 {
dictData, err = sysVersionService.GetDictionariesByIds(ctx, req.DictIds)
if err != nil {
global.GVA_LOG.Error("获取字典数据失败!", zap.Error(err))
response.FailWithMessage("获取字典数据失败:"+err.Error(), c)
return
}
}
// 处理菜单数据构建递归的children结构
processedMenus := buildMenuTree(menuData)
@ -282,6 +293,34 @@ func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
processedApis = append(processedApis, cleanApi)
}
// 处理字典数据清除ID和时间戳字段包含字典详情
processedDicts := make([]system.SysDictionary, 0, len(dictData))
for _, dict := range dictData {
cleanDict := system.SysDictionary{
Name: dict.Name,
Type: dict.Type,
Status: dict.Status,
Desc: dict.Desc,
}
// 处理字典详情数据清除ID和时间戳字段
cleanDetails := make([]system.SysDictionaryDetail, 0, len(dict.SysDictionaryDetails))
for _, detail := range dict.SysDictionaryDetails {
cleanDetail := system.SysDictionaryDetail{
Label: detail.Label,
Value: detail.Value,
Extend: detail.Extend,
Status: detail.Status,
Sort: detail.Sort,
// 不复制 ID, CreatedAt, UpdatedAt, SysDictionaryID
}
cleanDetails = append(cleanDetails, cleanDetail)
}
cleanDict.SysDictionaryDetails = cleanDetails
processedDicts = append(processedDicts, cleanDict)
}
// 构建导出数据
exportData := systemRes.ExportVersionResponse{
Version: systemReq.VersionInfo{
@ -290,8 +329,9 @@ func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
Description: req.Description,
ExportTime: time.Now().Format("2006-01-02 15:04:05"),
},
Menus: processedMenus,
Apis: processedApis,
Menus: processedMenus,
Apis: processedApis,
Dictionaries: processedDicts,
}
// 转换为JSON
@ -418,6 +458,15 @@ func (sysVersionApi *SysVersionApi) ImportVersion(c *gin.Context) {
}
}
// 导入字典数据
if len(importData.ExportDictionary) > 0 {
if err := sysVersionService.ImportDictionaries(importData.ExportDictionary); err != nil {
global.GVA_LOG.Error("导入字典失败!", zap.Error(err))
response.FailWithMessage("导入字典失败: "+err.Error(), c)
return
}
}
// 创建导入记录
jsonData, _ := json.Marshal(importData)
version := system.SysVersion{

View File

@ -2,107 +2,103 @@
# jwt configuration
jwt:
signing-key: qmPlus
expires-time: 7d
buffer-time: 1d
issuer: qmPlus
signing-key: qmPlus
expires-time: 7d
buffer-time: 1d
issuer: qmPlus
# zap logger configuration
zap:
level: info
format: console
prefix: "[github.com/flipped-aurora/gin-vue-admin/server]"
director: log
show-line: true
encode-level: LowercaseColorLevelEncoder
stacktrace-key: stacktrace
log-in-console: true
level: info
format: console
prefix: "[github.com/flipped-aurora/gin-vue-admin/server]"
director: log
show-line: true
encode-level: LowercaseColorLevelEncoder
stacktrace-key: stacktrace
log-in-console: true
retention-day: -1
# redis configuration
redis:
db: 0
addr: 177.7.0.14:6379
password: ""
#是否使用redis集群模式
useCluster: false
#使用集群模式addr和db默认无效
addr: 177.7.0.14:6379
password: ""
db: 0
clusterAddrs:
- "177.7.0.14:7000"
- "177.7.0.15:7001"
- "177.7.0.13:7002"
# redis-list configuration
redis-list:
- name: cache # 数据库的名称,注意: name 需要在 redis-list 中唯一
useCluster: false # 是否使用redis集群模式
addr: 177.7.0.14:6379 # 使用集群模式addr和db默认无效
password: ""
db: 0
clusterAddrs:
- "177.7.0.14:7000"
- "177.7.0.15:7001"
- "177.7.0.13:7002"
# mongo configuration
mongo:
coll: ''
options: ''
database: ''
username: ''
password: ''
min-pool-size: 0
max-pool-size: 100
socket-timeout-ms: 0
connect-timeout-ms: 0
is-zap: false
hosts:
- host: ''
port: ''
coll: ''
options: ''
database: ''
username: ''
password: ''
auth-source: ''
min-pool-size: 0
max-pool-size: 100
socket-timeout-ms: 0
connect-timeout-ms: 0
is-zap: false
hosts:
- host: ''
port: ''
# email configuration
email:
to: xxx@qq.com
port: 465
from: xxx@163.com
host: smtp.163.com
is-ssl: true
secret: xxx
nickname: test
to: xxx@qq.com
port: 465
from: xxx@163.com
host: smtp.163.com
is-ssl: true
secret: xxx
nickname: test
# system configuration
system:
env: public # Change to "develop" to skip authentication for development mode
addr: 8888
db-type: mysql
oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
use-redis: false # 使用redis
use-mongo: false # 使用mongo
use-multipoint: false
# IP限制次数 一个小时15000次
iplimit-count: 15000
# IP限制一个小时
iplimit-time: 3600
env: local # 修改为public可以关闭路由日志输出
addr: 8888
db-type: mysql
oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
use-redis: false # 使用redis
use-mongo: false # 使用mongo
use-multipoint: false
# IP限制次数 一个小时15000次
iplimit-count: 15000
# IP限制一个小时
iplimit-time: 3600
# 路由全局前缀
router-prefix: ""
# 严格角色模式 打开后权限将会存在上下级关系
use-strict-auth: false
# captcha configuration
captcha:
key-long: 6
img-width: 240
img-height: 80
open-captcha: 0 # 0代表一直开启大于0代表限制次数
open-captcha-timeout: 3600 # open-captcha大于0时才生效
key-long: 6
img-width: 240
img-height: 80
open-captcha: 0 # 0代表一直开启大于0代表限制次数
open-captcha-timeout: 3600 # open-captcha大于0时才生效
# mysql connect configuration
# 未初始化之前请勿手动修改数据库信息如果一定要手动初始化请看https://gin-vue-admin.com/docs/first_master
mysql:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
# pgsql connect configuration
# 未初始化之前请勿手动修改数据库信息如果一定要手动初始化请看https://gin-vue-admin.com/docs/first_master
pgsql:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
db-list:
- disable: true # 是否禁用
type: "" # 数据库的类型,目前支持mysql、pgsql
alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一
path: ""
port: ""
config: ""
@ -114,107 +110,174 @@ db-list:
log-mode: ""
log-zap: false
# pgsql connect configuration
# 未初始化之前请勿手动修改数据库信息如果一定要手动初始化请看https://gin-vue-admin.com/docs/first_master
pgsql:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
oracle:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
mssql:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
sqlite:
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
db-list:
- disable: true # 是否禁用
type: "" # 数据库的类型,目前支持mysql、pgsql、mssql、oracle
alias-name: "" # 数据库的名称,注意: alias-name 需要在db-list中唯一
path: ""
port: ""
config: ""
db-name: ""
username: ""
password: ""
max-idle-conns: 10
max-open-conns: 100
log-mode: ""
log-zap: false
# local configuration
local:
path: uploads/file
store-path: uploads/file
path: uploads/file
store-path: uploads/file
# autocode configuration
autocode:
transfer-restart: true
# root 自动适配项目根目录
# 请不要手动配置,他会在项目加载的时候识别出根路径
root: ""
server: /server
server-plug: /plugin/%s
server-api: /api/v1/%s
server-initialize: /initialize
server-model: /model/%s
server-request: /model/%s/request/
server-router: /router/%s
server-service: /service/%s
web: /web/src
web-api: /api
web-form: /view
web-table: /view
web: web/src
root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径
server: server
module: 'github.com/flipped-aurora/gin-vue-admin/server'
ai-path: "" # AI服务路径
# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址)
qiniu:
zone: ZoneHuaDong
bucket: ""
img-path: ""
use-https: false
access-key: ""
secret-key: ""
use-cdn-domains: false
zone: ZoneHuaDong
bucket: ""
img-path: ""
use-https: false
access-key: ""
secret-key: ""
use-cdn-domains: false
# minio oss configuration
minio:
endpoint: yourEndpoint
access-key-id: yourAccessKeyId
access-key-secret: yourAccessKeySecret
bucket-name: yourBucketName
use-ssl: false
base-path: ""
bucket-url: "http://host:9000/yourBucketName"
# aliyun oss configuration
aliyun-oss:
endpoint: yourEndpoint
access-key-id: yourAccessKeyId
access-key-secret: yourAccessKeySecret
bucket-name: yourBucketName
bucket-url: yourBucketUrl
base-path: yourBasePath
endpoint: yourEndpoint
access-key-id: yourAccessKeyId
access-key-secret: yourAccessKeySecret
bucket-name: yourBucketName
bucket-url: yourBucketUrl
base-path: yourBasePath
# tencent cos configuration
tencent-cos:
bucket: xxxxx-10005608
region: ap-shanghai
secret-id: your-secret-id
secret-key: your-secret-key
base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
bucket: xxxxx-10005608
region: ap-shanghai
secret-id: your-secret-id
secret-key: your-secret-key
base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
# aws s3 configuration (minio compatible)
aws-s3:
bucket: xxxxx-10005608
region: ap-shanghai
endpoint: ""
s3-force-path-style: false
disable-ssl: false
secret-id: your-secret-id
secret-key: your-secret-key
base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
bucket: xxxxx-10005608
region: ap-shanghai
endpoint: ""
s3-force-path-style: false
disable-ssl: false
secret-id: your-secret-id
secret-key: your-secret-key
base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server
# cloudflare r2 configuration
cloudflare-r2:
bucket: xxxx0bucket
base-url: https://gin.vue.admin.com
path: uploads
account-id: xxx_account_id
access-key-id: xxx_key_id
secret-access-key: xxx_secret_key
# huawei obs configuration
hua-wei-obs:
path: you-path
bucket: you-bucket
endpoint: you-endpoint
access-key: you-access-key
secret-key: you-secret-key
path: you-path
bucket: you-bucket
endpoint: you-endpoint
access-key: you-access-key
secret-key: you-secret-key
# excel configuration
excel:
dir: ./resource/excel/
dir: ./resource/excel/
# timer task db clear table
Timer:
start: true
spec: "@daily" # 定时任务详细配置参考 https://pkg.go.dev/github.com/robfig/cron/v3
detail:
- tableName: sys_operation_records
compareField: created_at
interval: 2160h
- tableName: jwt_blacklists
compareField: created_at
interval: 168h
# disk usage configuration
disk-list:
- mount-point: "/"
# 跨域配置
# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用
cors:
mode: whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
whitelist:
- allow-origin: example1.com
allow-headers: content-type
allow-methods: GET, POST
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
- allow-origin: example2.com
allow-headers: content-type
allow-methods: GET, POST
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
whitelist:
- allow-origin: example1.com
allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
allow-methods: POST, GET
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
- allow-origin: example2.com
allow-headers: content-type
allow-methods: GET, POST
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
mcp:
name: GVA_MCP
version: v1.0.0
sse_path: /sse
message_path: /message
url_prefix: ''

View File

@ -1,10 +1,18 @@
package config
import (
"fmt"
"net"
"net/url"
)
type Oracle struct {
GeneralDB `yaml:",inline" mapstructure:",squash"`
}
func (m *Oracle) Dsn() string {
return "oracle://" + m.Username + ":" + m.Password + "@" + m.Path + ":" + m.Port + "/" + m.Dbname + "?" + m.Config
dsn := fmt.Sprintf("oracle://%s:%s@%s/%s?%s", url.PathEscape(m.Username), url.PathEscape(m.Password),
net.JoinHostPort(m.Path, m.Port), url.PathEscape(m.Dbname), m.Config)
return dsn
}

View File

@ -35,7 +35,7 @@ func RunServer() {
fmt.Printf(`
欢迎使用 gin-vue-admin
当前版本:v2.8.4
当前版本:%s
加群方式:微信号shouzi_1994 QQ群470239250
项目地址https://github.com/flipped-aurora/gin-vue-admin
插件市场:https://plugin.gin-vue-admin.com
@ -48,6 +48,7 @@ func RunServer() {
** 版权所有方flipped-aurora开源团队 **
** 版权持有公司北京翻转极光科技有限责任公司 **
** 剔除授权标识需购买商用授权https://gin-vue-admin.com/empower/index.html **
`, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展**
`, global.Version, address, address, global.GVA_CONFIG.MCP.SSEPath, address, global.GVA_CONFIG.MCP.MessagePath)
initServer(address, Router, 10*time.Minute, 10*time.Minute)
}

View File

@ -1,7 +1,10 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs
import "github.com/swaggo/swag"
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/swaggo/swag"
)
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
@ -10411,7 +10414,7 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "v2.8.4",
Version: global.Version,
Host: "",
BasePath: "",
Schemes: []string{},

12
server/global/version.go Normal file
View File

@ -0,0 +1,12 @@
package global
// Version 版本信息
// 目前只有Version正式使用 其余为预留
const (
// Version 当前版本号
Version = "v2.8.5"
// AppName 应用名称
AppName = "Gin-Vue-Admin"
// Description 应用描述
Description = "使用gin+vue进行极速开发的全栈开发基础平台"
)

View File

@ -7,6 +7,7 @@ require (
github.com/aws/aws-sdk-go v1.55.6
github.com/casbin/casbin/v2 v2.103.0
github.com/casbin/gorm-adapter/v3 v3.32.0
github.com/dzwvip/gorm-oracle v0.1.2
github.com/fsnotify/fsnotify v1.8.0
github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0
@ -17,6 +18,7 @@ require (
github.com/gookit/color v1.5.4
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/kirklin/go-swd v0.0.3
github.com/mark3labs/mcp-go v0.31.0
github.com/mholt/archives v0.1.1
github.com/minio/minio-go/v7 v7.0.84
@ -75,6 +77,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gammazero/toposort v0.1.1 // indirect
@ -109,7 +112,6 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kirklin/go-swd v0.0.3 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
@ -141,6 +143,7 @@ require (
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sijms/go-ora/v2 v2.7.17 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/sorairolake/lzip-go v0.3.5 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@ -149,6 +152,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/thoas/go-funk v0.7.0 // indirect
github.com/tidwall/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect

View File

@ -115,6 +115,10 @@ github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj6
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dzwvip/gorm-oracle v0.1.2 h1:811aFDY7oDfKWHc0Z0lHdXzzr89EmKBSwc/jLJ8GU5g=
github.com/dzwvip/gorm-oracle v0.1.2/go.mod h1:TbF7idnO9UgGpJ0qJpDZby1/wGquzP5GYof88ScBITE=
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
@ -291,6 +295,7 @@ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJk
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -457,6 +462,8 @@ github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFt
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/sijms/go-ora/v2 v2.7.17 h1:M/pYIqjaMUeBxyzOWp2oj4ntF6fHSBloJWGNH9vbmsU=
github.com/sijms/go-ora/v2 v2.7.17/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
github.com/silenceper/wechat/v2 v2.1.9 h1:wc092gUkGbbBRTdzPxROhQhOH5iE98stnfzKA73mnTo=
github.com/silenceper/wechat/v2 v2.1.9/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
@ -508,6 +515,8 @@ github.com/tencentyun/cos-go-sdk-v5 v0.7.60 h1:/e/tmvRmfKexr/QQIBzWhOkZWsmY3EK72
github.com/tencentyun/cos-go-sdk-v5 v0.7.60/go.mod h1:8+hG+mQMuRP/OIS9d83syAvXvrMj9HhkND6Q1fLghw0=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/thoas/go-funk v0.7.0 h1:GmirKrs6j6zJbhJIficOsz2aAI7700KsU/5YrdHRM1Y=
github.com/thoas/go-funk v0.7.0/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@ -529,8 +538,6 @@ github.com/unrolled/secure v1.17.0 h1:Io7ifFgo99Bnh0J7+Q+qcMzWM6kaDPCA5FroFZEdbW
github.com/unrolled/secure v1.17.0/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
github.com/volcengine/volc-sdk-golang v1.0.23 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU=
github.com/volcengine/volcengine-go-sdk v1.1.25 h1:wwR2DTJGw2sOZ1wTWaQLn03PGO0O+motGvsoVvAp5Zk=
github.com/volcengine/volcengine-go-sdk v1.1.25/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo=
github.com/volcengine/volcengine-go-sdk v1.1.31 h1:qrNd/fu+ZWzH93EBSPBSntGwgwo7cHITxxv1IdvLxls=
github.com/volcengine/volcengine-go-sdk v1.1.31/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
@ -894,6 +901,7 @@ gorm.io/driver/sqlserver v1.5.4 h1:xA+Y1KDNspv79q43bPyjDMUgHoYHLhXYmdFcYPobg8g=
gorm.io/driver/sqlserver v1.5.4/go.mod h1:+frZ/qYmuna11zHPlh5oc2O6ZA/lS88Keb0XSH1Zh/g=
gorm.io/gen v0.3.26 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY=
gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=

View File

@ -28,7 +28,9 @@ func GormMssql() *gorm.DB {
DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
}
if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
// 数据库配置
general := m.GeneralDB
if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil {
return nil
} else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
@ -48,7 +50,9 @@ func GormMssqlByConfig(m config.Mssql) *gorm.DB {
DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
}
if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
// 数据库配置
general := m.GeneralDB
if db, err := gorm.Open(sqlserver.New(mssqlConfig), internal.Gorm.Config(general)); err != nil {
panic(err)
} else {
db.InstanceSet("gorm:table_options", "ENGINE=InnoDB")

View File

@ -34,8 +34,9 @@ func initMysqlDatabase(m config.Mysql) *gorm.DB {
DefaultStringSize: 191, // string 类型字段的默认长度
SkipInitializeWithVersion: false, // 根据版本自动配置
}
if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
// 数据库配置
general := m.GeneralDB
if db, err := gorm.Open(mysql.New(mysqlConfig), internal.Gorm.Config(general)); err != nil {
panic(err)
} else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)

View File

@ -1,18 +1,14 @@
package initialize
import (
//"github.com/dzwvip/oracle"
oracle "github.com/dzwvip/gorm-oracle"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/initialize/internal"
//_ "github.com/godror/godror"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// GormOracle 初始化oracle数据库
// 如果需要Oracle库 放开import里的注释 把下方 mysql.Config 改为 oracle.Config ; mysql.New 改为 oracle.New
func GormOracle() *gorm.DB {
m := global.GVA_CONFIG.Oracle
return initOracleDatabase(m)
@ -28,13 +24,9 @@ func initOracleDatabase(m config.Oracle) *gorm.DB {
if m.Dbname == "" {
return nil
}
oracleConfig := mysql.Config{
DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度
}
if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
// 数据库配置
general := m.GeneralDB
if db, err := gorm.Open(oracle.Open(m.Dsn()), internal.Gorm.Config(general)); err != nil {
panic(err)
} else {
sqlDB, _ := db.DB()

View File

@ -30,7 +30,9 @@ func initPgSqlDatabase(p config.Pgsql) *gorm.DB {
DSN: p.Dsn(), // DSN data source name
PreferSimpleProtocol: false,
}
if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(p.Prefix, p.Singular)); err != nil {
// 数据库配置
general := p.GeneralDB
if db, err := gorm.Open(postgres.New(pgsqlConfig), internal.Gorm.Config(general)); err != nil {
panic(err)
} else {
sqlDB, _ := db.DB()

View File

@ -25,7 +25,9 @@ func initSqliteDatabase(s config.Sqlite) *gorm.DB {
return nil
}
if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(s.Prefix, s.Singular)); err != nil {
// 数据库配置
general := s.GeneralDB
if db, err := gorm.Open(sqlite.Open(s.Dsn()), internal.Gorm.Config(general)); err != nil {
panic(err)
} else {
sqlDB, _ := db.DB()

View File

@ -1,12 +1,12 @@
package internal
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"time"
)
var Gorm = new(_gorm)
@ -15,22 +15,7 @@ type _gorm struct{}
// Config gorm 自定义配置
// Author [SliverHorn](https://github.com/SliverHorn)
func (g *_gorm) Config(prefix string, singular bool) *gorm.Config {
var general config.GeneralDB
switch global.GVA_CONFIG.System.DbType {
case "mysql":
general = global.GVA_CONFIG.Mysql.GeneralDB
case "pgsql":
general = global.GVA_CONFIG.Pgsql.GeneralDB
case "oracle":
general = global.GVA_CONFIG.Oracle.GeneralDB
case "sqlite":
general = global.GVA_CONFIG.Sqlite.GeneralDB
case "mssql":
general = global.GVA_CONFIG.Mssql.GeneralDB
default:
general = global.GVA_CONFIG.Mysql.GeneralDB
}
func (g *_gorm) Config(general config.GeneralDB) *gorm.Config {
return &gorm.Config{
Logger: logger.New(NewWriter(general), logger.Config{
SlowThreshold: 200 * time.Millisecond,
@ -38,8 +23,8 @@ func (g *_gorm) Config(prefix string, singular bool) *gorm.Config {
Colorful: true,
}),
NamingStrategy: schema.NamingStrategy{
TablePrefix: prefix,
SingularTable: singular,
TablePrefix: general.Prefix,
SingularTable: general.Singular,
},
DisableForeignKeyConstraintWhenMigrating: true,
}

View File

@ -21,7 +21,7 @@ import (
// @Tag.Description 用户
// @title Gin-Vue-Admin Swagger API接口文档
// @version v2.8.4
// @version v2.8.5
// @description 使用gin+vue进行极速开发的全栈开发基础平台
// @securityDefinitions.apikey ApiKeyAuth
// @in header

View File

@ -41,7 +41,11 @@ type ApiCreator struct{}
// New 创建API创建工具
func (a *ApiCreator) New() mcp.Tool {
return mcp.NewTool("create_api",
mcp.WithDescription("创建后端API记录用于AI编辑器自动添加API接口时自动创建对应的API权限记录。注意使用gva_auto_generate创建的包和模块会自动创建API权限无需调用此工具。仅在AI编辑器自动添加API或router下的文件产生路径变化时使用。"),
mcp.WithDescription(`创建后端API记录用于AI编辑器自动添加API接口时自动创建对应的API权限记录
**重要限制**
- 当使用gva_auto_generate工具且needCreatedModules=true时模块创建会自动生成API权限不应调用此工具
- 仅在以下情况使用1) 单独创建API不涉及模块创建2) AI编辑器自动添加API3) router下的文件产生路径变化时`),
mcp.WithString("path",
mcp.Required(),
mcp.Description("API路径/user/create"),

165
server/mcp/api_lister.go Normal file
View File

@ -0,0 +1,165 @@
package mcpTool
import (
"context"
"encoding/json"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
func init() {
// 注册工具将在enter.go中统一处理
RegisterTool(&ApiLister{})
}
// ApiInfo API信息结构
type ApiInfo struct {
ID uint `json:"id,omitempty"` // 数据库ID仅数据库API有
Path string `json:"path"` // API路径
Description string `json:"description,omitempty"` // API描述
ApiGroup string `json:"apiGroup,omitempty"` // API组
Method string `json:"method"` // HTTP方法
Source string `json:"source"` // 来源database 或 gin
}
// ApiListResponse API列表响应结构
type ApiListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
DatabaseApis []ApiInfo `json:"databaseApis"` // 数据库中的API
GinApis []ApiInfo `json:"ginApis"` // gin框架中的API
TotalCount int `json:"totalCount"` // 总数量
}
// ApiLister API列表工具
type ApiLister struct{}
// New 创建API列表工具
func (a *ApiLister) New() mcp.Tool {
return mcp.NewTool("list_all_apis",
mcp.WithDescription(`获取系统中所有的API接口分为两组
**功能说明**
- 返回数据库中已注册的API列表
- 返回gin框架中实际注册的路由API列表
- 帮助前端判断是使用现有API还是需要创建新的API,如果api在前端未使用且需要前端调用的时候请到api文件夹下对应模块的js中添加方法并暴露给当前业务调用
**返回数据结构**
- databaseApis: 数据库中的API记录包含ID描述分组等完整信息
- ginApis: gin路由中的API仅包含路径和方法需要AI根据路径自行揣摩路径的业务含义例如/api/user/:id 表示根据用户ID获取用户信息`),
)
}
// Handle 处理API列表请求
func (a *ApiLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取数据库中的API
databaseApis, err := a.getDatabaseApis()
if err != nil {
global.GVA_LOG.Error("获取数据库API失败", zap.Error(err))
errorResponse := ApiListResponse{
Success: false,
Message: "获取数据库API失败: " + err.Error(),
}
resultJSON, _ := json.Marshal(errorResponse)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(resultJSON),
},
},
}, nil
}
// 获取gin路由中的API
ginApis, err := a.getGinApis()
if err != nil {
global.GVA_LOG.Error("获取gin路由API失败", zap.Error(err))
errorResponse := ApiListResponse{
Success: false,
Message: "获取gin路由API失败: " + err.Error(),
}
resultJSON, _ := json.Marshal(errorResponse)
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(resultJSON),
},
},
}, nil
}
// 构建响应
response := ApiListResponse{
Success: true,
Message: "获取API列表成功",
DatabaseApis: databaseApis,
GinApis: ginApis,
TotalCount: len(databaseApis) + len(ginApis),
}
global.GVA_LOG.Info("API列表获取成功",
zap.Int("数据库API数量", len(databaseApis)),
zap.Int("gin路由API数量", len(ginApis)),
zap.Int("总数量", response.TotalCount))
resultJSON, err := json.Marshal(response)
if err != nil {
return nil, fmt.Errorf("序列化结果失败: %v", err)
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(resultJSON),
},
},
}, nil
}
// getDatabaseApis 获取数据库中的所有API
func (a *ApiLister) getDatabaseApis() ([]ApiInfo, error) {
var apis []system.SysApi
err := global.GVA_DB.Model(&system.SysApi{}).Order("api_group ASC, path ASC").Find(&apis).Error
if err != nil {
return nil, err
}
// 转换为ApiInfo格式
var result []ApiInfo
for _, api := range apis {
result = append(result, ApiInfo{
ID: api.ID,
Path: api.Path,
Description: api.Description,
ApiGroup: api.ApiGroup,
Method: api.Method,
Source: "database",
})
}
return result, nil
}
// getGinApis 获取gin路由中的所有API包含被忽略的API
func (a *ApiLister) getGinApis() ([]ApiInfo, error) {
// 从gin路由信息中获取所有API
var result []ApiInfo
for _, route := range global.GVA_ROUTERS {
result = append(result, ApiInfo{
Path: route.Path,
Method: route.Method,
Source: "gin",
})
}
return result, nil
}

View File

@ -1,81 +0,0 @@
package mcpTool
import (
"context"
"errors"
"fmt"
"github.com/mark3labs/mcp-go/mcp"
"time"
)
func init() {
RegisterTool(&CurrentTime{})
}
type CurrentTime struct {
}
// 获取当前系统时间
func (t *CurrentTime) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取当前系统时间
timezone, ok := request.GetArguments()["timezone"].(string)
if !ok {
return nil, errors.New("参数错误timezone 必须是字符串类型")
}
// 根据timezone参数加载对应时区
loc, err := loadTimeZone(timezone)
if err != nil {
return nil, err
}
// 获取当前时间并转换为指定时区
currentTime := time.Now().In(loc).Format("2006-01-02 15:04:05")
//返回
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("%s 时区的当前时间是:%s", timezone, currentTime),
},
},
}, nil
}
func (t *CurrentTime) New() mcp.Tool {
return mcp.NewTool("currentTime",
mcp.WithDescription("获取当前系统时间"),
mcp.WithString("timezone",
mcp.Required(),
mcp.Description("时区"),
mcp.Enum("UTC", "CST", "PST", "EST", "GMT", "CET", "JST", "MST", "IST", "AST", "HST"),
))
}
// 将简写时区转换为IANA标准时区
func loadTimeZone(timezone string) (*time.Location, error) {
// 时区映射表
timezoneMap := map[string]string{
"UTC": "UTC",
"CST": "Asia/Shanghai", // 中国标准时间
"PST": "America/Los_Angeles",
"EST": "America/New_York",
"GMT": "GMT",
"CET": "Europe/Paris",
"JST": "Asia/Tokyo",
"MST": "America/Denver",
"IST": "Asia/Kolkata",
"AST": "Asia/Riyadh", // 阿拉伯标准时间
"HST": "Pacific/Honolulu",
}
// 获取标准时区名称
tzName, exists := timezoneMap[timezone]
if !exists {
return nil, errors.New("不支持的时区: " + timezone)
}
// 加载时区
return time.LoadLocation(tzName)
}

View File

@ -7,13 +7,13 @@ ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了
```go
type ExecutionPlan struct {
PackageName string `json:"packageName"` // 包名,如:"user", "order", "product"
PackageType string `json:"packageType"` // "plugin" 或 "package"
NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包
NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息当NeedCreatedPackage=true时必需
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组当NeedCreatedModules=true时必需支持批量创建
Paths map[string]string `json:"paths,omitempty"` // 路径信息
PackageName string `json:"packageName"` // 包名,如:"user", "order", "product"
PackageType string `json:"packageType"` // "plugin" 或 "package"
NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包
NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息当NeedCreatedPackage=true时必需
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组当NeedCreatedModules=true时必需支持批量创建
Paths map[string]string `json:"paths,omitempty"` // 路径信息
}
```
@ -501,19 +501,19 @@ type DataSource struct {
2. **NeedCreatedPackage**: 当为true时PackageInfo必须提供
3. **NeedCreatedModules**: 当为true时ModulesInfo必须提供
4. **字段类型**: FieldType支持的类型包括
- string字符串
- richtext富文本
- int整型
- bool布尔值
- float64浮点型
- time.Time时间
- enum枚举
- picture单图片字符串
- pictures多图片json字符串
- video视频字符串
- file文件json字符串
- jsonJSON
- array数组
- string字符串
- richtext富文本
- int整型
- bool布尔值
- float64浮点型
- time.Time时间
- enum枚举
- picture单图片字符串
- pictures多图片json字符串
- video视频字符串
- file文件json字符串
- jsonJSON
- array数组
5. **搜索类型**: FieldSearchType支持EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等
6. **索引类型**: FieldIndexType支持index, unique等
7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段

View File

@ -1,79 +0,0 @@
package mcpTool
import (
"context"
"errors"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
"gorm.io/gorm"
)
func init() {
RegisterTool(&GetNickname{})
}
type GetNickname struct{}
// 根据用户username获取nickname
func (t *GetNickname) New() mcp.Tool {
return mcp.NewTool("getNickname",
mcp.WithDescription("根据用户username获取nickname"),
mcp.WithString("username",
mcp.Required(),
mcp.Description("用户的username"),
))
}
// Handle 处理获取昵称的请求
func (t *GetNickname) Handle(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 1. 参数验证
username, ok := request.GetArguments()["username"].(string)
if !ok {
return nil, errors.New("参数错误username 必须是字符串类型")
}
if username == "" {
return nil, errors.New("参数错误username 不能为空")
}
// 2. 记录操作日志
global.GVA_LOG.Info("getNickname 工具被调用")
// 3. 优化查询,只选择需要的字段
var user struct {
NickName string
}
err := global.GVA_DB.Model(&system.SysUser{}).
Select("nick_name").
Where("username = ?", username).
First(&user).Error
// 4. 优化错误处理
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("用户 %s 不存在", username),
},
},
}, nil
}
global.GVA_LOG.Error("数据库查询错误")
return nil, errors.New("系统错误,请稍后再试")
}
// 构造回复信息
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("用户 %s 的昵称是 %s", username, user.NickName),
},
},
}, nil
}

View File

@ -9,6 +9,7 @@ import (
"path/filepath"
"strings"
"time"
"unicode"
"github.com/flipped-aurora/gin-vue-admin/server/global"
common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
@ -135,6 +136,10 @@ func (t *AutomationModuleAnalyzer) New() mcp.Tool {
2. AI分析需求为1xxx2xxx格式 gva_auto_generate执行创建
3. 创建完成后根据需要使用其他辅助工具
**重要限制**
- 当needCreatedModules=true时模块创建会自动生成API和菜单因此不应再调用api_creator和menu_creator工具
- 只有在单独创建API或菜单不涉及模块创建时才使用api_creator和menu_creator工具
重要ExecutionPlan结构体格式要求支持批量创建
{
"packageName": "包名(string)",
@ -169,10 +174,10 @@ func (t *AutomationModuleAnalyzer) New() mcp.Tool {
"generateWeb": "是否生成前端(bool)",
"generateServer": "是否生成后端(bool)",
"fields": [{
"fieldName": "字段名(string)",
"fieldName": "字段名(string)必须大写开头",
"fieldDesc": "字段描述(string)",
"fieldType": "字段类型:string/int/bool/time.Time等(string)",
"fieldJson": "JSON标签(string)",
"fieldType": "字段类型支持string字符串,richtext富文本,int整型,bool布尔值,float64浮点型,time.Time时间,enum枚举,picture单图片字符串,pictures多图片json字符串,video视频字符串,file文件json字符串,jsonJSON,array数组",
"fieldJson": "JSON标签(string 必须是小驼峰命名,例:userName)",
"dataTypeLong": "数据长度(string)",
"comment": "注释(string)",
"columnName": "数据库列名(string)",
@ -184,7 +189,7 @@ func (t *AutomationModuleAnalyzer) New() mcp.Tool {
"desc": "详情显示(bool)",
"excel": "导入导出(bool)",
"require": "是否必填(bool)",
"defaultValue": "默认值(string)",
"defaultValue": "默认值(string)JSON类型array,json,file,pictures请保持为空他们不可以设置默认值",
"errorText": "错误提示(string)",
"clearable": "是否可清空(bool)",
"sort": "是否排序(bool)",
@ -803,8 +808,8 @@ func (t *AutomationModuleAnalyzer) handleAnalyze(ctx context.Context, request mc
"fields": [{
"fieldName": "字段名(必须大写开头)",
"fieldDesc": "字段描述",
"fieldType": "GO 语言的数据类型",
"fieldJson": "json标签",
"fieldType": "字段类型支持string字符串,richtext富文本,int整型,bool布尔值,float64浮点型,time.Time时间,enum枚举,picture单图片字符串,pictures多图片json字符串,video视频字符串,file文件json字符串,jsonJSON,array数组",
"fieldJson": "json标签(string 必须是小驼峰命名,例:userName)",
"dataTypeLong": "长度",
"comment": "注释",
"columnName": "数据库列名",
@ -1239,6 +1244,7 @@ func (t *AutomationModuleAnalyzer) validateExecutionPlan(plan *ExecutionPlan) er
if field.FieldName == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1)
}
if field.FieldDesc == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1)
}
@ -1252,6 +1258,48 @@ func (t *AutomationModuleAnalyzer) validateExecutionPlan(plan *ExecutionPlan) er
return fmt.Errorf("模块 %d 字段 %d 的 columnName 不能为空", moduleIndex+1, i+1)
}
// 确保字段名首字母大写
if len(field.FieldName) > 0 {
firstChar := string(field.FieldName[0])
if firstChar >= "a" && firstChar <= "z" {
moduleInfo.Fields[i].FieldName = strings.ToUpper(firstChar) + field.FieldName[1:]
}
}
// 确保FieldJson使用小驼峰命名
if len(field.FieldJson) > 0 {
// 处理下划线命名转小驼峰
if strings.Contains(field.FieldJson, "_") {
parts := strings.Split(field.FieldJson, "_")
camelCase := strings.ToLower(parts[0])
for j := 1; j < len(parts); j++ {
if len(parts[j]) > 0 {
camelCase += strings.ToUpper(string(parts[j][0])) + strings.ToLower(parts[j][1:])
}
}
moduleInfo.Fields[i].FieldJson = camelCase
} else {
// 处理首字母大写转小写
firstChar := string(field.FieldJson[0])
if firstChar >= "A" && firstChar <= "Z" {
moduleInfo.Fields[i].FieldJson = strings.ToLower(firstChar) + field.FieldJson[1:]
}
}
}
// 确保ColumnName使用下划线命名
if len(field.ColumnName) > 0 {
// 将驼峰命名转换为下划线命名
var result strings.Builder
for i, r := range field.ColumnName {
if i > 0 && r >= 'A' && r <= 'Z' {
result.WriteRune('_')
}
result.WriteRune(unicode.ToLower(r))
}
moduleInfo.Fields[i].ColumnName = result.String()
}
// 验证字段类型
validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"}
validType := false
@ -1365,6 +1413,13 @@ func (t *AutomationModuleAnalyzer) executeCreation(ctx context.Context, plan *Ex
}
result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo))
// 添加重要提醒不要使用其他MCP工具
result.Message += "\n\n⚠ 重要提醒:\n"
result.Message += "模块创建已完成API和菜单已自动生成。请不要再调用以下MCP工具\n"
result.Message += "- api_creatorAPI权限已在模块创建时自动生成\n"
result.Message += "- menu_creator前端菜单已在模块创建时自动生成\n"
result.Message += "如需修改API或菜单请直接在系统管理界面中进行配置。\n"
}
result.Message += "已构建目录结构信息; "

View File

@ -64,7 +64,11 @@ type MenuCreator struct{}
// New 创建菜单创建工具
func (m *MenuCreator) New() mcp.Tool {
return mcp.NewTool("create_menu",
mcp.WithDescription("创建前端菜单记录用于AI编辑器自动添加前端页面时自动创建对应的菜单项。注意使用gva_auto_generate创建的包和模块会自动创建菜单项无需调用此工具。仅在AI编辑器自动添加前端页面时使用。"),
mcp.WithDescription(`创建前端菜单记录用于AI编辑器自动添加前端页面时自动创建对应的菜单项
**重要限制**
- 当使用gva_auto_generate工具且needCreatedModules=true时模块创建会自动生成菜单项不应调用此工具
- 仅在以下情况使用1) 单独创建菜单不涉及模块创建2) AI编辑器自动添加前端页面时`),
mcp.WithNumber("parentId",
mcp.Description("父菜单ID0表示根菜单"),
mcp.DefaultNumber(0),

112
server/mcp/menu_lister.go Normal file
View File

@ -0,0 +1,112 @@
package mcpTool
import (
"context"
"encoding/json"
"fmt"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/system"
"github.com/mark3labs/mcp-go/mcp"
"go.uber.org/zap"
)
// 注册工具
func init() {
// 注册工具将在enter.go中统一处理
RegisterTool(&MenuLister{})
}
// MenuListResponse 菜单列表响应结构
type MenuListResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
Menus []system.SysBaseMenu `json:"menus"`
TotalCount int `json:"totalCount"`
Description string `json:"description"`
}
// MenuLister 菜单列表工具
type MenuLister struct{}
// New 创建菜单列表工具
func (m *MenuLister) New() mcp.Tool {
return mcp.NewTool("list_all_menus",
mcp.WithDescription(`获取系统中所有菜单信息包括菜单树结构路由信息组件路径等用于前端编写vue-router时正确跳转
**功能说明**
- 返回完整的菜单树形结构
- 包含路由配置信息pathnamecomponent
- 包含菜单元数据titleiconkeepAlive等
- 包含菜单参数和按钮配置
- 支持父子菜单关系展示
**使用场景**
- 前端路由配置获取所有菜单信息用于配置vue-router
- 菜单权限管理了解系统中所有可用的菜单项
- 导航组件开发构建动态导航菜单
- 系统架构分析了解系统的菜单结构和页面组织`),
)
}
// Handle 处理菜单列表请求
func (m *MenuLister) Handle(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 获取所有基础菜单
allMenus, err := m.getAllMenus()
if err != nil {
global.GVA_LOG.Error("获取菜单列表失败", zap.Error(err))
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("获取菜单列表失败: %v", err),
},
},
IsError: true,
}, nil
}
// 构建返回结果
response := MenuListResponse{
Success: true,
Message: "获取菜单列表成功",
Menus: allMenus,
TotalCount: len(allMenus),
Description: "系统中所有菜单信息的标准列表,包含路由配置和组件信息",
}
// 序列化响应
responseJSON, err := json.MarshalIndent(response, "", " ")
if err != nil {
global.GVA_LOG.Error("序列化菜单响应失败", zap.Error(err))
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: fmt.Sprintf("序列化响应失败: %v", err),
},
},
IsError: true,
}, nil
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.TextContent{
Type: "text",
Text: string(responseJSON),
},
},
}, nil
}
// getAllMenus 获取所有基础菜单
func (m *MenuLister) getAllMenus() ([]system.SysBaseMenu, error) {
var menus []system.SysBaseMenu
err := global.GVA_DB.Order("sort").Preload("Parameters").Preload("MenuBtn").Find(&menus).Error
if err != nil {
return nil, err
}
return menus, nil
}

View File

@ -20,6 +20,8 @@ type RequirementAnalysisRequest struct {
UserRequirement string `json:"userRequirement"`
}
// RequirementAnalysisResponse 需求分析响应
type RequirementAnalysisResponse struct {
AIPrompt string `json:"aiPrompt"` // 给AI的提示词

View File

@ -2,8 +2,6 @@ package response
import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/model/pet"
)
// ChatResponse 宠物助手聊天响应结构体
@ -26,37 +24,37 @@ type StreamEvent struct {
Data interface{} `json:"data"` // 事件数据
}
// StreamMessageData 流式消息数据结构体
// StreamMessageData 流式消息数据结构体(简化版)
type StreamMessageData struct {
Delta string `json:"delta"` // 增量消息内容
SessionId string `json:"sessionId"` // 会话ID
RequestId string `json:"requestId"` // 请求ID
Delta string `json:"delta"` // 增量消息内容
}
// StreamErrorData 流式错误数据结构体
// StreamErrorData 流式错误数据结构体(简化版)
type StreamErrorData struct {
Error string `json:"error"` // 错误信息
Code string `json:"code"` // 错误代码
RequestId string `json:"requestId"` // 请求ID
Error string `json:"error"` // 错误信息
}
// StreamDoneData 流式完成数据结构体
// StreamDoneData 流式完成数据结构体(简化版)
type StreamDoneData struct {
Message string `json:"message"` // 完整消息内容
SessionId string `json:"sessionId"` // 会话ID
RequestId string `json:"requestId"` // 请求ID
TokenCount int `json:"tokenCount"` // Token消耗数量
ResponseTime int64 `json:"responseTime"` // 响应时间(毫秒)
IsSensitive bool `json:"isSensitive"` // 是否包含敏感词
FinishReason string `json:"finishReason"` // 完成原因
Message string `json:"message"` // 完整消息内容
SessionId string `json:"sessionId"` // 会话ID
}
// ChatHistoryItem 聊天历史项结构体(简化版)
type ChatHistoryItem struct {
ID uint `json:"id"` // 消息ID
Role string `json:"role"` // 角色user/assistant
Message string `json:"message"` // 消息内容
SessionId string `json:"sessionId"` // 会话ID
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// ChatHistoryResponse 聊天历史响应结构体
type ChatHistoryResponse struct {
List []pet.PetAiAssistantConversations `json:"list"` // 对话记录列表
Total int64 `json:"total"` // 总记录数
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页大小
List []ChatHistoryItem `json:"list"` // 对话记录列表
Total int64 `json:"total"` // 总记录数
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页大小
}
// SessionInfo 会话信息结构体

View File

@ -20,13 +20,15 @@ type ExportVersionRequest struct {
Description string `json:"description"` // 版本描述
MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表
ApiIds []uint `json:"apiIds"` // 选中的API ID列表
DictIds []uint `json:"dictIds"` // 选中的字典ID列表
}
// ImportVersionRequest 导入版本请求结构体
type ImportVersionRequest struct {
VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息
ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据直接复用SysBaseMenu
ExportApi []system.SysApi `json:"apis"` // API数据直接复用SysApi
VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息
ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据直接复用SysBaseMenu
ExportApi []system.SysApi `json:"apis"` // API数据直接复用SysApi
ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据直接复用SysDictionary
}
// VersionInfo 版本信息结构体

View File

@ -7,7 +7,8 @@ import (
// ExportVersionResponse 导出版本响应结构体
type ExportVersionResponse struct {
Version request.VersionInfo `json:"version"` // 版本信息
Menus []system.SysBaseMenu `json:"menus"` // 菜单数据直接复用SysBaseMenu
Apis []system.SysApi `json:"apis"` // API数据直接复用SysApi
}
Version request.VersionInfo `json:"version"` // 版本信息
Menus []system.SysBaseMenu `json:"menus"` // 菜单数据直接复用SysBaseMenu
Apis []system.SysApi `json:"apis"` // API数据直接复用SysApi
Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据直接复用SysDictionary
}

View File

@ -8,10 +8,10 @@ import (
// 版本管理 结构体 SysVersion
type SysVersion struct {
global.GVA_MODEL
VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称
VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号
Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述
VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据
VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称
VersionCode *string `json:"versionCode" form:"versionCode" gorm:"comment:版本号;column:version_code;size:100;" binding:"required"` //版本号
Description *string `json:"description" form:"description" gorm:"comment:版本描述;column:description;size:500;"` //版本描述
VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据
}
// TableName 版本管理 SysVersion自定义表名 sys_versions

View File

@ -110,7 +110,7 @@ getDataSourceFunc()
<el-date-picker
v-model="searchInfo.createdAtRange"
class="w-[380px]"
class="!w-380px"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
@ -161,7 +161,7 @@ getDataSourceFunc()
>
<el-table-column type="selection" width="55" />
{{ if .GvaModel }}
<el-table-column sortable align="left" label="日期" prop="CreatedAt" {{- if .IsTree }} min-{{- end -}}width="180">
<el-table-column sortable align="left" label="日期" prop="CreatedAt" {{ if .IsTree -}} min-{{- end -}}width="180">
<template #default="scope">{{ "{{ formatDate(scope.row.CreatedAt) }}" }}</template>
</el-table-column>
{{ end }}
@ -393,8 +393,8 @@ const searchInfo = ref({})
// 排序
const sortChange = ({ prop, order }) => {
const sortMap = {
CreatedAt:"CreatedAt",
ID:"ID",
CreatedAt:"created_at",
ID:"id",
{{- range .Fields}}
{{- if .Table}}
{{- if and .Sort}}

View File

@ -108,7 +108,7 @@ getDataSourceFunc()
</template>
<el-date-picker
v-model="searchInfo.createdAtRange"
class="w-[380px]"
class="!w-380px"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
@ -158,7 +158,7 @@ getDataSourceFunc()
>
<el-table-column type="selection" width="55" />
{{ if .GvaModel }}
<el-table-column sortable align="left" label="日期" prop="CreatedAt" {{- if .IsTree }} min-{{- end -}}width="180">
<el-table-column sortable align="left" label="日期" prop="CreatedAt" {{ if .IsTree -}} min-{{- end -}}width="180">
<template #default="scope">{{ "{{ formatDate(scope.row.CreatedAt) }}" }}</template>
</el-table-column>
{{ end }}
@ -388,8 +388,8 @@ const searchInfo = ref({})
// 排序
const sortChange = ({ prop, order }) => {
const sortMap = {
CreatedAt:"CreatedAt",
ID:"ID",
CreatedAt:"created_at",
ID:"id",
{{- range .Fields}}
{{- if .Table}}
{{- if and .Sort}}

View File

@ -16,8 +16,6 @@ func (p *PetAssistantRouter) InitPetAssistantRouter(UserRouter *gin.RouterGroup,
{
// 宠物助手问答相关路由
petAssistantRouter.POST("ask", petAssistantApi.AskPetAssistant) // 向宠物助手提问
petAssistantRouter.POST("stream-ask", petAssistantApi.StreamAskPetAssistant) // 向宠物助手流式提问
petAssistantRouter.GET("history", petAssistantApi.GetAssistantHistory) // 获取宠物助手对话历史
petAssistantRouter.DELETE("clear-history", petAssistantApi.ClearAssistantHistory) // 清空宠物助手对话历史
petAssistantRouter.GET("sessions", petAssistantApi.GetAssistantSessions) // 获取宠物助手会话列表

View File

@ -18,7 +18,7 @@ func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) {
userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息
userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息
userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组
userRouter.POST("resetPassword", baseApi.ResetPassword) // 设置用户权限组
userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码
userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置
}
{

View File

@ -140,8 +140,8 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
global.GVA_LOG.Error("敏感词检测失败", zap.Error(err))
eventChan <- StreamEvent{
Event: "error",
Data: map[string]interface{}{
"error": "敏感词检测失败",
Data: petResponse.StreamErrorData{
Error: "敏感词检测失败",
},
}
return err
@ -158,18 +158,17 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
eventChan <- StreamEvent{
Event: "message",
Data: ChatResponse{
Message: "您的消息包含不当内容,请重新输入。",
SessionId: req.SessionId,
IsSensitive: true,
TokenCount: 0,
ResponseTime: time.Since(startTime).Milliseconds(),
Data: petResponse.StreamMessageData{
Delta: "您的消息包含不当内容,请重新输入。",
},
}
eventChan <- StreamEvent{
Event: "done",
Data: map[string]interface{}{"finished": true},
Data: petResponse.StreamDoneData{
Message: "您的消息包含不当内容,请重新输入。",
SessionId: req.SessionId,
},
}
return nil
}
@ -186,8 +185,8 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
global.GVA_LOG.Error("获取对话历史失败", zap.Error(err))
eventChan <- StreamEvent{
Event: "error",
Data: map[string]interface{}{
"error": "获取对话历史失败",
Data: petResponse.StreamErrorData{
Error: "获取对话历史失败",
},
}
return err
@ -215,8 +214,8 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
global.GVA_LOG.Error("LLM流式调用失败", zap.Error(err))
eventChan <- StreamEvent{
Event: "error",
Data: map[string]interface{}{
"error": "LLM调用失败",
Data: petResponse.StreamErrorData{
Error: "LLM调用失败",
},
}
}
@ -246,9 +245,8 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
// 转发消息给客户端
eventChan <- StreamEvent{
Event: "message",
Data: map[string]interface{}{
"delta": delta,
"sessionId": sessionId,
Data: petResponse.StreamMessageData{
Delta: delta,
},
}
}
@ -284,13 +282,9 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
// 发送完成事件
eventChan <- StreamEvent{
Event: "done",
Data: ChatResponse{
Message: fullMessage,
SessionId: sessionId,
IsSensitive: aiHasSensitive,
TokenCount: tokenCount,
ResponseTime: responseTime,
RequestId: llmReq.RequestID,
Data: petResponse.StreamDoneData{
Message: fullMessage,
SessionId: sessionId,
},
}
return nil
@ -300,8 +294,8 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
global.GVA_LOG.Error("LLM流式响应错误", zap.Any("error", llmEvent.Data.Error))
eventChan <- StreamEvent{
Event: "error",
Data: map[string]interface{}{
"error": "LLM响应错误",
Data: petResponse.StreamErrorData{
Error: "LLM响应错误",
},
}
return fmt.Errorf("LLM响应错误")
@ -311,7 +305,7 @@ func (p *PetChatService) StreamChat(ctx context.Context, userId uint, req ChatRe
}
// GetChatHistory 获取对话历史
func (p *PetChatService) GetChatHistory(ctx context.Context, userId uint, sessionId string, limit int) ([]pet.PetAiAssistantConversations, error) {
func (p *PetChatService) GetChatHistory(ctx context.Context, userId uint, sessionId string, limit int) ([]petResponse.ChatHistoryItem, error) {
var conversations []pet.PetAiAssistantConversations
query := global.GVA_DB.WithContext(ctx).Where("user_id = ?", userId)
@ -331,7 +325,28 @@ func (p *PetChatService) GetChatHistory(ctx context.Context, userId uint, sessio
conversations[i], conversations[j] = conversations[j], conversations[i]
}
return conversations, nil
// 转换为简化结构
items := make([]petResponse.ChatHistoryItem, 0, len(conversations))
for _, conv := range conversations {
if conv.MessageContent != nil && conv.Role != nil {
item := petResponse.ChatHistoryItem{
ID: conv.ID,
Role: *conv.Role,
Message: *conv.MessageContent,
SessionId: "",
CreatedAt: conv.CreatedAt,
}
// 设置会话ID如果存在
if conv.SessionId != nil {
item.SessionId = *conv.SessionId
}
items = append(items, item)
}
}
return items, nil
}
// SaveConversation 保存对话记录
@ -368,7 +383,7 @@ func (p *PetChatService) GetChatSessions(ctx context.Context, userId uint) ([]ma
}
// buildMessages 构建LLM请求消息
func (p *PetChatService) buildMessages(history []pet.PetAiAssistantConversations, userMessage string) []utils.LLMMessage {
func (p *PetChatService) buildMessages(history []petResponse.ChatHistoryItem, userMessage string) []utils.LLMMessage {
messages := []utils.LLMMessage{
{
Role: "system",
@ -378,12 +393,10 @@ func (p *PetChatService) buildMessages(history []pet.PetAiAssistantConversations
// 添加历史对话
for _, conv := range history {
if conv.MessageContent != nil && conv.Role != nil {
messages = append(messages, utils.LLMMessage{
Role: *conv.Role,
Content: *conv.MessageContent,
})
}
messages = append(messages, utils.LLMMessage{
Role: conv.Role,
Content: conv.Message,
})
}
// 添加当前用户消息

View File

@ -64,7 +64,7 @@ WHERE
lower(a.table_name) = ?
AND lower(a.OWNER) = ?
ORDER BY
a.COLUMN_ID;
a.COLUMN_ID
`
err = global.GVA_DBList[businessDB].Raw(sql, tableName, dbName).Scan(&entities).Error

View File

@ -86,6 +86,12 @@ func (sysVersionService *SysVersionService) GetApisByIds(ctx context.Context, id
return
}
// GetDictionariesByIds 根据ID列表获取字典数据
func (sysVersionService *SysVersionService) GetDictionariesByIds(ctx context.Context, ids []uint) (dictionaries []system.SysDictionary, err error) {
err = global.GVA_DB.Where("id in ?", ids).Preload("SysDictionaryDetails").Find(&dictionaries).Error
return
}
// ImportMenus 导入菜单数据
func (sysVersionService *SysVersionService) ImportMenus(ctx context.Context, menus []system.SysBaseMenu) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
@ -194,3 +200,31 @@ func (sysVersionService *SysVersionService) ImportApis(apis []system.SysApi) err
return nil
})
}
// ImportDictionaries 导入字典数据
func (sysVersionService *SysVersionService) ImportDictionaries(dictionaries []system.SysDictionary) error {
return global.GVA_DB.Transaction(func(tx *gorm.DB) error {
for _, dict := range dictionaries {
// 检查字典是否已存在
var existingDict system.SysDictionary
if err := tx.Where("type = ?", dict.Type).First(&existingDict).Error; err == nil {
// 字典已存在,跳过
continue
}
// 创建新字典
newDict := system.SysDictionary{
Name: dict.Name,
Type: dict.Type,
Status: dict.Status,
Desc: dict.Desc,
SysDictionaryDetails: dict.SysDictionaryDetails,
}
if err := tx.Create(&newDict).Error; err != nil {
return err
}
}
return nil
})
}

View File

@ -66,7 +66,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e
}
dicts[2].SysDictionaryDetails = []sysModel.SysDictionaryDetail{
{Label: "date", Status: &True},
{Label: "date", Value: "0", Status: &True, Extend: "mysql", Sort: 0},
{Label: "time", Value: "1", Status: &True, Extend: "mysql", Sort: 1},
{Label: "year", Value: "2", Status: &True, Extend: "mysql", Sort: 2},
{Label: "datetime", Value: "3", Status: &True, Extend: "mysql", Sort: 3},
@ -74,7 +74,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e
{Label: "timestamptz", Value: "6", Status: &True, Extend: "pgsql", Sort: 5},
}
dicts[3].SysDictionaryDetails = []sysModel.SysDictionaryDetail{
{Label: "float", Status: &True},
{Label: "float", Value: "0", Status: &True, Extend: "mysql", Sort: 0},
{Label: "double", Value: "1", Status: &True, Extend: "mysql", Sort: 1},
{Label: "decimal", Value: "2", Status: &True, Extend: "mysql", Sort: 2},
{Label: "numeric", Value: "3", Status: &True, Extend: "pgsql", Sort: 3},
@ -82,7 +82,7 @@ func (i *initDictDetail) InitializeData(ctx context.Context) (context.Context, e
}
dicts[4].SysDictionaryDetails = []sysModel.SysDictionaryDetail{
{Label: "char", Status: &True},
{Label: "char", Value: "0", Status: &True, Extend: "mysql", Sort: 0},
{Label: "varchar", Value: "1", Status: &True, Extend: "mysql", Sort: 1},
{Label: "tinyblob", Value: "2", Status: &True, Extend: "mysql", Sort: 2},
{Label: "tinytext", Value: "3", Status: &True, Extend: "mysql", Sort: 3},

View File

@ -70,7 +70,7 @@ func (i *initUser) InitializeData(ctx context.Context) (next context.Context, er
Username: "a303176530",
Password: password,
NickName: "用户1",
HeaderImg: "https:///qmplusimg.henrongyi.top/1572075907logo.png",
HeaderImg: "https://qmplusimg.henrongyi.top/1572075907logo.png",
AuthorityId: 9528,
Phone: "17611111111",
Email: "333333333@qq.com"},

View File

@ -46,7 +46,8 @@ func GenerateField(field systemReq.AutoCodeField) string {
gormTag += "column:" + field.ColumnName + ";"
if field.DataTypeLong != "" && field.FieldType != "enum" {
// 对于int类型根据DataTypeLong决定具体的Go类型不使用size标签
if field.DataTypeLong != "" && field.FieldType != "enum" && field.FieldType != "int" {
gormTag += fmt.Sprintf("size:%s;", field.DataTypeLong)
}
@ -85,8 +86,27 @@ func GenerateField(field systemReq.AutoCodeField) string {
tagContent := fmt.Sprintf(`json:"%s" form:"%s" gorm:"%s"`,
field.FieldJson, field.FieldJson, gormTag)
// 对于int类型根据DataTypeLong决定具体的Go类型
var fieldType string
if field.FieldType == "int" {
switch field.DataTypeLong {
case "1", "2", "3":
fieldType = "int8"
case "4", "5":
fieldType = "int16"
case "6", "7", "8", "9", "10":
fieldType = "int32"
case "11", "12", "13", "14", "15", "16", "17", "18", "19", "20":
fieldType = "int64"
default:
fieldType = "int64"
}
} else {
fieldType = field.FieldType
}
result = fmt.Sprintf(`%s *%s `+"`"+`%s`+"`"+``,
field.FieldName, field.FieldType, tagContent)
field.FieldName, fieldType, tagContent)
}
if field.Require {
@ -222,11 +242,11 @@ func GenerateSearchFormItem(field systemReq.AutoCodeField) string {
`
} else if field.FieldType == "float64" || field.FieldType == "int" {
if field.FieldSearchType == "BETWEEN" || field.FieldSearchType == "NOT BETWEEN" {
result += fmt.Sprintf(` <el-input class="w-40" v-model.number="searchInfo.start%s" placeholder="最小值" />
result += fmt.Sprintf(` <el-input class="!w-40" v-model.number="searchInfo.start%s" placeholder="最小值" />
`, field.FieldName)
result += `
`
result += fmt.Sprintf(` <el-input class="w-40" v-model.number="searchInfo.end%s" placeholder="最大值" />
result += fmt.Sprintf(` <el-input class="!w-40" v-model.number="searchInfo.end%s" placeholder="最大值" />
`, field.FieldName)
} else {
result += fmt.Sprintf(` <el-input v-model.number="searchInfo.%s" placeholder="搜索条件" />
@ -250,7 +270,7 @@ func GenerateSearchFormItem(field systemReq.AutoCodeField) string {
`
result += ` </template>
`
result += fmt.Sprintf(`<el-date-picker class="w-[380px]" v-model="searchInfo.%sRange" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"></el-date-picker>`, field.FieldJson)
result += fmt.Sprintf(`<el-date-picker class="!w-380px" v-model="searchInfo.%sRange" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间"></el-date-picker>`, field.FieldJson)
} else {
result += fmt.Sprintf(`<el-date-picker v-model="searchInfo.%s" type="datetime" placeholder="搜索条件"></el-date-picker>`, field.FieldJson)
}

View File

@ -34,17 +34,11 @@ func (s *SensitiveWordUtil) InitDetector() error {
detector, err := swd.New()
if err != nil {
s.initErr = err
// 只有在global.GVA_LOG不为nil时才记录日志
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Failed to initialize sensitive word detector", zap.Error(err))
}
global.GVA_LOG.Error("Failed to initialize sensitive word detector", zap.Error(err))
return
}
s.detector = detector
// 只有在global.GVA_LOG不为nil时才记录日志
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Sensitive word detector initialized successfully")
}
global.GVA_LOG.Info("Sensitive word detector initialized successfully")
})
return s.initErr
}

View File

@ -5,7 +5,7 @@ var (
ApiVerify = Rules{"Path": {NotEmpty()}, "Description": {NotEmpty()}, "ApiGroup": {NotEmpty()}, "Method": {NotEmpty()}}
MenuVerify = Rules{"Path": {NotEmpty()}, "Name": {NotEmpty()}, "Component": {NotEmpty()}, "Sort": {Ge("0")}}
MenuMetaVerify = Rules{"Title": {NotEmpty()}}
LoginVerify = Rules{"CaptchaId": {NotEmpty()}, "Captcha": {NotEmpty()}, "Username": {NotEmpty()}, "Password": {NotEmpty()}}
LoginVerify = Rules{"Username": {NotEmpty()}, "Password": {NotEmpty()}}
RegisterVerify = Rules{"Username": {NotEmpty()}, "NickName": {NotEmpty()}, "Password": {NotEmpty()}, "AuthorityId": {NotEmpty()}}
PageInfoVerify = Rules{"Page": {NotEmpty()}, "PageSize": {NotEmpty()}}
CustomerVerify = Rules{"CustomerName": {NotEmpty()}, "CustomerPhoneData": {NotEmpty()}}

View File

@ -98,18 +98,14 @@ func (v *VolcengineLLMUtil) InitClient() error {
// 验证配置
if config.ApiKey == "" {
v.initErr = fmt.Errorf("volcengine api key is empty")
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Volcengine configuration error", zap.Error(v.initErr))
}
global.GVA_LOG.Error("Volcengine configuration error", zap.Error(v.initErr))
return
}
// 创建ARK Runtime客户端使用API Key
v.client = arkruntime.NewClientWithApiKey(config.ApiKey)
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Volcengine LLM client initialized successfully")
}
global.GVA_LOG.Info("Volcengine LLM client initialized successfully")
})
return v.initErr
}
@ -165,9 +161,7 @@ func (v *VolcengineLLMUtil) ChatCompletion(req LLMRequest) (*LLMResponse, error)
// 调用API
resp, err := v.client.CreateChatCompletion(context.Background(), chatReq)
if err != nil {
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
}
global.GVA_LOG.Error("Chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
return nil, err
}
@ -199,9 +193,7 @@ func (v *VolcengineLLMUtil) ChatCompletion(req LLMRequest) (*LLMResponse, error)
Usage: usage,
}
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Chat completion successful", zap.String("requestID", req.RequestID))
}
global.GVA_LOG.Info("Chat completion successful", zap.String("requestID", req.RequestID))
return chatResp, nil
}
@ -264,16 +256,12 @@ func (v *VolcengineLLMUtil) StreamChatCompletion(req LLMRequest, eventChan chan<
// 调用流式API
stream_resp, err := v.client.CreateChatCompletionStream(ctx, chatReq)
if err != nil {
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Stream chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
}
global.GVA_LOG.Error("Stream chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
return err
}
defer stream_resp.Close()
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Stream chat completion started", zap.String("requestID", req.RequestID))
}
global.GVA_LOG.Info("Stream chat completion started", zap.String("requestID", req.RequestID))
// 处理流式响应
for {
@ -293,14 +281,10 @@ func (v *VolcengineLLMUtil) StreamChatCompletion(req LLMRequest, eventChan chan<
Object: "chat.completion.chunk",
},
}
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Stream chat completion finished", zap.String("requestID", req.RequestID))
}
global.GVA_LOG.Info("Stream chat completion finished", zap.String("requestID", req.RequestID))
return nil
}
if global.GVA_LOG != nil {
global.GVA_LOG.Error("Stream receive error", zap.Error(err), zap.String("requestID", req.RequestID))
}
global.GVA_LOG.Error("Stream receive error", zap.Error(err), zap.String("requestID", req.RequestID))
eventChan <- LLMStreamEvent{
Event: "error",
Data: LLMResponse{
@ -359,9 +343,7 @@ func (v *VolcengineLLMUtil) StopGeneration(requestID string) error {
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
cancelFunc()
v.activeSessions.Delete(requestID)
if global.GVA_LOG != nil {
global.GVA_LOG.Info("Generation stopped", zap.String("requestID", requestID))
}
global.GVA_LOG.Info("Generation stopped", zap.String("requestID", requestID))
return nil
}
}

View File

@ -1,6 +1,6 @@
// 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级)
const fs = require('fs')
const path = require('path')
import fs from 'fs'
import path from 'path'
const wfPath = path.resolve(__dirname, './node_modules/.bin')
fs.readdir(wfPath, (err, files) => {

View File

@ -3,7 +3,7 @@
未经授权的商用使用可能会被我们的资产搜索引擎爬取并可能导致后续索赔索赔金额将不低于高级授权费的十倍请您遵守版权法律法规尊重知识产权
*/
var child_process = require('child_process')
import child_process from 'child_process'
var url = 'https://www.gin-vue-admin.com'
var cmd = ''

View File

@ -1,6 +1,6 @@
{
"name": "gin-vue-admin",
"version": "2.8.4",
"version": "2.8.5",
"private": true,
"scripts": {
"serve": "node openDocument.js && vite --host --mode development",
@ -9,10 +9,12 @@
"preview": "vite preview",
"fix-memory-limit": "cross-env LIMIT=4096 increase-memory-limit"
},
"type": "module",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@form-create/designer": "^3.2.6",
"@form-create/element-ui": "^3.2.10",
"@unocss/transformer-directives": "^66.4.2",
"@vue-office/docx": "^1.6.2",
"@vue-office/excel": "^1.7.11",
"@vue-office/pdf": "^2.0.2",
@ -39,10 +41,9 @@
"screenfull": "^6.0.2",
"sortablejs": "^1.15.3",
"spark-md5": "^3.0.2",
"tailwindcss": "^3.4.10",
"universal-cookie": "^7",
"vform3-builds": "^3.0.10",
"vite-auto-import-svg": "^1.6.0",
"vite-auto-import-svg": "^1.9.0",
"vue": "^3.5.7",
"vue-cropper": "^1.1.4",
"vue-echarts": "^7.0.3",
@ -55,6 +56,9 @@
"devDependencies": {
"@babel/eslint-parser": "^7.25.1",
"@eslint/js": "^8.56.0",
"@unocss/extractor-svelte": "^66.4.2",
"@unocss/preset-wind3": "^66.4.2",
"@unocss/vite": "^66.5.0",
"@vitejs/plugin-legacy": "^6.0.0",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/cli-plugin-babel": "~5.0.8",
@ -69,8 +73,10 @@
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.19.2",
"globals": "^16.3.0",
"sass": "^1.78.0",
"terser": "^5.31.6",
"unocss": "^66.4.2",
"vite": "^6.2.3",
"vite-plugin-banner": "^0.8.0",
"vite-plugin-importer": "^0.2.5",

View File

@ -1,6 +0,0 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
}

View File

@ -7,13 +7,13 @@
<!-- 弹窗头部 -->
<div class="p-5 border-b border-gray-100 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-800">{{ displayData.title }}</h3>
<button class="text-gray-400 hover:text-gray-600 transition-colors" @click="closeModal">
<close />
</button>
<div class="text-gray-400 hover:text-gray-600 transition-colors cursor-pointer" @click="closeModal">
<close class="h-6 w-6" />
</div>
</div>
<!-- 弹窗内容 -->
<div class="p-6">
<div class="p-6 pt-0">
<!-- 错误类型 -->
<div class="mb-4">
<div class="text-xs font-medium text-gray-500 uppercase mb-2">错误类型</div>
@ -45,16 +45,16 @@
<!-- 弹窗底部 -->
<div class="py-2 px-4 border-t border-gray-100 flex justify-end">
<button class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm shadow-sm" @click="handleConfirm">
<div class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors font-medium text-sm shadow-sm cursor-pointer" @click="handleConfirm">
确定
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref, computed, onMounted } from 'vue';
import { defineProps, defineEmits, computed } from 'vue';
const props = defineProps({
errorData: {

View File

@ -27,6 +27,7 @@
import { ElMessage } from 'element-plus'
import { getUrl } from '@/utils/image'
import { useUserStore } from '@/pinia/modules/user'
const emits = defineEmits(['change', 'update:modelValue'])
@ -35,6 +36,7 @@
emits('update:modelValue', valueHtml.value)
}
const userStore = useUserStore()
const props = defineProps({
modelValue: {
type: String,
@ -53,6 +55,9 @@
editorConfig.MENU_CONF['uploadImage'] = {
fieldName: 'file',
server: basePath + '/fileUploadAndDownload/upload?noSave=1',
headers: {
'x-token': userStore.token,
},
customInsert(res, insertFn) {
if (res.code === 0) {
const urlPath = getUrl(res.data.file.url)

View File

@ -47,7 +47,7 @@
const model = defineModel({ type: Array })
const fileList = ref(model.value)
const fileList = ref(model.value || [])
const emits = defineEmits(['on-success', 'on-error'])

View File

@ -1,6 +1,8 @@
/**
* 网站配置文件
*/
import packageInfo from '../../package.json'
const greenText = (text) => `\x1b[32m${text}\x1b[0m`
const config = {
@ -17,7 +19,7 @@ export const viteLogo = (env) => {
`> 欢迎使用Gin-Vue-Admin开源地址https://github.com/flipped-aurora/gin-vue-admin`
)
)
console.log(greenText(`> 当前版本:v2.8.4`))
console.log(greenText(`> 当前版本:v${packageInfo.version}`))
console.log(greenText(`> 加群方式:微信shouzi_1994 QQ群470239250`))
console.log(
greenText(`> 项目地址https://github.com/flipped-aurora/gin-vue-admin`)

View File

@ -4,13 +4,14 @@
* */
// 加载网站配置文件夹
import { register } from './global'
import packageInfo from '../../package.json'
export default {
install: (app) => {
register(app)
console.log(`
欢迎使用 Gin-Vue-Admin
当前版本:v2.8.4
当前版本:v${packageInfo.version}
加群方式:微信shouzi_1994 QQ群622360840
项目地址https://github.com/flipped-aurora/gin-vue-admin
插件市场:https://plugin.gin-vue-admin.com
@ -22,6 +23,7 @@ export default {
** 版权所有方flipped-aurora开源团队 **
** 版权持有公司北京翻转极光科技有限责任公司 **
** 剔除授权标识需购买商用授权https://gin-vue-admin.com/empower/index.html **
** 感谢您对Gin-Vue-Admin的支持与关注 合法授权使用更有利于项目的长久发展**
`)
}
}

View File

@ -7,22 +7,7 @@ export default {
// 当被绑定的元素插入到 DOM 中时……
mounted: function (el, binding) {
const userInfo = userStore.userInfo
let type = ''
switch (Object.prototype.toString.call(binding.value)) {
case '[object Array]':
type = 'Array'
break
case '[object String]':
type = 'String'
break
case '[object Number]':
type = 'Number'
break
default:
type = ''
break
}
if (type === '') {
if (!binding.value){
el.parentNode.removeChild(el)
return
}

View File

@ -1,5 +1,6 @@
import './style/element_visiable.scss'
import 'element-plus/theme-chalk/dark/css-vars.css'
import 'uno.css';
import { createApp } from 'vue'
import ElementPlus from 'element-plus'

View File

@ -93,7 +93,7 @@ export const useUserStore = defineStore('user', () => {
}
if (!router.hasRoute(userInfo.value.authority.defaultRouter)) {
ElMessage.error('请联系管理员进行授权')
ElMessage.error('不存在可以登陆的首页,请联系管理员进行配置')
} else {
await router.replace({ name: userInfo.value.authority.defaultRouter })
}

View File

@ -1,9 +1,6 @@
@use '@/style/main.scss';
@use '@/style/reset';
@tailwind base;
@tailwind components;
@tailwind utilities;
.el-button {
font-weight: 400;

View File

@ -17,7 +17,15 @@
}
.gva-btn-list {
@apply mb-3 flex items-center;
@apply mb-3 flex items-center flex-wrap gap-2;
.el-button+.el-button{
@apply ml-0 !important;
}
.el-upload{
.el-button{
@apply ml-0 !important;
}
}
}
#nprogress .bar {

View File

@ -24,6 +24,7 @@
<script setup>
import { useUserStore } from '@/pinia/modules/user'
import { useRouter } from 'vue-router'
import { emitter } from '@/utils/bus'
defineOptions({
name: 'Error'
@ -32,6 +33,17 @@
const userStore = useUserStore()
const router = useRouter()
const toDashboard = () => {
router.push({ name: userStore.userInfo.authority.defaultRouter })
try {
router.push({ name: userStore.userInfo.authority.defaultRouter })
} catch (error) {
emitter.emit('show-error', {
code: '401',
message: "检测到其他用户修改了路由权限,请重新登录",
fn: () => {
userStore.ClearStorage()
router.push({ name: 'Login', replace: true })
}
})
}
}
</script>

View File

@ -136,7 +136,7 @@
// @ts-ignore
import { initDB } from '@/api/initdb'
import { reactive, ref } from 'vue'
import { ElLoading, ElMessage } from 'element-plus'
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
defineOptions({
@ -274,7 +274,25 @@
type: 'success',
message: res.msg
})
router.push({ name: 'Login' })
// AI
ElMessageBox.confirm(
'已经完成基础数据库初始化建议先进行编辑器AI助手配置以获得更好的开发体验。',
'配置完成',
{
confirmButtonText: '查看AI配置文档',
cancelButtonText: '稍后配置',
type: 'success',
center: true
}
).then(() => {
// AI
window.open('https://www.gin-vue-admin.com/guide/server/mcp.html', '_blank')
router.push({ name: 'Login' })
}).catch(() => {
//
router.push({ name: 'Login' })
})
}
loading.close()
} catch (_) {

View File

@ -7,7 +7,7 @@
<el-menu
:default-active="routerStore.topActive"
mode="horizontal"
class="border-r-0 border-b-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
class="!border-r-0 border-b-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
unique-opened
@select="(index, _, ele) => selectMenuItem(index, _, ele, true)"
>
@ -34,7 +34,7 @@
:collapse="isCollapse"
:collapse-transition="false"
:default-active="active"
class="border-r-0 w-full"
class="!border-r-0 w-full"
unique-opened
@select="(index, _, ele) => selectMenuItem(index, _, ele, false)"
>

View File

@ -6,7 +6,7 @@
<el-menu
:default-active="active"
mode="horizontal"
class="border-r-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
class="!border-r-0 w-full flex gap-1 items-center box-border h-[calc(100%-1px)]"
unique-opened
:ellipsis="shouldEllipsis"
@select="selectMenuItem"

View File

@ -11,7 +11,7 @@
:collapse="isCollapse"
:collapse-transition="false"
:default-active="active"
class="border-r-0 w-full"
class="!border-r-0 w-full"
unique-opened
@select="selectMenuItem"
>

View File

@ -2,7 +2,7 @@
<div class="flex h-full">
<!-- 一级菜单常驻侧边栏 -->
<div
class="relative h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
class="relative !h-full bg-white text-slate-700 dark:text-slate-300 dark:bg-slate-900 border-r shadow dark:shadow-gray-700"
:style="{
width: config.layout_side_collapsed_width + 'px'
}"
@ -12,7 +12,7 @@
:collapse="true"
:collapse-transition="false"
:default-active="topActive"
class="border-r-0 w-full"
class="!border-r-0 w-full"
unique-opened
@select="selectTopMenuItem"
>
@ -74,7 +74,7 @@
:collapse="isCollapse"
:collapse-transition="false"
:default-active="active"
class="border-r-0 w-full"
class="!border-r-0 w-full"
unique-opened
@select="selectMenuItem"
>

View File

@ -8,7 +8,7 @@
<el-tooltip class="" effect="dark" content="视频教程" placement="bottom">
<el-dropdown @command="toDoc">
<el-icon
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
>
<Film />
</el-icon>
@ -29,7 +29,7 @@
<el-tooltip class="" effect="dark" content="搜索" placement="bottom">
<el-icon
@click="handleCommand"
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
>
<Search />
</el-icon>
@ -37,7 +37,7 @@
<el-tooltip class="" effect="dark" content="系统设置" placement="bottom">
<el-icon
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
@click="toggleSetting"
>
<Setting />
@ -46,7 +46,7 @@
<el-tooltip class="" effect="dark" content="刷新" placement="bottom">
<el-icon
class="w-8 h-8 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 dark:border-gray-600 cursor-pointer border-solid"
:class="showRefreshAnmite ? 'animate-spin' : ''"
@click="toggleRefresh"
>
@ -61,14 +61,14 @@
>
<el-icon
v-if="appStore.isDark"
class="w-8 h-8 shadow rounded-full border border-gray-600 cursor-pointer border-solid"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-600 cursor-pointer border-solid"
@click="appStore.toggleTheme(false)"
>
<Sunny />
</el-icon>
<el-icon
v-else
class="w-8 h-8 shadow rounded-full border border-gray-200 cursor-pointer border-solid"
class="w-8 h-8 p-2 shadow rounded-full border border-gray-200 cursor-pointer border-solid"
@click="appStore.toggleTheme(true)"
>
<Moon />

View File

@ -7,11 +7,11 @@
:font="font"
:z-index="9999"
:gap="[180, 150]"
class="absolute inset-0 pointer-events-none"
class="!absolute !inset-0 !pointer-events-none"
:content="userStore.userInfo.nickName"
/>
<gva-header />
<div class="flex flex-row w-full gva-container pt-16 box-border h-full">
<div class="flex flex-row w-full gva-container pt-16 box-border !h-full">
<gva-aside
v-if="
config.side_mode === 'normal' || config.side_mode === 'sidebar' ||

View File

@ -22,14 +22,14 @@
</div>
</template>
<div class="h-full bg-white dark:bg-gray-900">
<div class="bg-white dark:bg-gray-900">
<div class="px-8 pt-4 pb-6 border-b border-gray-200 dark:border-gray-700">
<div class="flex justify-center">
<div class="inline-flex bg-gray-100 dark:bg-gray-800 rounded-xl p-1.5 border border-gray-200 dark:border-gray-700 shadow-sm">
<button
<div
v-for="tab in tabs"
:key="tab.key"
class="px-6 py-3 text-base font-medium rounded-lg transition-all duration-150 ease-in-out min-w-[80px]"
class="px-6 py-3 text-base text-center cursor-pointer font-medium rounded-lg transition-all duration-150 ease-in-out min-w-[80px]"
:class="[
activeTab === tab.key
? 'text-white shadow-md transform -translate-y-0.5'
@ -39,7 +39,7 @@
@click="activeTab = tab.key"
>
{{ tab.label }}
</button>
</div>
</div>
</div>
</div>

View File

@ -8,7 +8,7 @@
v-model="activeValue"
:closable="!(historys.length === 1 && $route.name === defaultRouter)"
type="card"
class="bg-white text-slate-700 dark:text-slate-500 dark:bg-slate-900"
class="bg-white text-slate-700 dark:text-slate-500 dark:bg-slate-900 pt-1"
@contextmenu.prevent="openContextMenu($event)"
@tab-click="changeTab"
@tab-remove="removeTab"

View File

@ -23,7 +23,7 @@
<template #default="{ node, data }">
<span class="custom-tree-node">
<span>{{ node.label }}</span>
<span v-if="node.checked">
<span v-if="node.checked && !data.name?.startsWith('http://') && !data.name?.startsWith('https://')">
<el-button
type="primary"
link

View File

@ -6,7 +6,7 @@
:options="pathOptions"
v-model="activeComponent"
filterable
class="w-full"
class="!w-full"
clearable
@change="emitChange"
/>

View File

@ -117,9 +117,9 @@
<el-input v-model="exportForm.description" type="textarea" placeholder="请输入版本描述" />
</el-form-item>
<el-form-item label="发版信息">
<div class="flex gap-5 w-full">
<div class="flex gap-3 w-full">
<!-- 菜单选择 -->
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/2">
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
<span class="m-0 text-gray-800 text-base font-medium">选择菜单</span>
</div>
@ -140,7 +140,7 @@
</div>
<!-- API选择 -->
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/2">
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
<span class="m-0 text-gray-800 text-base font-medium">选择API</span>
</div>
@ -153,7 +153,7 @@
<el-tree ref="apiTreeRef" :data="apiTreeData" :default-checked-keys="selectedApiIds"
:props="apiTreeProps" default-expand-all highlight-current node-key="onlyId" show-checkbox
:filter-node-method="filterApiNode" @check="onApiCheck" class="api-tree">
<template #default="{ _, data }">
<template #default="{ data }">
<div class="flex items-center justify-between w-full pr-1">
<span>{{ data.description }}</span>
<el-tooltip :content="data.path">
@ -166,6 +166,32 @@
</el-tree>
</div>
</div>
<!-- 字典选择 -->
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full flex-1 w-1/3">
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
<span class="m-0 text-gray-800 text-base font-medium">选择字典</span>
</div>
<div class="px-4 py-3 border-b border-gray-300 bg-gray-50">
<el-input v-model="dictFilterText" placeholder="输入关键字进行过滤" clearable size="small" />
</div>
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
<el-tree ref="dictTreeRef" :data="dictTreeData" :default-checked-keys="selectedDictIds"
:props="dictTreeProps" default-expand-all highlight-current node-key="ID" show-checkbox
:filter-node-method="filterDictNode" @check="onDictCheck" class="dict-tree">
<template #default="{ data }">
<div class="flex items-center justify-between w-full pr-1">
<span>{{ data.name || data.label }}</span>
<el-tooltip :content="data.desc || (data.value ? `值: ${data.value}` : '')">
<span class="text-gray-500 text-xs ml-2">
{{ data.type || (data.value ? `值: ${data.value}` : '') }}
</span>
</el-tooltip>
</div>
</template>
</el-tree>
</div>
</div>
</div>
</el-form-item>
</el-form>
@ -212,8 +238,8 @@
</el-form-item>
<el-form-item label="预览内容" v-if="importPreviewData">
<div class="flex flex-col flex-1 gap-4 border border-gray-300 rounded p-4 bg-gray-50">
<div class="flex gap-5 w-full">
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/2">
<div class="flex gap-3 w-full">
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
<h3 class="m-0 text-gray-800 text-base font-medium">菜单 ({{ getTotalMenuCount() }})</h3>
@ -238,7 +264,7 @@
</div>
</div>
</div>
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/2">
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
<h3 class="m-0 text-gray-800 text-base font-medium">API ({{ importPreviewData.apis?.length || 0 }})</h3>
@ -263,6 +289,33 @@
</div>
</div>
</div>
<div class="border border-gray-300 rounded overflow-hidden flex-1 w-1/3">
<div class="flex flex-col border border-gray-300 rounded overflow-hidden h-full">
<div class="flex justify-between items-center px-4 py-3 bg-gray-50 border-b border-gray-300">
<h3 class="m-0 text-gray-800 text-base font-medium">字典 ({{ importPreviewData.dictionaries?.length || 0 }})</h3>
</div>
<div class="flex-1 p-2 min-h-[300px] max-h-[400px] overflow-y-auto">
<el-tree
:data="previewDictTreeData"
:props="dictTreeProps"
node-key="ID"
:expand-on-click-node="false"
:check-on-click-node="false"
:show-checkbox="false"
default-expand-all
>
<template #default="{ data }">
<div class="flex-1 flex items-center justify-between text-sm pr-2">
<span>{{ data.name || data.label }}</span>
<span class="text-gray-500 text-xs ml-2">
{{ data.type || (data.value ? `值: ${data.value}` : '') }}
</span>
</div>
</template>
</el-tree>
</div>
</div>
</div>
</div>
</div>
</el-form-item>
@ -286,14 +339,13 @@ import {
// API
import { getMenuList } from '@/api/menu'
import { getApiList } from '@/api/api'
import { getSysDictionaryList } from '@/api/sysDictionary'
//
import { getDictFunc, formatDate, filterDict } from '@/utils/format'
import { formatDate } from '@/utils/format'
import { ElMessage, ElMessageBox } from 'element-plus'
import { UploadFilled } from '@element-plus/icons-vue'
import { ref, reactive, watch } from 'vue'
//
import { useBtnAuth } from '@/utils/btnAuth'
import { ref, watch } from 'vue'
import { useAppStore } from "@/pinia"
defineOptions({
@ -313,21 +365,26 @@ const exportForm = ref({
versionCode: '',
description: '',
menuIds: [],
apiIds: []
apiIds: [],
dictIds: []
})
//
const menuTreeData = ref([])
const apiTreeData = ref([])
const dictTreeData = ref([])
const selectedMenuIds = ref([])
const selectedApiIds = ref([])
const selectedDictIds = ref([])
const menuFilterText = ref('')
const apiFilterTextName = ref('')
const apiFilterTextPath = ref('')
const dictFilterText = ref('')
//
const menuTreeRef = ref(null)
const apiTreeRef = ref(null)
const dictTreeRef = ref(null)
//
const menuTreeProps = ref({
@ -342,6 +399,21 @@ const apiTreeProps = ref({
label: 'description'
})
const dictTreeProps = ref({
children: 'sysDictionaryDetails',
label: function (data) {
//
if (data.name) {
return data.name
}
//
if (data.label) {
return data.label
}
return '未知项'
}
})
//
const importDialogVisible = ref(false)
const importLoading = ref(false)
@ -350,36 +422,10 @@ const importPreviewData = ref(null)
const uploadRef = ref(null)
const previewMenuTreeData = ref([])
const previewApiTreeData = ref([])
const previewDictTreeData = ref([])
//
const rule = reactive({
versionName: [{
required: true,
message: '请输入版本名称',
trigger: ['input', 'blur'],
},
{
whitespace: true,
message: '不能只输入空格',
trigger: ['input', 'blur'],
}
],
versionCode: [{
required: true,
message: '请输入版本号',
trigger: ['input', 'blur'],
},
{
whitespace: true,
message: '不能只输入空格',
trigger: ['input', 'blur'],
}
]
})
const elFormRef = ref()
const elSearchFormRef = ref()
// =========== ===========
@ -549,6 +595,19 @@ const getMenuAndApiList = async () => {
}
}
//
const getDictList = async () => {
try {
const dictRes = await getSysDictionaryList({ page: 1, pageSize: 9999 })
if (dictRes.code === 0) {
dictTreeData.value = dictRes.data || []
}
} catch (error) {
console.error('获取字典数据失败:', error)
ElMessage.error('获取字典数据失败')
}
}
// API
const buildApiTree = (apis) => {
const apiObj = {}
@ -595,6 +654,20 @@ const filterApiNode = (value, data) => {
return matchesName && matchesPath
}
const filterDictNode = (value, data) => {
if (!value) return true
const name = data.name || ''
const type = data.type || ''
const desc = data.desc || ''
const label = data.label || ''
const dataValue = data.value || ''
return name.indexOf(value) !== -1 ||
type.indexOf(value) !== -1 ||
desc.indexOf(value) !== -1 ||
label.indexOf(value) !== -1 ||
dataValue.indexOf(value) !== -1
}
const onMenuCheck = (data, checked) => {
if (checked.checkedKeys) {
selectedMenuIds.value = checked.checkedKeys
@ -607,6 +680,12 @@ const onApiCheck = (data, checked) => {
}
}
const onDictCheck = (data, checked) => {
if (checked.checkedKeys) {
selectedDictIds.value = checked.checkedKeys
}
}
//
watch(menuFilterText, (val) => {
if (menuTreeRef.value) {
@ -620,10 +699,17 @@ watch([apiFilterTextName, apiFilterTextPath], () => {
}
})
watch(dictFilterText, (val) => {
if (dictTreeRef.value) {
dictTreeRef.value.filter(val)
}
})
//
const openExportDialog = async () => {
exportDialogVisible.value = true
await getMenuAndApiList()
await getDictList()
}
const closeExportDialog = () => {
@ -633,13 +719,16 @@ const closeExportDialog = () => {
versionCode: '',
description: '',
menuIds: [],
apiIds: []
apiIds: [],
dictIds: []
}
selectedMenuIds.value = []
selectedApiIds.value = []
selectedDictIds.value = []
menuFilterText.value = ''
apiFilterTextName.value = ''
apiFilterTextPath.value = ''
dictFilterText.value = ''
}
const handleExport = async () => {
@ -650,15 +739,18 @@ const handleExport = async () => {
exportLoading.value = true
try {
// API
// API
const checkedMenus = menuTreeRef.value ? menuTreeRef.value.getCheckedNodes(false, true) : []
const checkedApis = apiTreeRef.value ? apiTreeRef.value.getCheckedNodes(true) : []
const checkedDicts = dictTreeRef.value ? dictTreeRef.value.getCheckedNodes(true) : []
const menuIds = checkedMenus.map(menu => menu.ID)
const apiIds = checkedApis.map(api => api.ID)
const dictIds = checkedDicts.map(dict => dict.ID)
exportForm.value.menuIds = menuIds
exportForm.value.apiIds = apiIds
exportForm.value.dictIds = dictIds
const res = await exportVersion(exportForm.value)
if (res.code !== 0) {
@ -748,29 +840,14 @@ const getTotalMenuCount = () => {
return countMenus(importPreviewData.value.menus)
}
//
const buildTreeData = (data, parentId = 0) => {
const tree = []
// parentId"0"0
const targetParentId = parentId === 0 ? [0, "0"] : [parentId]
const items = data.filter(item => targetParentId.includes(item.parentId))
items.forEach(item => {
const children = buildTreeData(data, item.ID)
if (children.length > 0) {
item.children = children
}
tree.push(item)
})
return tree
}
const handleJsonContentChange = () => {
if (!importJsonContent.value.trim()) {
importPreviewData.value = null
previewMenuTreeData.value = []
previewApiTreeData.value = []
previewDictTreeData.value = []
return
}
@ -780,7 +857,8 @@ const handleJsonContentChange = () => {
//
importPreviewData.value = {
menus: data.menus || [],
apis: data.apis || []
apis: data.apis || [],
dictionaries: data.dictionaries || []
}
// 使children
@ -810,11 +888,19 @@ const handleJsonContentChange = () => {
} else {
previewApiTreeData.value = []
}
//
if (data.dictionaries && data.dictionaries.length > 0) {
previewDictTreeData.value = data.dictionaries
} else {
previewDictTreeData.value = []
}
} catch (error) {
console.error('JSON解析失败:', error)
importPreviewData.value = null
previewMenuTreeData.value = []
previewApiTreeData.value = []
previewDictTreeData.value = []
}
}

View File

@ -1,23 +0,0 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
important: true,
theme: {
extend: {
backgroundColor: {
main: '#F5F5F5'
},
textColor: {
active: 'var(--el-color-primary)'
},
boxShadowColor: {
active: 'var(--el-color-primary)'
},
borderColor: {
'table-border': 'var(--el-border-color-lighter)'
}
}
},
darkMode: 'class',
plugins: []
}

26
web/uno.config.js Normal file
View File

@ -0,0 +1,26 @@
import { defineConfig } from '@unocss/vite';
import presetWind3 from '@unocss/preset-wind3';
import transformerDirectives from '@unocss/transformer-directives'
export default defineConfig({
theme: {
backgroundColor: {
main: '#F5F5F5'
},
textColor: {
active: 'var(--el-color-primary)'
},
boxShadowColor: {
active: 'var(--el-color-primary)'
},
borderColor: {
'table-border': 'var(--el-border-color-lighter)'
}
},
presets: [
presetWind3({ dark: 'class' })
],
transformers: [
transformerDirectives(),
],
})

View File

@ -9,6 +9,8 @@ import vueDevTools from 'vite-plugin-vue-devtools'
import VueFilePathPlugin from './vitePlugin/componentName/index.js'
import { svgBuilder } from 'vite-auto-import-svg'
import { AddSecret } from './vitePlugin/secret'
import UnoCSS from '@unocss/vite'
// @see https://cn.vitejs.dev/config/
export default ({ mode }) => {
AddSecret('')
@ -111,7 +113,8 @@ export default ({ mode }) => {
vuePlugin(),
svgBuilder(['./src/plugin/','./src/assets/icons/'],base, outDir,'assets', NODE_ENV),
[Banner(`\n Build based on gin-vue-admin \n Time : ${timestamp}`)],
VueFilePathPlugin('./src/pathInfo.json')
VueFilePathPlugin('./src/pathInfo.json'),
UnoCSS()
]
}
return config