Compare commits

...
58 Commits
Author SHA1 Message Date
yvan 6a3649107c 1 2025-09-11 16:31:31 +08:00
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 27c97aa5c1 Merge remote-tracking branch 'origin/main' 2025-09-10 17:25:00 +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
yvan a416353c29 1 2025-09-09 16:05:39 +08:00
yvan c88252272c 1 2025-09-09 15:30:14 +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
yvan f35125cbce wechat 2025-09-05 23:37:57 +08:00
yvan a8b67d750b wechat 2025-09-05 23:37:31 +08:00
yvan e26a9e7735 wechat 2025-09-05 23:35:47 +08:00
yvan 16fb22479f wechat 2025-09-05 22:43:51 +08:00
yvan 89cf5f0d57 wechat 2025-09-05 22:43:48 +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
yvan 4e6fe9a456 wechat 2025-08-19 21:57:19 +08:00
yvan db64e158ce wechat 2025-08-19 21:14:46 +08:00
PiexlMax(奇淼 1d700ad5b6 Update README.md 2025-08-19 18:26:17 +08:00
yvan 5b43f99d9a wechat 2025-08-15 18:13:13 +08:00
yvan 8f44b85ec3 wechat 2025-08-15 17:56:37 +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
159 changed files with 3779 additions and 2074 deletions
+2 -1
View File
@@ -22,7 +22,8 @@
<table> <table>
<tr> <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>📄 创建基础模板</p>
<p>🤖 AI生成结构</p> <p>🤖 AI生成结构</p>
<p>⏰ 生成代码</p> <p>⏰ 生成代码</p>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

+5 -1
View File
@@ -1,6 +1,9 @@
package pet package pet
import "github.com/flipped-aurora/gin-vue-admin/server/service" import (
"github.com/flipped-aurora/gin-vue-admin/server/api/v1/pet/user"
"github.com/flipped-aurora/gin-vue-admin/server/service"
)
type ApiGroup struct { type ApiGroup struct {
PetAdoptionApplicationsApi PetAdoptionApplicationsApi
@@ -13,6 +16,7 @@ type ApiGroup struct {
PetFamilyPetsApi PetFamilyPetsApi
PetPetsApi PetPetsApi
PetRecordsApi PetRecordsApi
PetUserApiGroup user.ApiGroup
} }
var ( var (
@@ -167,22 +167,3 @@ func (petAdoptionApplicationsApi *PetAdoptionApplicationsApi) GetPetAdoptionAppl
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetAdoptionApplicationsPublic 不需要鉴权的petAdoptionApplications表接口
// @Tags PetAdoptionApplications
// @Summary 不需要鉴权的petAdoptionApplications表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petAdoptionApplications/getPetAdoptionApplicationsPublic [get]
func (petAdoptionApplicationsApi *PetAdoptionApplicationsApi) GetPetAdoptionApplicationsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petAdoptionApplicationsService.GetPetAdoptionApplicationsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petAdoptionApplications表接口信息",
}, "获取成功", c)
}
-19
View File
@@ -167,22 +167,3 @@ func (petAdoptionPostsApi *PetAdoptionPostsApi) GetPetAdoptionPostsList(c *gin.C
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetAdoptionPostsPublic 不需要鉴权的petAdoptionPosts表接口
// @Tags PetAdoptionPosts
// @Summary 不需要鉴权的petAdoptionPosts表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petAdoptionPosts/getPetAdoptionPostsPublic [get]
func (petAdoptionPostsApi *PetAdoptionPostsApi) GetPetAdoptionPostsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petAdoptionPostsService.GetPetAdoptionPostsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petAdoptionPosts表接口信息",
}, "获取成功", c)
}
@@ -167,22 +167,3 @@ func (petAiAssistantConversationsApi *PetAiAssistantConversationsApi) GetPetAiAs
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetAiAssistantConversationsPublic 不需要鉴权的petAiAssistantConversations表接口
// @Tags PetAiAssistantConversations
// @Summary 不需要鉴权的petAiAssistantConversations表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petAiAssistantConversations/getPetAiAssistantConversationsPublic [get]
func (petAiAssistantConversationsApi *PetAiAssistantConversationsApi) GetPetAiAssistantConversationsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petAiAssistantConversationsService.GetPetAiAssistantConversationsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petAiAssistantConversations表接口信息",
}, "获取成功", c)
}
-19
View File
@@ -167,22 +167,3 @@ func (petAiConversationsApi *PetAiConversationsApi) GetPetAiConversationsList(c
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetAiConversationsPublic 不需要鉴权的petAiConversations表接口
// @Tags PetAiConversations
// @Summary 不需要鉴权的petAiConversations表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petAiConversations/getPetAiConversationsPublic [get]
func (petAiConversationsApi *PetAiConversationsApi) GetPetAiConversationsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petAiConversationsService.GetPetAiConversationsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petAiConversations表接口信息",
}, "获取成功", c)
}
-19
View File
@@ -167,22 +167,3 @@ func (petFamiliesApi *PetFamiliesApi) GetPetFamiliesList(c *gin.Context) {
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetFamiliesPublic 不需要鉴权的petFamilies表接口
// @Tags PetFamilies
// @Summary 不需要鉴权的petFamilies表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petFamilies/getPetFamiliesPublic [get]
func (petFamiliesApi *PetFamiliesApi) GetPetFamiliesPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petFamiliesService.GetPetFamiliesPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petFamilies表接口信息",
}, "获取成功", c)
}
@@ -167,22 +167,3 @@ func (petFamilyInvitationsApi *PetFamilyInvitationsApi) GetPetFamilyInvitationsL
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetFamilyInvitationsPublic 不需要鉴权的petFamilyInvitations表接口
// @Tags PetFamilyInvitations
// @Summary 不需要鉴权的petFamilyInvitations表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petFamilyInvitations/getPetFamilyInvitationsPublic [get]
func (petFamilyInvitationsApi *PetFamilyInvitationsApi) GetPetFamilyInvitationsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petFamilyInvitationsService.GetPetFamilyInvitationsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petFamilyInvitations表接口信息",
}, "获取成功", c)
}
-19
View File
@@ -167,22 +167,3 @@ func (petFamilyMembersApi *PetFamilyMembersApi) GetPetFamilyMembersList(c *gin.C
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetFamilyMembersPublic 不需要鉴权的petFamilyMembers表接口
// @Tags PetFamilyMembers
// @Summary 不需要鉴权的petFamilyMembers表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petFamilyMembers/getPetFamilyMembersPublic [get]
func (petFamilyMembersApi *PetFamilyMembersApi) GetPetFamilyMembersPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petFamilyMembersService.GetPetFamilyMembersPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petFamilyMembers表接口信息",
}, "获取成功", c)
}
-19
View File
@@ -167,22 +167,3 @@ func (petFamilyPetsApi *PetFamilyPetsApi) GetPetFamilyPetsList(c *gin.Context) {
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetFamilyPetsPublic 不需要鉴权的petFamilyPets表接口
// @Tags PetFamilyPets
// @Summary 不需要鉴权的petFamilyPets表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petFamilyPets/getPetFamilyPetsPublic [get]
func (petFamilyPetsApi *PetFamilyPetsApi) GetPetFamilyPetsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petFamilyPetsService.GetPetFamilyPetsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petFamilyPets表接口信息",
}, "获取成功", c)
}
-19
View File
@@ -167,22 +167,3 @@ func (petPetsApi *PetPetsApi) GetPetPetsList(c *gin.Context) {
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetPetsPublic 不需要鉴权的petPets表接口
// @Tags PetPets
// @Summary 不需要鉴权的petPets表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petPets/getPetPetsPublic [get]
func (petPetsApi *PetPetsApi) GetPetPetsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petPetsService.GetPetPetsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petPets表接口信息",
}, "获取成功", c)
}
-19
View File
@@ -167,22 +167,3 @@ func (petRecordsApi *PetRecordsApi) GetPetRecordsList(c *gin.Context) {
PageSize: pageInfo.PageSize, PageSize: pageInfo.PageSize,
}, "获取成功", c) }, "获取成功", c)
} }
// GetPetRecordsPublic 不需要鉴权的petRecords表接口
// @Tags PetRecords
// @Summary 不需要鉴权的petRecords表接口
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
// @Router /petRecords/getPetRecordsPublic [get]
func (petRecordsApi *PetRecordsApi) GetPetRecordsPublic(c *gin.Context) {
// 创建业务用Context
ctx := c.Request.Context()
// 此接口不需要鉴权
// 示例为返回了一个固定的消息接口,一般本接口用于C端服务,需要自己实现业务逻辑
petRecordsService.GetPetRecordsPublic(ctx)
response.OkWithDetailed(gin.H{
"info": "不需要鉴权的petRecords表接口信息",
}, "获取成功", c)
}
+20
View File
@@ -0,0 +1,20 @@
package user
import "github.com/flipped-aurora/gin-vue-admin/server/service"
type ApiGroup struct {
PetAssistantApi
}
var (
petAdoptionApplicationsService = service.ServiceGroupApp.PetServiceGroup.PetAdoptionApplicationsService
petAdoptionPostsService = service.ServiceGroupApp.PetServiceGroup.PetAdoptionPostsService
petAiAssistantConversationsService = service.ServiceGroupApp.PetServiceGroup.PetAiAssistantConversationsService
petAiConversationsService = service.ServiceGroupApp.PetServiceGroup.PetAiConversationsService
petFamiliesService = service.ServiceGroupApp.PetServiceGroup.PetFamiliesService
petFamilyInvitationsService = service.ServiceGroupApp.PetServiceGroup.PetFamilyInvitationsService
petFamilyMembersService = service.ServiceGroupApp.PetServiceGroup.PetFamilyMembersService
petFamilyPetsService = service.ServiceGroupApp.PetServiceGroup.PetFamilyPetsService
petPetsService = service.ServiceGroupApp.PetServiceGroup.PetPetsService
petRecordsService = service.ServiceGroupApp.PetServiceGroup.PetRecordsService
)
+244
View File
@@ -0,0 +1,244 @@
package user
import (
"io"
"strconv"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
petRequest "github.com/flipped-aurora/gin-vue-admin/server/model/pet/request"
petResponse "github.com/flipped-aurora/gin-vue-admin/server/model/pet/response"
"github.com/flipped-aurora/gin-vue-admin/server/service"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type PetAssistantApi struct{}
var petChatService = service.ServiceGroupApp.PetServiceGroup.PetChatService
// StreamAskPetAssistant 向宠物助手流式提问
// @Tags PetAssistant
// @Summary 向宠物助手流式提问接口
// @Security ApiKeyAuth
// @Accept application/json
// @Produce text/event-stream
// @Param data body petRequest.ChatRequest true "宠物助手流式提问请求"
// @Success 200 {string} string "流式响应"
// @Router /api/v1/pet/user/assistant/stream-ask [post]
func (p *PetAssistantApi) StreamAskPetAssistant(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
}
// 强制设置为流式响应
req.Stream = true
// 设置默认参数
if req.Temperature <= 0 {
req.Temperature = 0.7
}
if req.MaxTokens <= 0 {
req.MaxTokens = 1000
}
// 设置SSE响应头
ctx.Header("Content-Type", "text/event-stream")
ctx.Header("Cache-Control", "no-cache")
ctx.Header("Connection", "keep-alive")
ctx.Header("Access-Control-Allow-Origin", "*")
ctx.Header("Access-Control-Allow-Headers", "Cache-Control")
// 创建事件通道
eventChan := make(chan petResponse.StreamEvent, 100)
defer close(eventChan)
// 启动流式聊天
go func() {
if err := petChatService.StreamChat(businessCtx, userId, req, eventChan); err != nil {
global.GVA_LOG.Error("流式聊天失败", zap.Error(err), zap.Uint("userId", userId))
eventChan <- petResponse.StreamEvent{
Event: "error",
Data: map[string]interface{}{
"error": "流式聊天失败: " + err.Error(),
},
}
}
}()
// 发送流式数据
ctx.Stream(func(w io.Writer) bool {
select {
case event, ok := <-eventChan:
if !ok {
return false
}
switch event.Event {
case "message":
// 发送消息事件
ctx.SSEvent("message", event.Data)
case "error":
// 发送错误事件
ctx.SSEvent("error", event.Data)
return false
case "done":
// 发送完成事件
ctx.SSEvent("done", event.Data)
return false
}
return true
case <-time.After(30 * time.Second):
// 超时处理
ctx.SSEvent("error", map[string]interface{}{
"error": "流式响应超时",
})
return false
}
})
}
// GetAssistantHistory 获取宠物助手对话历史
// @Tags PetAssistant
// @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=[]petResponse.ChatHistoryItem,msg=string} "获取成功"
// @Router /api/v1/pet/user/assistant/history [get]
func (p *PetAssistantApi) GetAssistantHistory(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
}
// 获取查询参数
sessionId := ctx.Query("sessionId")
pageStr := ctx.DefaultQuery("page", "1")
pageSizeStr := ctx.DefaultQuery("pageSize", "20")
page, err := strconv.Atoi(pageStr)
if err != nil || page < 1 {
page = 1
}
pageSize, err := strconv.Atoi(pageSizeStr)
if err != nil || pageSize < 1 || pageSize > 100 {
pageSize = 20
}
// 计算limit
limit := pageSize
// 调用服务层
conversations, err := petChatService.GetChatHistory(businessCtx, userId, sessionId, limit)
if err != nil {
global.GVA_LOG.Error("获取聊天历史失败", zap.Error(err), zap.Uint("userId", userId))
response.FailWithMessage("获取聊天历史失败: "+err.Error(), ctx)
return
}
response.OkWithDetailed(conversations, "获取成功", ctx)
}
// ClearAssistantHistory 清空宠物助手对话历史
// @Tags PetAssistant
// @Summary 清空宠物助手对话历史记录
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Param sessionId query string false "会话ID,不传则清空所有会话"
// @Success 200 {object} response.Response{msg=string} "清空成功"
// @Router /api/v1/pet/user/assistant/clear-history [delete]
func (p *PetAssistantApi) ClearAssistantHistory(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
}
// 获取会话ID(可选)
sessionId := ctx.Query("sessionId")
// 调用服务层
err := petChatService.ClearChatHistory(businessCtx, userId, sessionId)
if err != nil {
global.GVA_LOG.Error("清空聊天历史失败", zap.Error(err), zap.Uint("userId", userId))
response.FailWithMessage("清空聊天历史失败: "+err.Error(), ctx)
return
}
if sessionId != "" {
response.OkWithMessage("指定会话历史清空成功", ctx)
} else {
response.OkWithMessage("所有聊天历史清空成功", ctx)
}
}
// GetAssistantSessions 获取宠物助手会话列表
// @Tags PetAssistant
// @Summary 获取用户的宠物助手会话列表
// @Security ApiKeyAuth
// @Accept application/json
// @Produce application/json
// @Success 200 {object} response.Response{data=[]map[string]interface{},msg=string} "获取成功"
// @Router /api/v1/pet/user/assistant/sessions [get]
func (p *PetAssistantApi) GetAssistantSessions(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
}
// 调用服务层
sessions, err := petChatService.GetChatSessions(businessCtx, userId)
if err != nil {
global.GVA_LOG.Error("获取会话列表失败", zap.Error(err), zap.Uint("userId", userId))
response.FailWithMessage("获取会话列表失败: "+err.Error(), ctx)
return
}
response.OkWithDetailed(sessions, "获取成功", ctx)
}
+23 -24
View File
@@ -27,8 +27,6 @@ import (
func (b *BaseApi) Login(c *gin.Context) { func (b *BaseApi) Login(c *gin.Context) {
var l systemReq.Login var l systemReq.Login
err := c.ShouldBindJSON(&l) err := c.ShouldBindJSON(&l)
key := c.ClientIP()
if err != nil { if err != nil {
response.FailWithMessage(err.Error(), c) response.FailWithMessage(err.Error(), c)
return return
@@ -39,6 +37,7 @@ func (b *BaseApi) Login(c *gin.Context) {
return return
} }
key := c.ClientIP()
// 判断验证码是否开启 // 判断验证码是否开启
openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数 openCaptcha := global.GVA_CONFIG.Captcha.OpenCaptcha // 是否开启防爆次数
openCaptchaTimeOut := global.GVA_CONFIG.Captcha.OpenCaptchaTimeOut // 缓存超时时间 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) var oc bool = openCaptcha == 0 || openCaptcha < interfaceToInt(v)
if oc && (l.Captcha == "" || l.CaptchaId == "" || !store.Verify(l.CaptchaId, l.Captcha, true)) {
if !oc || (l.CaptchaId != "" && l.Captcha != "" && store.Verify(l.CaptchaId, l.Captcha, true)) { // 验证码次数+1
u := &system.SysUser{Username: l.Username, Password: l.Password} global.BlackCache.Increment(key, 1)
user, err := userService.Login(u) response.FailWithMessage("验证码错误", c)
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)
return return
} }
// 验证码次数+1
global.BlackCache.Increment(key, 1) u := &system.SysUser{Username: l.Username, Password: l.Password}
response.FailWithMessage("验证码错误", c) 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 // TokenNext 登录以后签发jwt
+51 -2
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结构 // 处理菜单数据,构建递归的children结构
processedMenus := buildMenuTree(menuData) processedMenus := buildMenuTree(menuData)
@@ -282,6 +293,34 @@ func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
processedApis = append(processedApis, cleanApi) 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{ exportData := systemRes.ExportVersionResponse{
Version: systemReq.VersionInfo{ Version: systemReq.VersionInfo{
@@ -290,8 +329,9 @@ func (sysVersionApi *SysVersionApi) ExportVersion(c *gin.Context) {
Description: req.Description, Description: req.Description,
ExportTime: time.Now().Format("2006-01-02 15:04:05"), ExportTime: time.Now().Format("2006-01-02 15:04:05"),
}, },
Menus: processedMenus, Menus: processedMenus,
Apis: processedApis, Apis: processedApis,
Dictionaries: processedDicts,
} }
// 转换为JSON // 转换为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) jsonData, _ := json.Marshal(importData)
version := system.SysVersion{ version := system.SysVersion{
+218 -155
View File
@@ -2,107 +2,103 @@
# jwt configuration # jwt configuration
jwt: jwt:
signing-key: qmPlus signing-key: qmPlus
expires-time: 7d expires-time: 7d
buffer-time: 1d buffer-time: 1d
issuer: qmPlus issuer: qmPlus
# zap logger configuration # zap logger configuration
zap: zap:
level: info level: info
format: console format: console
prefix: "[github.com/flipped-aurora/gin-vue-admin/server]" prefix: "[github.com/flipped-aurora/gin-vue-admin/server]"
director: log director: log
show-line: true show-line: true
encode-level: LowercaseColorLevelEncoder encode-level: LowercaseColorLevelEncoder
stacktrace-key: stacktrace stacktrace-key: stacktrace
log-in-console: true log-in-console: true
retention-day: -1
# redis configuration # redis configuration
redis: redis:
db: 0 #是否使用redis集群模式
addr: 177.7.0.14:6379 useCluster: false
password: "" #使用集群模式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 configuration
mongo: mongo:
coll: '' coll: ''
options: '' options: ''
database: '' database: ''
username: '' username: ''
password: '' password: ''
min-pool-size: 0 auth-source: ''
max-pool-size: 100 min-pool-size: 0
socket-timeout-ms: 0 max-pool-size: 100
connect-timeout-ms: 0 socket-timeout-ms: 0
is-zap: false connect-timeout-ms: 0
hosts: is-zap: false
- host: '' hosts:
port: '' - host: ''
port: ''
# email configuration # email configuration
email: email:
to: xxx@qq.com to: xxx@qq.com
port: 465 port: 465
from: xxx@163.com from: xxx@163.com
host: smtp.163.com host: smtp.163.com
is-ssl: true is-ssl: true
secret: xxx secret: xxx
nickname: test nickname: test
# system configuration # system configuration
system: system:
env: public # Change to "develop" to skip authentication for development mode env: local # 修改为public可以关闭路由日志输出
addr: 8888 addr: 8888
db-type: mysql db-type: mysql
oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置 oss-type: local # 控制oss选择走本地还是 七牛等其他仓 自行增加其他oss仓可以在 server/utils/upload/upload.go 中 NewOss函数配置
use-redis: false # 使用redis use-redis: false # 使用redis
use-mongo: false # 使用mongo use-mongo: false # 使用mongo
use-multipoint: false use-multipoint: false
# IP限制次数 一个小时15000次 # IP限制次数 一个小时15000次
iplimit-count: 15000 iplimit-count: 15000
# IP限制一个小时 # IP限制一个小时
iplimit-time: 3600 iplimit-time: 3600
# 路由全局前缀
router-prefix: ""
# 严格角色模式 打开后权限将会存在上下级关系
use-strict-auth: false
# captcha configuration # captcha configuration
captcha: captcha:
key-long: 6 key-long: 6
img-width: 240 img-width: 240
img-height: 80 img-height: 80
open-captcha: 0 # 0代表一直开启,大于0代表限制次数 open-captcha: 0 # 0代表一直开启,大于0代表限制次数
open-captcha-timeout: 3600 # open-captcha大于0时才生效 open-captcha-timeout: 3600 # open-captcha大于0时才生效
# mysql connect configuration # mysql connect configuration
# 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master) # 未初始化之前请勿手动修改数据库信息!!!如果一定要手动初始化请看(https://gin-vue-admin.com/docs/first_master)
mysql: 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: "" path: ""
port: "" port: ""
config: "" config: ""
@@ -114,107 +110,174 @@ db-list:
log-mode: "" log-mode: ""
log-zap: false 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 configuration
local: local:
path: uploads/file path: uploads/file
store-path: uploads/file store-path: uploads/file
# autocode configuration # autocode configuration
autocode: autocode:
transfer-restart: true web: web/src
# root 自动适配项目根目录 root: "" # root 自动适配项目根目录, 请不要手动配置,他会在项目加载的时候识别出根路径
# 请不要手动配置,他会在项目加载的时候识别出根路径 server: server
root: "" module: 'github.com/flipped-aurora/gin-vue-admin/server'
server: /server ai-path: "" # AI服务路径
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
# qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址) # qiniu configuration (请自行七牛申请对应的 公钥 私钥 bucket 和 域名地址)
qiniu: qiniu:
zone: ZoneHuaDong zone: ZoneHuaDong
bucket: "" bucket: ""
img-path: "" img-path: ""
use-https: false use-https: false
access-key: "" access-key: ""
secret-key: "" secret-key: ""
use-cdn-domains: false 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 configuration
aliyun-oss: aliyun-oss:
endpoint: yourEndpoint endpoint: yourEndpoint
access-key-id: yourAccessKeyId access-key-id: yourAccessKeyId
access-key-secret: yourAccessKeySecret access-key-secret: yourAccessKeySecret
bucket-name: yourBucketName bucket-name: yourBucketName
bucket-url: yourBucketUrl bucket-url: yourBucketUrl
base-path: yourBasePath base-path: yourBasePath
# tencent cos configuration # tencent cos configuration
tencent-cos: tencent-cos:
bucket: xxxxx-10005608 bucket: xxxxx-10005608
region: ap-shanghai region: ap-shanghai
secret-id: your-secret-id secret-id: your-secret-id
secret-key: your-secret-key secret-key: your-secret-key
base-url: https://gin.vue.admin base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server path-prefix: github.com/flipped-aurora/gin-vue-admin/server
# aws s3 configuration (minio compatible) # aws s3 configuration (minio compatible)
aws-s3: aws-s3:
bucket: xxxxx-10005608 bucket: xxxxx-10005608
region: ap-shanghai region: ap-shanghai
endpoint: "" endpoint: ""
s3-force-path-style: false s3-force-path-style: false
disable-ssl: false disable-ssl: false
secret-id: your-secret-id secret-id: your-secret-id
secret-key: your-secret-key secret-key: your-secret-key
base-url: https://gin.vue.admin base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server 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 # huawei obs configuration
hua-wei-obs: hua-wei-obs:
path: you-path path: you-path
bucket: you-bucket bucket: you-bucket
endpoint: you-endpoint endpoint: you-endpoint
access-key: you-access-key access-key: you-access-key
secret-key: you-secret-key secret-key: you-secret-key
# excel configuration # excel configuration
excel: excel:
dir: ./resource/excel/ dir: ./resource/excel/
# timer task db clear table # disk usage configuration
Timer: disk-list:
start: true - mount-point: "/"
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
# 跨域配置 # 跨域配置
# 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用 # 需要配合 server/initialize/router.go -> `Router.Use(middleware.CorsByRules())` 使用
cors: cors:
mode: whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝 mode: strict-whitelist # 放行模式: allow-all, 放行全部; whitelist, 白名单模式, 来自白名单内域名的请求添加 cors 头; strict-whitelist 严格白名单模式, 白名单外的请求一律拒绝
whitelist: whitelist:
- allow-origin: example1.com - allow-origin: example1.com
allow-headers: content-type allow-headers: Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id
allow-methods: GET, POST allow-methods: POST, GET
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type
allow-credentials: true # 布尔值
- allow-origin: example2.com allow-credentials: true # 布尔值
allow-headers: content-type - allow-origin: example2.com
allow-methods: GET, POST allow-headers: content-type
expose-headers: Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type allow-methods: GET, POST
allow-credentials: true # 布尔值 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: ''
+2 -5
View File
@@ -237,11 +237,8 @@ tencent-cos:
base-url: https://gin.vue.admin base-url: https://gin.vue.admin
path-prefix: github.com/flipped-aurora/gin-vue-admin/server path-prefix: github.com/flipped-aurora/gin-vue-admin/server
volcengine: volcengine:
access-key: your-access-key api-key: 7562d83f-fc5c-4229-9aed-e8d9242c8683
secret-key: your-secret-key model: ep-20250909151934-m777l
region: cn-beijing
endpoint: https://ark.cn-beijing.volces.com
default-model: ep-xxx
wechat: wechat:
mini-app-id: wx0f5dc17ba3f9fe31 mini-app-id: wx0f5dc17ba3f9fe31
mini-app-secret: 5e700810a6f56717e28af76dbb63983d mini-app-secret: 5e700810a6f56717e28af76dbb63983d
+9 -1
View File
@@ -1,10 +1,18 @@
package config package config
import (
"fmt"
"net"
"net/url"
)
type Oracle struct { type Oracle struct {
GeneralDB `yaml:",inline" mapstructure:",squash"` GeneralDB `yaml:",inline" mapstructure:",squash"`
} }
func (m *Oracle) Dsn() string { 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
} }
+2 -5
View File
@@ -1,9 +1,6 @@
package config package config
type Volcengine struct { type Volcengine struct {
AccessKey string `mapstructure:"access-key" json:"access-key" yaml:"access-key"` // 火山引擎访问密钥ID ApiKey string `mapstructure:"api-key" json:"api-key" yaml:"api-key"` // 火山引擎API密钥
SecretKey string `mapstructure:"secret-key" json:"secret-key" yaml:"secret-key"` // 火山引擎访问密钥Secret Model string `mapstructure:"model" json:"model" yaml:"model"` // 模型ID,如:ep-xxx
Region string `mapstructure:"region" json:"region" yaml:"region"` // 区域,如:cn-beijing
Endpoint string `mapstructure:"endpoint" json:"endpoint" yaml:"endpoint"` // 服务端点,如:https://ark.cn-beijing.volces.com
DefaultModel string `mapstructure:"default-model" json:"default-model" yaml:"default-model"` // 默认模型ID,如:ep-xxx
} }
+3 -2
View File
@@ -35,7 +35,7 @@ func RunServer() {
fmt.Printf(` fmt.Printf(`
欢迎使用 gin-vue-admin 欢迎使用 gin-vue-admin
当前版本:v2.8.4 当前版本:%s
加群方式:微信号:shouzi_1994 QQ群:470239250 加群方式:微信号:shouzi_1994 QQ群:470239250
项目地址:https://github.com/flipped-aurora/gin-vue-admin 项目地址:https://github.com/flipped-aurora/gin-vue-admin
插件市场:https://plugin.gin-vue-admin.com 插件市场:https://plugin.gin-vue-admin.com
@@ -48,6 +48,7 @@ func RunServer() {
** 版权所有方:flipped-aurora开源团队 ** ** 版权所有方:flipped-aurora开源团队 **
** 版权持有公司:北京翻转极光科技有限责任公司 ** ** 版权持有公司:北京翻转极光科技有限责任公司 **
** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html ** ** 剔除授权标识需购买商用授权: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) initServer(address, Router, 10*time.Minute, 10*time.Minute)
} }
+5 -2
View File
@@ -1,7 +1,10 @@
// Package docs Code generated by swaggo/swag. DO NOT EDIT // Package docs Code generated by swaggo/swag. DO NOT EDIT
package docs package docs
import "github.com/swaggo/swag" import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/swaggo/swag"
)
const docTemplate = `{ const docTemplate = `{
"schemes": {{ marshal .Schemes }}, "schemes": {{ marshal .Schemes }},
@@ -10411,7 +10414,7 @@ const docTemplate = `{
// SwaggerInfo holds exported Swagger Info so clients can modify it // SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{ var SwaggerInfo = &swag.Spec{
Version: "v2.8.4", Version: global.Version,
Host: "", Host: "",
BasePath: "", BasePath: "",
Schemes: []string{}, Schemes: []string{},
+12
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进行极速开发的全栈开发基础平台"
)
+8 -5
View File
@@ -1,14 +1,13 @@
module github.com/flipped-aurora/gin-vue-admin/server module github.com/flipped-aurora/gin-vue-admin/server
go 1.23 go 1.25.0
toolchain go1.23.9
require ( require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/aws/aws-sdk-go v1.55.6 github.com/aws/aws-sdk-go v1.55.6
github.com/casbin/casbin/v2 v2.103.0 github.com/casbin/casbin/v2 v2.103.0
github.com/casbin/gorm-adapter/v3 v3.32.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/fsnotify/fsnotify v1.8.0
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
@@ -19,6 +18,7 @@ require (
github.com/gookit/color v1.5.4 github.com/gookit/color v1.5.4
github.com/huaweicloud/huaweicloud-sdk-go-obs v3.24.9+incompatible 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/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/mark3labs/mcp-go v0.31.0
github.com/mholt/archives v0.1.1 github.com/mholt/archives v0.1.1
github.com/minio/minio-go/v7 v7.0.84 github.com/minio/minio-go/v7 v7.0.84
@@ -33,13 +33,13 @@ require (
github.com/silenceper/wechat/v2 v2.1.9 github.com/silenceper/wechat/v2 v2.1.9
github.com/songzhibin97/gkit v1.2.13 github.com/songzhibin97/gkit v1.2.13
github.com/spf13/viper v1.19.0 github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
github.com/swaggo/files v1.0.1 github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0 github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
github.com/tencentyun/cos-go-sdk-v5 v0.7.60 github.com/tencentyun/cos-go-sdk-v5 v0.7.60
github.com/unrolled/secure v1.17.0 github.com/unrolled/secure v1.17.0
github.com/volcengine/volcengine-go-sdk v1.1.25 github.com/volcengine/volcengine-go-sdk v1.1.31
github.com/xuri/excelize/v2 v2.9.0 github.com/xuri/excelize/v2 v2.9.0
go.mongodb.org/mongo-driver v1.17.2 go.mongodb.org/mongo-driver v1.17.2
go.uber.org/automaxprocs v1.6.0 go.uber.org/automaxprocs v1.6.0
@@ -77,6 +77,7 @@ require (
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
github.com/dustin/go-humanize v1.0.1 // 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/fatih/structs v1.1.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/gammazero/toposort v0.1.1 // indirect github.com/gammazero/toposort v0.1.1 // indirect
@@ -142,6 +143,7 @@ require (
github.com/sagikazarmark/locafero v0.7.0 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // 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/sirupsen/logrus v1.9.0 // indirect
github.com/sorairolake/lzip-go v0.3.5 // indirect github.com/sorairolake/lzip-go v0.3.5 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
@@ -150,6 +152,7 @@ require (
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/therootcompany/xz v1.0.1 // 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/gjson v1.14.1 // indirect
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
+16 -3
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/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 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 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/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/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= 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/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 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@@ -305,6 +310,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
github.com/kirklin/go-swd v0.0.3 h1:NvtV4jV9mm/1gobGYp7UE6bgBhCdJfBckO+ePzFffbo=
github.com/kirklin/go-swd v0.0.3/go.mod h1:bnU1Fz3Uil9T1mRyiwjyeV39567+iDFaxj7XRQUmK2s=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
@@ -455,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/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 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 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 h1:wc092gUkGbbBRTdzPxROhQhOH5iE98stnfzKA73mnTo=
github.com/silenceper/wechat/v2 v2.1.9/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s= github.com/silenceper/wechat/v2 v2.1.9/go.mod h1:7Iu3EhQYVtDUJAj+ZVRy8yom75ga7aDWv8RurLkVm0s=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
@@ -489,8 +498,9 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
@@ -505,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/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 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= 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 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
@@ -526,8 +538,8 @@ 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/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 h1:anOslb2Qp6ywnsbyq9jqR0ljuO63kg9PY+4OehIk5R8=
github.com/volcengine/volc-sdk-golang v1.0.23/go.mod h1:AfG/PZRUkHJ9inETvbjNifTDgut25Wbkm2QoYBTbvyU= 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.31 h1:qrNd/fu+ZWzH93EBSPBSntGwgwo7cHITxxv1IdvLxls=
github.com/volcengine/volcengine-go-sdk v1.1.25/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo= github.com/volcengine/volcengine-go-sdk v1.1.31/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
@@ -889,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/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 h1:sFf1j7vNStimPRRAtH4zz5NiHM+1dr6eA9aaRdplyhY=
gorm.io/gen v0.3.26/go.mod h1:a5lq5y3w4g5LMxBcw0wnO6tYUCdNutWODq5LrIt75LE= 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.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.0/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.7-0.20240204074919-46816ad31dde/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
+6 -2
View File
@@ -28,7 +28,9 @@ func GormMssql() *gorm.DB {
DSN: m.Dsn(), // DSN data source name DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度 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 return nil
} else { } else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) 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 DSN: m.Dsn(), // DSN data source name
DefaultStringSize: 191, // string 类型字段的默认长度 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) panic(err)
} else { } else {
db.InstanceSet("gorm:table_options", "ENGINE=InnoDB") db.InstanceSet("gorm:table_options", "ENGINE=InnoDB")
+3 -2
View File
@@ -34,8 +34,9 @@ func initMysqlDatabase(m config.Mysql) *gorm.DB {
DefaultStringSize: 191, // string 类型字段的默认长度 DefaultStringSize: 191, // string 类型字段的默认长度
SkipInitializeWithVersion: false, // 根据版本自动配置 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) panic(err)
} else { } else {
db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine) db.InstanceSet("gorm:table_options", "ENGINE="+m.Engine)
+4 -12
View File
@@ -1,18 +1,14 @@
package initialize package initialize
import ( 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/config"
"github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/initialize/internal" "github.com/flipped-aurora/gin-vue-admin/server/initialize/internal"
//_ "github.com/godror/godror"
"gorm.io/driver/mysql"
"gorm.io/gorm" "gorm.io/gorm"
) )
// GormOracle 初始化oracle数据库 // GormOracle 初始化oracle数据库
// 如果需要Oracle库 放开import里的注释 把下方 mysql.Config 改为 oracle.Config ; mysql.New 改为 oracle.New
func GormOracle() *gorm.DB { func GormOracle() *gorm.DB {
m := global.GVA_CONFIG.Oracle m := global.GVA_CONFIG.Oracle
return initOracleDatabase(m) return initOracleDatabase(m)
@@ -28,13 +24,9 @@ func initOracleDatabase(m config.Oracle) *gorm.DB {
if m.Dbname == "" { if m.Dbname == "" {
return nil return nil
} }
// 数据库配置
oracleConfig := mysql.Config{ general := m.GeneralDB
DSN: m.Dsn(), // DSN data source name if db, err := gorm.Open(oracle.Open(m.Dsn()), internal.Gorm.Config(general)); err != nil {
DefaultStringSize: 191, // string 类型字段的默认长度
}
if db, err := gorm.Open(mysql.New(oracleConfig), internal.Gorm.Config(m.Prefix, m.Singular)); err != nil {
panic(err) panic(err)
} else { } else {
sqlDB, _ := db.DB() sqlDB, _ := db.DB()
+3 -1
View File
@@ -30,7 +30,9 @@ func initPgSqlDatabase(p config.Pgsql) *gorm.DB {
DSN: p.Dsn(), // DSN data source name DSN: p.Dsn(), // DSN data source name
PreferSimpleProtocol: false, 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) panic(err)
} else { } else {
sqlDB, _ := db.DB() sqlDB, _ := db.DB()
+3 -1
View File
@@ -25,7 +25,9 @@ func initSqliteDatabase(s config.Sqlite) *gorm.DB {
return nil 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) panic(err)
} else { } else {
sqlDB, _ := db.DB() sqlDB, _ := db.DB()
+5 -20
View File
@@ -1,12 +1,12 @@
package internal package internal
import ( import (
"time"
"github.com/flipped-aurora/gin-vue-admin/server/config" "github.com/flipped-aurora/gin-vue-admin/server/config"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/logger" "gorm.io/gorm/logger"
"gorm.io/gorm/schema" "gorm.io/gorm/schema"
"time"
) )
var Gorm = new(_gorm) var Gorm = new(_gorm)
@@ -15,22 +15,7 @@ type _gorm struct{}
// Config gorm 自定义配置 // Config gorm 自定义配置
// Author [SliverHorn](https://github.com/SliverHorn) // Author [SliverHorn](https://github.com/SliverHorn)
func (g *_gorm) Config(prefix string, singular bool) *gorm.Config { func (g *_gorm) Config(general config.GeneralDB) *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
}
return &gorm.Config{ return &gorm.Config{
Logger: logger.New(NewWriter(general), logger.Config{ Logger: logger.New(NewWriter(general), logger.Config{
SlowThreshold: 200 * time.Millisecond, SlowThreshold: 200 * time.Millisecond,
@@ -38,8 +23,8 @@ func (g *_gorm) Config(prefix string, singular bool) *gorm.Config {
Colorful: true, Colorful: true,
}), }),
NamingStrategy: schema.NamingStrategy{ NamingStrategy: schema.NamingStrategy{
TablePrefix: prefix, TablePrefix: general.Prefix,
SingularTable: singular, SingularTable: general.Singular,
}, },
DisableForeignKeyConstraintWhenMigrating: true, DisableForeignKeyConstraintWhenMigrating: true,
} }
-2
View File
@@ -5,7 +5,6 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/email" "github.com/flipped-aurora/gin-vue-admin/server/plugin/email"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine"
"github.com/flipped-aurora/gin-vue-admin/server/utils/plugin" "github.com/flipped-aurora/gin-vue-admin/server/utils/plugin"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -34,7 +33,6 @@ func bizPluginV1(group ...*gin.RouterGroup) {
global.GVA_CONFIG.Email.IsSSL, global.GVA_CONFIG.Email.IsSSL,
global.GVA_CONFIG.Email.IsLoginAuth, global.GVA_CONFIG.Email.IsLoginAuth,
), ),
volcengine.CreateVolcenginePlug(),
) )
holder(public, private) holder(public, private)
} }
+3 -1
View File
@@ -73,8 +73,10 @@ func Routers() *gin.Engine {
PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) PublicGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix) PrivateGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
UserGroup := Router.Group(global.GVA_CONFIG.System.RouterPrefix)
PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
UserGroup.Use(middleware.UserJWTAuth())
{ {
// 健康监测 // 健康监测
@@ -114,7 +116,7 @@ func Routers() *gin.Engine {
InstallPlugin(PrivateGroup, PublicGroup, Router) InstallPlugin(PrivateGroup, PublicGroup, Router)
// 注册业务路由 // 注册业务路由
initBizRouter(PrivateGroup, PublicGroup) initBizRouter(PrivateGroup, PublicGroup, UserGroup)
global.GVA_ROUTERS = Router.Routes() global.GVA_ROUTERS = Router.Routes()
+6
View File
@@ -12,6 +12,7 @@ func holder(routers ...*gin.RouterGroup) {
func initBizRouter(routers ...*gin.RouterGroup) { func initBizRouter(routers ...*gin.RouterGroup) {
privateGroup := routers[0] privateGroup := routers[0]
publicGroup := routers[1] publicGroup := routers[1]
userGroup := routers[2]
holder(publicGroup, privateGroup) holder(publicGroup, privateGroup)
{ {
petRouter := router.RouterGroupApp.Pet petRouter := router.RouterGroupApp.Pet
@@ -25,5 +26,10 @@ func initBizRouter(routers ...*gin.RouterGroup) {
petRouter.InitPetFamilyPetsRouter(privateGroup, publicGroup) petRouter.InitPetFamilyPetsRouter(privateGroup, publicGroup)
petRouter.InitPetPetsRouter(privateGroup, publicGroup) // 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。 petRouter.InitPetPetsRouter(privateGroup, publicGroup) // 占位方法,保证文件可以正确加载,避免go空变量检测报错,请勿删除。
petRouter.InitPetRecordsRouter(privateGroup, publicGroup) petRouter.InitPetRecordsRouter(privateGroup, publicGroup)
// 用户相关路由(需要UserJWTAuth认证)
if userGroup != nil {
petRouter.InitPetAssistantRouter(userGroup, publicGroup)
}
} }
} }
+1 -1
View File
@@ -21,7 +21,7 @@ import (
// @Tag.Description 用户 // @Tag.Description 用户
// @title Gin-Vue-Admin Swagger API接口文档 // @title Gin-Vue-Admin Swagger API接口文档
// @version v2.8.4 // @version v2.8.5
// @description 使用gin+vue进行极速开发的全栈开发基础平台 // @description 使用gin+vue进行极速开发的全栈开发基础平台
// @securityDefinitions.apikey ApiKeyAuth // @securityDefinitions.apikey ApiKeyAuth
// @in header // @in header
+5 -1
View File
@@ -41,7 +41,11 @@ type ApiCreator struct{}
// New 创建API创建工具 // New 创建API创建工具
func (a *ApiCreator) New() mcp.Tool { func (a *ApiCreator) New() mcp.Tool {
return mcp.NewTool("create_api", 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编辑器自动添加API;3) router下的文件产生路径变化时`),
mcp.WithString("path", mcp.WithString("path",
mcp.Required(), mcp.Required(),
mcp.Description("API路径,如:/user/create"), mcp.Description("API路径,如:/user/create"),
+165
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
}
-81
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)
}
+20 -20
View File
@@ -7,13 +7,13 @@ ExecutionPlan 是用于自动化模块创建的执行计划结构体,包含了
```go ```go
type ExecutionPlan struct { type ExecutionPlan struct {
PackageName string `json:"packageName"` // 包名,如:"user", "order", "product" PackageName string `json:"packageName"` // 包名,如:"user", "order", "product"
PackageType string `json:"packageType"` // "plugin" 或 "package" PackageType string `json:"packageType"` // "plugin" 或 "package"
NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包 NeedCreatedPackage bool `json:"needCreatedPackage"` // 是否需要创建包
NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块 NeedCreatedModules bool `json:"needCreatedModules"` // 是否需要创建模块
PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息(当NeedCreatedPackage=true时必需) PackageInfo *request.SysAutoCodePackageCreate `json:"packageInfo,omitempty"` // 包信息(当NeedCreatedPackage=true时必需)
ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组(当NeedCreatedModules=true时必需,支持批量创建) ModulesInfo []*request.AutoCode `json:"modulesInfo,omitempty"` // 模块信息数组(当NeedCreatedModules=true时必需,支持批量创建)
Paths map[string]string `json:"paths,omitempty"` // 路径信息 Paths map[string]string `json:"paths,omitempty"` // 路径信息
} }
``` ```
@@ -501,19 +501,19 @@ type DataSource struct {
2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供 2. **NeedCreatedPackage**: 当为true时,PackageInfo必须提供
3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供 3. **NeedCreatedModules**: 当为true时,ModulesInfo必须提供
4. **字段类型**: FieldType支持的类型包括: 4. **字段类型**: FieldType支持的类型包括:
- string(字符串) - string(字符串)
- richtext(富文本) - richtext(富文本)
- int(整型) - int(整型)
- bool(布尔值) - bool(布尔值)
- float64(浮点型) - float64(浮点型)
- time.Time(时间) - time.Time(时间)
- enum(枚举) - enum(枚举)
- picture(单图片,字符串) - picture(单图片,字符串)
- pictures(多图片,json字符串) - pictures(多图片,json字符串)
- video(视频,字符串) - video(视频,字符串)
- file(文件,json字符串) - file(文件,json字符串)
- json(JSON) - json(JSON)
- array(数组) - array(数组)
5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等 5. **搜索类型**: FieldSearchType支持:EQ, NE, GT, GE, LT, LE, LIKE, BETWEEN等
6. **索引类型**: FieldIndexType支持:index, unique等 6. **索引类型**: FieldIndexType支持:index, unique等
7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段 7. **GvaModel**: 设置为true时会自动包含ID、CreatedAt、UpdatedAt、DeletedAt字段
-79
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
}
+61 -6
View File
@@ -9,6 +9,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"unicode"
"github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/global"
common "github.com/flipped-aurora/gin-vue-admin/server/model/common/request" 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(执行创建) 2. AI分析需求为1xxx2xxx格式 → gva_auto_generate(执行创建)
3. 创建完成后,根据需要使用其他辅助工具 3. 创建完成后,根据需要使用其他辅助工具
**重要限制:**
- 当needCreatedModules=true时,模块创建会自动生成API和菜单,因此不应再调用api_creator和menu_creator工具
- 只有在单独创建API或菜单(不涉及模块创建)时才使用api_creator和menu_creator工具
重要:ExecutionPlan结构体格式要求(支持批量创建): 重要:ExecutionPlan结构体格式要求(支持批量创建):
{ {
"packageName": "包名(string)", "packageName": "包名(string)",
@@ -169,10 +174,10 @@ func (t *AutomationModuleAnalyzer) New() mcp.Tool {
"generateWeb": "是否生成前端(bool)", "generateWeb": "是否生成前端(bool)",
"generateServer": "是否生成后端(bool)", "generateServer": "是否生成后端(bool)",
"fields": [{ "fields": [{
"fieldName": "字段名(string)", "fieldName": "字段名(string)必须大写开头",
"fieldDesc": "字段描述(string)", "fieldDesc": "字段描述(string)",
"fieldType": "字段类型:string/int/bool/time.Time等(string)", "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)",
"fieldJson": "JSON标签(string)", "fieldJson": "JSON标签(string 必须是小驼峰命名,例:userName)",
"dataTypeLong": "数据长度(string)", "dataTypeLong": "数据长度(string)",
"comment": "注释(string)", "comment": "注释(string)",
"columnName": "数据库列名(string)", "columnName": "数据库列名(string)",
@@ -184,7 +189,7 @@ func (t *AutomationModuleAnalyzer) New() mcp.Tool {
"desc": "详情显示(bool)", "desc": "详情显示(bool)",
"excel": "导入导出(bool)", "excel": "导入导出(bool)",
"require": "是否必填(bool)", "require": "是否必填(bool)",
"defaultValue": "默认值(string)", "defaultValue": "默认值(string),JSON类型(array,json,file,pictures)请保持为空他们不可以设置默认值",
"errorText": "错误提示(string)", "errorText": "错误提示(string)",
"clearable": "是否可清空(bool)", "clearable": "是否可清空(bool)",
"sort": "是否排序(bool)", "sort": "是否排序(bool)",
@@ -803,8 +808,8 @@ func (t *AutomationModuleAnalyzer) handleAnalyze(ctx context.Context, request mc
"fields": [{ "fields": [{
"fieldName": "字段名(必须大写开头)", "fieldName": "字段名(必须大写开头)",
"fieldDesc": "字段描述", "fieldDesc": "字段描述",
"fieldType": "GO 语言的数据类型", "fieldType": "字段类型支持:string(字符串),richtext(富文本),int(整型),bool(布尔值),float64(浮点型),time.Time(时间),enum(枚举),picture(单图片,字符串),pictures(多图片,json字符串),video(视频,字符串),file(文件,json字符串),json(JSON),array(数组)",
"fieldJson": "json标签", "fieldJson": "json标签(string 必须是小驼峰命名,例:userName)",
"dataTypeLong": "长度", "dataTypeLong": "长度",
"comment": "注释", "comment": "注释",
"columnName": "数据库列名", "columnName": "数据库列名",
@@ -1239,6 +1244,7 @@ func (t *AutomationModuleAnalyzer) validateExecutionPlan(plan *ExecutionPlan) er
if field.FieldName == "" { if field.FieldName == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1) return fmt.Errorf("模块 %d 字段 %d 的 fieldName 不能为空", moduleIndex+1, i+1)
} }
if field.FieldDesc == "" { if field.FieldDesc == "" {
return fmt.Errorf("模块 %d 字段 %d 的 fieldDesc 不能为空", moduleIndex+1, i+1) 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) 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"} validFieldTypes := []string{"string", "int", "int64", "float64", "bool", "time.Time", "enum", "picture", "video", "file", "pictures", "array", "richtext", "json"}
validType := false validType := false
@@ -1365,6 +1413,13 @@ func (t *AutomationModuleAnalyzer) executeCreation(ctx context.Context, plan *Ex
} }
result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo)) result.Message += fmt.Sprintf("批量创建完成,共处理 %d 个模块; ", len(plan.ModulesInfo))
// 添加重要提醒:不要使用其他MCP工具
result.Message += "\n\n⚠️ 重要提醒:\n"
result.Message += "模块创建已完成,API和菜单已自动生成。请不要再调用以下MCP工具:\n"
result.Message += "- api_creator:API权限已在模块创建时自动生成\n"
result.Message += "- menu_creator:前端菜单已在模块创建时自动生成\n"
result.Message += "如需修改API或菜单,请直接在系统管理界面中进行配置。\n"
} }
result.Message += "已构建目录结构信息; " result.Message += "已构建目录结构信息; "
+5 -1
View File
@@ -64,7 +64,11 @@ type MenuCreator struct{}
// New 创建菜单创建工具 // New 创建菜单创建工具
func (m *MenuCreator) New() mcp.Tool { func (m *MenuCreator) New() mcp.Tool {
return mcp.NewTool("create_menu", 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.WithNumber("parentId",
mcp.Description("父菜单ID,0表示根菜单"), mcp.Description("父菜单ID,0表示根菜单"),
mcp.DefaultNumber(0), mcp.DefaultNumber(0),
+112
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时正确跳转
**功能说明:**
- 返回完整的菜单树形结构
- 包含路由配置信息(path、name、component)
- 包含菜单元数据(title、icon、keepAlive等)
- 包含菜单参数和按钮配置
- 支持父子菜单关系展示
**使用场景:**
- 前端路由配置:获取所有菜单信息用于配置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
}
+2
View File
@@ -20,6 +20,8 @@ type RequirementAnalysisRequest struct {
UserRequirement string `json:"userRequirement"` UserRequirement string `json:"userRequirement"`
} }
// RequirementAnalysisResponse 需求分析响应 // RequirementAnalysisResponse 需求分析响应
type RequirementAnalysisResponse struct { type RequirementAnalysisResponse struct {
AIPrompt string `json:"aiPrompt"` // 给AI的提示词 AIPrompt string `json:"aiPrompt"` // 给AI的提示词
+89
View File
@@ -0,0 +1,89 @@
package middleware
import (
"errors"
"strconv"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/golang-jwt/jwt/v5"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/gin-gonic/gin"
)
func UserJWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 user-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := utils.GetUserToken(c)
if token == "" {
response.NoAuth("未登录或非法访问,请登录", c)
c.Abort()
return
}
if appUserIsBlacklist(token) {
response.NoAuth("您的帐户异地登陆或令牌失效", c)
utils.ClearUserToken(c)
c.Abort()
return
}
j := utils.NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseAppUserToken(token)
if err != nil {
if errors.Is(err, utils.TokenExpired) {
response.NoAuth("登录已过期,请重新登录", c)
utils.ClearUserToken(c)
c.Abort()
return
}
response.NoAuth(err.Error(), c)
utils.ClearUserToken(c)
c.Abort()
return
}
// 已登录用户被管理员禁用 需要使该用户的jwt失效 此处比较消耗性能 如果需要 请自行打开
// 用户被删除的逻辑 需要优化 此处比较消耗性能 如果需要 请自行打开
//if user, err := userService.FindUserByUuid(claims.UUID.String()); err != nil || user.Enable == 2 {
// _ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: token})
// response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
// c.Abort()
//}
c.Set("user_claims", claims)
if claims.ExpiresAt.Unix()-time.Now().Unix() < claims.BufferTime {
dr, _ := utils.ParseDuration(global.GVA_CONFIG.JWT.ExpiresTime)
claims.ExpiresAt = jwt.NewNumericDate(time.Now().Add(dr))
newToken, _ := j.CreateAppUserTokenByOldToken(token, *claims)
newClaims, _ := j.ParseAppUserToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt.Unix(), 10))
utils.SetUserToken(c, newToken, int(dr.Seconds()))
if global.GVA_CONFIG.System.UseMultipoint {
// 记录新的活跃jwt
_ = utils.SetRedisJWT(newToken, newClaims.AppBaseClaims.OpenID)
}
}
c.Next()
if newToken, exists := c.Get("new-token"); exists {
c.Header("new-token", newToken.(string))
}
if newExpiresAt, exists := c.Get("new-expires-at"); exists {
c.Header("new-expires-at", newExpiresAt.(string))
}
}
}
//@author: [piexlmax](https://github.com/piexlmax)
//@function: appUserIsBlacklist
//@description: 判断用户端JWT是否在黑名单内部
//@param: jwt string
//@return: bool
func appUserIsBlacklist(jwt string) bool {
_, ok := global.BlackCache.Get("user_blacklist:" + jwt)
return ok
}
@@ -11,6 +11,10 @@ type PetAiAssistantConversations struct {
UserId *int `json:"userId" form:"userId" gorm:"comment:用户ID;column:user_id;size:20;" binding:"required"` //用户ID UserId *int `json:"userId" form:"userId" gorm:"comment:用户ID;column:user_id;size:20;" binding:"required"` //用户ID
MessageContent *string `json:"messageContent" form:"messageContent" gorm:"comment:消息内容;column:message_content;" binding:"required"` //消息内容 MessageContent *string `json:"messageContent" form:"messageContent" gorm:"comment:消息内容;column:message_content;" binding:"required"` //消息内容
Role *string `json:"role" form:"role" gorm:"comment:发送者角色:user-用户,assistant-AI助手;column:role;size:20;" binding:"required"` //发送者角色:user-用户,assistant-AI助手 Role *string `json:"role" form:"role" gorm:"comment:发送者角色:user-用户,assistant-AI助手;column:role;size:20;" binding:"required"` //发送者角色:user-用户,assistant-AI助手
SessionId *string `json:"sessionId" form:"sessionId" gorm:"comment:会话ID;column:session_id;size:64;"` //会话ID
IsSensitive *bool `json:"isSensitive" form:"isSensitive" gorm:"comment:是否包含敏感词;column:is_sensitive;default:false;"` //是否包含敏感词
TokenCount *int `json:"tokenCount" form:"tokenCount" gorm:"comment:Token消耗数量;column:token_count;default:0;"` //Token消耗数量
ResponseTime *int `json:"responseTime" form:"responseTime" gorm:"comment:响应时间(ms);column:response_time;default:0;"` //响应时间(ms)
} }
// TableName petAiAssistantConversations表 PetAiAssistantConversations自定义表名 pet_ai_assistant_conversations // TableName petAiAssistantConversations表 PetAiAssistantConversations自定义表名 pet_ai_assistant_conversations
+60
View File
@@ -0,0 +1,60 @@
package request
import (
"github.com/flipped-aurora/gin-vue-admin/server/model/common/request"
)
// ChatRequest 宠物助手聊天请求结构体
type ChatRequest struct {
Message string `json:"message" binding:"required" validate:"required,min=1,max=2000"` // 用户消息内容,必填,1-2000字符
SessionId string `json:"sessionId" validate:"omitempty,uuid4"` // 会话ID,可选,UUID格式
Stream bool `json:"stream"` // 是否流式响应,默认false
Temperature float64 `json:"temperature" validate:"omitempty,min=0,max=2"` // 温度参数,控制随机性,范围0-2
MaxTokens int `json:"maxTokens" validate:"omitempty,min=1,max=4000"` // 最大生成token数,范围1-4000
Model string `json:"model" validate:"omitempty,min=1,max=100"` // 模型名称,可选
TopP float64 `json:"topP" validate:"omitempty,min=0,max=1"` // 核采样参数,范围0-1
}
// ChatHistoryRequest 获取聊天历史请求结构体
type ChatHistoryRequest struct {
SessionId string `json:"sessionId" form:"sessionId" validate:"omitempty,uuid4"` // 会话ID,可选,UUID格式
request.PageInfo // 分页信息
}
// ClearHistoryRequest 清空聊天历史请求结构体
type ClearHistoryRequest struct {
SessionId string `json:"sessionId" form:"sessionId" validate:"omitempty,uuid4"` // 会话ID,可选,不传则清空所有会话
}
// SessionsRequest 获取会话列表请求结构体
type SessionsRequest struct {
request.PageInfo // 分页信息
}
// StopGenerationRequest 停止生成请求结构体
type StopGenerationRequest struct {
RequestId string `json:"requestId" binding:"required" validate:"required,uuid4"` // 请求ID,必填,UUID格式
}
// RegenerateRequest 重新生成回复请求结构体
type RegenerateRequest struct {
SessionId string `json:"sessionId" binding:"required" validate:"required,uuid4"` // 会话ID,必填
MessageId uint `json:"messageId" binding:"required" validate:"required,min=1"` // 要重新生成的消息ID
Temperature float64 `json:"temperature" validate:"omitempty,min=0,max=2"` // 温度参数
MaxTokens int `json:"maxTokens" validate:"omitempty,min=1,max=4000"` // 最大token数
}
// FeedbackRequest 用户反馈请求结构体
type FeedbackRequest struct {
MessageId uint `json:"messageId" binding:"required" validate:"required,min=1"` // 消息ID,必填
FeedbackType string `json:"feedbackType" binding:"required" validate:"required,oneof=like dislike"` // 反馈类型:like/dislike
Comment string `json:"comment" validate:"omitempty,max=500"` // 反馈评论,可选,最多500字符
}
// ExportHistoryRequest 导出聊天历史请求结构体
type ExportHistoryRequest struct {
SessionId string `json:"sessionId" form:"sessionId" validate:"omitempty,uuid4"` // 会话ID,可选
Format string `json:"format" form:"format" validate:"omitempty,oneof=json txt markdown"` // 导出格式:json/txt/markdown
StartTime string `json:"startTime" form:"startTime" validate:"omitempty,datetime=2006-01-02"` // 开始时间,可选
EndTime string `json:"endTime" form:"endTime" validate:"omitempty,datetime=2006-01-02"` // 结束时间,可选
}
+126
View File
@@ -0,0 +1,126 @@
package response
import (
"time"
)
// ChatResponse 宠物助手聊天响应结构体
type ChatResponse struct {
ID uint `json:"id"` // 消息ID
Message string `json:"message"` // AI回复消息内容
SessionId string `json:"sessionId"` // 会话ID
IsSensitive bool `json:"isSensitive"` // 是否包含敏感词
TokenCount int `json:"tokenCount"` // Token消耗数量
ResponseTime int64 `json:"responseTime"` // 响应时间(毫秒)
RequestId string `json:"requestId,omitempty"` // 请求ID,用于流式响应管理
Model string `json:"model,omitempty"` // 使用的模型名称
CreatedAt time.Time `json:"createdAt"` // 创建时间
FinishReason string `json:"finishReason,omitempty"` // 完成原因:stop/length/content_filter
}
// StreamEvent 流式事件结构体
type StreamEvent struct {
Event string `json:"event"` // 事件类型: message, error, done, start
Data interface{} `json:"data"` // 事件数据
}
// StreamMessageData 流式消息数据结构体(简化版)
type StreamMessageData struct {
Delta string `json:"delta"` // 增量消息内容
}
// StreamErrorData 流式错误数据结构体(简化版)
type StreamErrorData struct {
Error string `json:"error"` // 错误信息
}
// StreamDoneData 流式完成数据结构体(简化版)
type StreamDoneData struct {
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 []ChatHistoryItem `json:"list"` // 对话记录列表
Total int64 `json:"total"` // 总记录数
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页大小
}
// SessionInfo 会话信息结构体
type SessionInfo struct {
SessionId string `json:"sessionId"` // 会话ID
LastUpdated time.Time `json:"lastUpdated"` // 最后更新时间
MessageCount int `json:"messageCount"` // 消息数量
FirstMessage string `json:"firstMessage"` // 第一条消息内容(用作会话标题)
CreatedAt time.Time `json:"createdAt"` // 创建时间
}
// SessionsResponse 会话列表响应结构体
type SessionsResponse struct {
List []SessionInfo `json:"list"` // 会话列表
Total int64 `json:"total"` // 总会话数
Page int `json:"page"` // 当前页码
PageSize int `json:"pageSize"` // 每页大小
}
// TokenUsage Token使用情况结构体
type TokenUsage struct {
PromptTokens int `json:"promptTokens"` // 输入token数
CompletionTokens int `json:"completionTokens"` // 输出token数
TotalTokens int `json:"totalTokens"` // 总token数
}
// ModelInfo 模型信息结构体
type ModelInfo struct {
Name string `json:"name"` // 模型名称
Description string `json:"description"` // 模型描述
MaxTokens int `json:"maxTokens"` // 最大token数
Available bool `json:"available"` // 是否可用
}
// ModelsResponse 可用模型列表响应结构体
type ModelsResponse struct {
Models []ModelInfo `json:"models"` // 模型列表
}
// StatsResponse 统计信息响应结构体
type StatsResponse struct {
TotalSessions int64 `json:"totalSessions"` // 总会话数
TotalMessages int64 `json:"totalMessages"` // 总消息数
TotalTokens int64 `json:"totalTokens"` // 总token消耗
LastChatTime *time.Time `json:"lastChatTime"` // 最后聊天时间
AverageResponse float64 `json:"averageResponse"` // 平均响应时间(毫秒)
SensitiveCount int64 `json:"sensitiveCount"` // 敏感词触发次数
PopularQuestions []string `json:"popularQuestions"` // 热门问题
}
// ExportResponse 导出响应结构体
type ExportResponse struct {
FileName string `json:"fileName"` // 文件名
FileSize int64 `json:"fileSize"` // 文件大小(字节)
DownloadUrl string `json:"downloadUrl"` // 下载链接
ExpiresAt int64 `json:"expiresAt"` // 过期时间戳
}
// HealthResponse 健康检查响应结构体
type HealthResponse struct {
Status string `json:"status"` // 服务状态:healthy/unhealthy
LLMService string `json:"llmService"` // LLM服务状态
SensitiveFilter string `json:"sensitiveFilter"` // 敏感词过滤状态
Database string `json:"database"` // 数据库状态
LastChecked time.Time `json:"lastChecked"` // 最后检查时间
ResponseTime int64 `json:"responseTime"` // 响应时间(毫秒)
ActiveSessions int `json:"activeSessions"` // 活跃会话数
QueuedRequests int `json:"queuedRequests"` // 排队请求数
}
+28
View File
@@ -0,0 +1,28 @@
package request
import (
jwt "github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// AppUserClaims 小程序用户专用Claims结构
type AppUserClaims struct {
AppBaseClaims
BufferTime int64
jwt.RegisteredClaims
}
// AppBaseClaims 小程序用户基础信息
type AppBaseClaims struct {
UUID uuid.UUID `json:"uuid"` // 用户UUID
ID uint `json:"id"` // 用户ID
OpenID string `json:"openId"` // 微信openid
UnionID string `json:"unionId"` // 微信unionid(可选)
NickName string `json:"nickName"` // 用户昵称
Avatar string `json:"avatar"` // 用户头像
Phone string `json:"phone"` // 手机号(可选)
Gender int `json:"gender"` // 性别:0-未知,1-男,2-女
City string `json:"city"` // 城市
Province string `json:"province"` // 省份
Country string `json:"country"` // 国家
}
+5 -3
View File
@@ -20,13 +20,15 @@ type ExportVersionRequest struct {
Description string `json:"description"` // 版本描述 Description string `json:"description"` // 版本描述
MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表 MenuIds []uint `json:"menuIds"` // 选中的菜单ID列表
ApiIds []uint `json:"apiIds"` // 选中的API ID列表 ApiIds []uint `json:"apiIds"` // 选中的API ID列表
DictIds []uint `json:"dictIds"` // 选中的字典ID列表
} }
// ImportVersionRequest 导入版本请求结构体 // ImportVersionRequest 导入版本请求结构体
type ImportVersionRequest struct { type ImportVersionRequest struct {
VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息 VersionInfo VersionInfo `json:"version" binding:"required"` // 版本信息
ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu ExportMenu []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi ExportApi []system.SysApi `json:"apis"` // API数据,直接复用SysApi
ExportDictionary []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary
} }
// VersionInfo 版本信息结构体 // VersionInfo 版本信息结构体
+4 -3
View File
@@ -7,7 +7,8 @@ import (
// ExportVersionResponse 导出版本响应结构体 // ExportVersionResponse 导出版本响应结构体
type ExportVersionResponse struct { type ExportVersionResponse struct {
Version request.VersionInfo `json:"version"` // 版本信息 Version request.VersionInfo `json:"version"` // 版本信息
Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu Menus []system.SysBaseMenu `json:"menus"` // 菜单数据,直接复用SysBaseMenu
Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi Apis []system.SysApi `json:"apis"` // API数据,直接复用SysApi
Dictionaries []system.SysDictionary `json:"dictionaries"` // 字典数据,直接复用SysDictionary
} }
+4 -4
View File
@@ -8,10 +8,10 @@ import (
// 版本管理 结构体 SysVersion // 版本管理 结构体 SysVersion
type SysVersion struct { type SysVersion struct {
global.GVA_MODEL global.GVA_MODEL
VersionName *string `json:"versionName" form:"versionName" gorm:"comment:版本名称;column:version_name;size:255;" binding:"required"` //版本名称 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"` //版本号 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;"` //版本描述 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;"` //版本数据 VersionData *string `json:"versionData" form:"versionData" gorm:"comment:版本数据JSON;column:version_data;type:text;"` //版本数据
} }
// TableName 版本管理 SysVersion自定义表名 sys_versions // TableName 版本管理 SysVersion自定义表名 sys_versions
-7
View File
@@ -1,7 +0,0 @@
package api
type ApiGroup struct {
LLMApi
}
var ApiGroupApp = new(ApiGroup)
-177
View File
@@ -1,177 +0,0 @@
package api
import (
"fmt"
"io"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine/model/request"
volcengineResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine/service"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"go.uber.org/zap"
)
type LLMApi struct{}
// ChatCompletion
// @Tags Volcengine
// @Summary LLM聊天完成
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.ChatRequest true "聊天请求参数"
// @Success 200 {object} response.Response{data=volcengineResponse.ChatResponse} "聊天响应"
// @Router /volcengine/llm/chat [post]
func (l *LLMApi) ChatCompletion(c *gin.Context) {
var chatReq request.ChatRequest
err := c.ShouldBindJSON(&chatReq)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
// 生成请求ID
if chatReq.RequestID == "" {
chatReq.RequestID = uuid.New().String()
}
// 根据stream参数选择响应模式
if chatReq.Stream {
// 流式响应 (SSE)
l.handleStreamResponse(c, chatReq)
} else {
// 普通JSON响应
l.handleNormalResponse(c, chatReq)
}
}
// handleNormalResponse 处理普通JSON响应
func (l *LLMApi) handleNormalResponse(c *gin.Context, chatReq request.ChatRequest) {
resp, err := service.LLMServiceApp.ChatCompletion(chatReq)
if err != nil {
global.GVA_LOG.Error("聊天完成失败!", zap.Error(err), zap.String("requestID", chatReq.RequestID))
response.FailWithMessage("聊天完成失败: "+err.Error(), c)
return
}
global.GVA_LOG.Info("聊天完成成功", zap.String("requestID", chatReq.RequestID))
response.OkWithDetailed(resp, "聊天完成成功", c)
}
// handleStreamResponse 处理流式响应 (SSE)
func (l *LLMApi) handleStreamResponse(c *gin.Context, chatReq request.ChatRequest) {
// 设置SSE响应头
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Headers", "Cache-Control")
// 创建事件通道
eventChan := make(chan volcengineResponse.StreamEvent, 100)
defer close(eventChan)
// 启动流式处理
go func() {
err := service.LLMServiceApp.StreamChatCompletion(chatReq, eventChan)
if err != nil {
global.GVA_LOG.Error("流式聊天失败!", zap.Error(err), zap.String("requestID", chatReq.RequestID))
eventChan <- volcengineResponse.StreamEvent{
Event: "error",
Data: volcengineResponse.ChatResponse{
ID: chatReq.RequestID,
Error: &volcengineResponse.APIError{
Code: "stream_error",
Message: err.Error(),
Type: "internal_error",
},
},
}
}
}()
// 发送流式数据
c.Stream(func(w io.Writer) bool {
select {
case event, ok := <-eventChan:
if !ok {
return false
}
switch event.Event {
case "message":
// 发送消息事件
c.SSEvent("message", event.Data)
case "error":
// 发送错误事件
c.SSEvent("error", event.Data)
return false
case "done":
// 发送完成事件
c.SSEvent("done", event.Data)
return false
}
return true
case <-time.After(30 * time.Second):
// 超时处理
c.SSEvent("error", volcengineResponse.ChatResponse{
ID: chatReq.RequestID,
Error: &volcengineResponse.APIError{
Code: "timeout",
Message: "Stream timeout",
Type: "timeout_error",
},
})
return false
}
})
}
// StopGeneration
// @Tags Volcengine
// @Summary 停止LLM生成
// @Security ApiKeyAuth
// @accept application/json
// @Produce application/json
// @Param data body request.StopRequest true "停止请求参数"
// @Success 200 {object} response.Response{data=volcengineResponse.StopResponse} "停止响应"
// @Router /volcengine/llm/stop [post]
func (l *LLMApi) StopGeneration(c *gin.Context) {
var stopReq request.StopRequest
err := c.ShouldBindJSON(&stopReq)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
resp, err := service.LLMServiceApp.StopGeneration(stopReq)
if err != nil {
global.GVA_LOG.Error("停止生成失败!", zap.Error(err), zap.String("requestID", stopReq.RequestID))
response.FailWithMessage("停止生成失败: "+err.Error(), c)
return
}
if resp.Success {
global.GVA_LOG.Info("停止生成成功", zap.String("requestID", stopReq.RequestID))
response.OkWithDetailed(resp, "停止生成成功", c)
} else {
global.GVA_LOG.Warn("停止生成失败", zap.String("requestID", stopReq.RequestID), zap.String("reason", resp.Message))
response.FailWithDetailed(resp, resp.Message, c)
}
}
// GetActiveSessionsCount
// @Tags Volcengine
// @Summary 获取活跃会话数量
// @Security ApiKeyAuth
// @Produce application/json
// @Success 200 {object} response.Response{data=int} "活跃会话数量"
// @Router /volcengine/llm/sessions [get]
func (l *LLMApi) GetActiveSessionsCount(c *gin.Context) {
count := service.LLMServiceApp.GetActiveSessionsCount()
response.OkWithDetailed(gin.H{"count": count}, fmt.Sprintf("当前活跃会话数量: %d", count), c)
}
-23
View File
@@ -1,23 +0,0 @@
package volcengine
import (
"github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine/router"
"github.com/gin-gonic/gin"
)
type volcenginePlugin struct{}
// CreateVolcenginePlug 创建volcengine插件实例
func CreateVolcenginePlug() *volcenginePlugin {
return &volcenginePlugin{}
}
// Register 注册路由
func (*volcenginePlugin) Register(group *gin.RouterGroup) {
router.RouterGroupApp.InitLLMRouter(group)
}
// RouterPath 返回注册路由路径
func (*volcenginePlugin) RouterPath() string {
return "volcengine"
}
@@ -1,23 +0,0 @@
package request
// Message 聊天消息结构体
type Message struct {
Role string `json:"role" form:"role" binding:"required"` // 角色: system, user, assistant
Content string `json:"content" form:"content" binding:"required"` // 消息内容
}
// ChatRequest 聊天请求结构体
type ChatRequest struct {
Messages []Message `json:"messages" form:"messages" binding:"required"` // 对话消息列表
Model string `json:"model" form:"model"` // 模型名称,如果为空则使用默认模型
Stream bool `json:"stream" form:"stream"` // 是否流式响应,默认false
Temperature float64 `json:"temperature" form:"temperature"` // 温度参数,控制随机性,范围0-1
MaxTokens int `json:"max_tokens" form:"max_tokens"` // 最大生成token数
TopP float64 `json:"top_p" form:"top_p"` // 核采样参数
RequestID string `json:"request_id,omitempty" form:"request_id,omitempty"` // 请求ID,用于流式响应管理
}
// StopRequest 停止生成请求结构体
type StopRequest struct {
RequestID string `json:"request_id" form:"request_id" binding:"required"` // 要停止的请求ID
}
@@ -1,66 +0,0 @@
package response
import "time"
// Message 聊天消息结构体
type Message struct {
Role string `json:"role"` // 角色: system, user, assistant
Content string `json:"content"` // 消息内容
}
// Choice 响应选择结构体
type Choice struct {
Index int `json:"index"` // 选择索引
Message Message `json:"message,omitempty"` // 完整响应消息(非流式)
Delta Message `json:"delta,omitempty"` // 增量消息(流式)
FinishReason string `json:"finish_reason,omitempty"` // 结束原因: stop, length, content_filter
}
// Usage 使用量统计结构体
type Usage struct {
PromptTokens int `json:"prompt_tokens"` // 输入token数
CompletionTokens int `json:"completion_tokens"` // 输出token数
TotalTokens int `json:"total_tokens"` // 总token数
}
// APIError API错误结构体
type APIError struct {
Code string `json:"code"` // 错误代码
Message string `json:"message"` // 错误消息
Type string `json:"type"` // 错误类型
}
// ChatResponse 聊天响应结构体
type ChatResponse struct {
ID string `json:"id"` // 响应ID
Object string `json:"object"` // 对象类型: chat.completion 或 chat.completion.chunk
Created int64 `json:"created"` // 创建时间戳
Model string `json:"model"` // 使用的模型
Choices []Choice `json:"choices"` // 响应选择列表
Usage *Usage `json:"usage,omitempty"` // 使用量统计(非流式响应)
Error *APIError `json:"error,omitempty"` // 错误信息
}
// StopResponse 停止生成响应结构体
type StopResponse struct {
Success bool `json:"success"` // 是否成功停止
Message string `json:"message"` // 响应消息
RequestID string `json:"request_id"` // 请求ID
StoppedAt int64 `json:"stopped_at"` // 停止时间戳
}
// StreamEvent 流式事件结构体
type StreamEvent struct {
Event string `json:"event"` // 事件类型: message, error, done
Data ChatResponse `json:"data"` // 事件数据
}
// ChatSession 聊天会话管理结构体(用于内部管理)
type ChatSession struct {
RequestID string `json:"request_id"` // 请求ID
UserID uint `json:"user_id"` // 用户ID
Model string `json:"model"` // 使用的模型
StartTime time.Time `json:"start_time"` // 开始时间
IsStreaming bool `json:"is_streaming"` // 是否流式响应
Status string `json:"status"` // 状态: running, completed, stopped, error
}
-7
View File
@@ -1,7 +0,0 @@
package router
type RouterGroup struct {
LLMRouter
}
var RouterGroupApp = new(RouterGroup)
@@ -1,30 +0,0 @@
package router
import (
"github.com/flipped-aurora/gin-vue-admin/server/middleware"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine/api"
"github.com/gin-gonic/gin"
)
type LLMRouter struct{}
// InitLLMRouter 初始化LLM路由
func (l *LLMRouter) InitLLMRouter(Router *gin.RouterGroup) {
// LLM相关路由组,使用操作记录中间件
llmRouter := Router.Group("llm").Use(middleware.OperationRecord())
// LLM查询路由组,不使用操作记录中间件(用于GET请求)
llmRouterWithoutRecord := Router.Group("llm")
// 获取API实例
llmApi := api.ApiGroupApp.LLMApi
{
// 需要记录操作的路由(POST请求)
llmRouter.POST("chat", llmApi.ChatCompletion) // LLM聊天完成
llmRouter.POST("stop", llmApi.StopGeneration) // 停止LLM生成
}
{
// 不需要记录操作的路由(GET请求)
llmRouterWithoutRecord.GET("sessions", llmApi.GetActiveSessionsCount) // 获取活跃会话数量
}
}
@@ -1,316 +0,0 @@
package service
import (
"context"
"fmt"
"sync"
"time"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine/model/request"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/volcengine/model/response"
"github.com/google/uuid"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime"
"github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
"go.uber.org/zap"
)
type LLMService struct {
client *arkruntime.Client
activeSessions sync.Map // requestID -> context.CancelFunc
}
var LLMServiceApp = new(LLMService)
// InitVolcengineClient 初始化火山引擎客户端
func (l *LLMService) InitVolcengineClient() error {
config := global.GVA_CONFIG.Volcengine
// 验证配置
if config.AccessKey == "" || config.SecretKey == "" {
return fmt.Errorf("volcengine access key or secret key is empty")
}
// 创建ARK Runtime客户端
l.client = arkruntime.NewClientWithAkSk(config.AccessKey, config.SecretKey)
global.GVA_LOG.Info("Volcengine client initialized successfully")
return nil
}
// ChatCompletion 非流式聊天完成
func (l *LLMService) ChatCompletion(req request.ChatRequest) (*response.ChatResponse, error) {
if l.client == nil {
if err := l.InitVolcengineClient(); err != nil {
return nil, err
}
}
// 生成请求ID
if req.RequestID == "" {
req.RequestID = uuid.New().String()
}
// 转换消息格式
messages := make([]*model.ChatCompletionMessage, len(req.Messages))
for i, msg := range req.Messages {
messages[i] = &model.ChatCompletionMessage{
Role: msg.Role,
Content: &model.ChatCompletionMessageContent{
StringValue: &msg.Content,
},
}
}
// 设置模型
modelName := req.Model
if modelName == "" {
modelName = global.GVA_CONFIG.Volcengine.DefaultModel
}
// 创建请求
chatReq := &model.ChatCompletionRequest{
Model: modelName,
Messages: messages,
}
// 设置可选参数
if req.Temperature > 0 {
chatReq.Temperature = float32(req.Temperature)
}
if req.MaxTokens > 0 {
chatReq.MaxTokens = req.MaxTokens
}
if req.TopP > 0 {
chatReq.TopP = float32(req.TopP)
}
// 创建上下文
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// 存储取消函数
l.activeSessions.Store(req.RequestID, cancel)
defer l.activeSessions.Delete(req.RequestID)
// 调用API
resp, err := l.client.CreateChatCompletion(ctx, chatReq)
if err != nil {
global.GVA_LOG.Error("Chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
return nil, fmt.Errorf("chat completion failed: %w", err)
}
// 转换响应格式
chatResp := &response.ChatResponse{
ID: req.RequestID,
Object: "chat.completion",
Created: time.Now().Unix(),
Model: modelName,
Choices: make([]response.Choice, len(resp.Choices)),
}
for i, choice := range resp.Choices {
messageContent := ""
if choice.Message.Content != nil && choice.Message.Content.StringValue != nil {
messageContent = *choice.Message.Content.StringValue
}
chatResp.Choices[i] = response.Choice{
Index: i,
Message: response.Message{
Role: choice.Message.Role,
Content: messageContent,
},
FinishReason: string(choice.FinishReason),
}
}
// 设置使用量统计
chatResp.Usage = &response.Usage{
PromptTokens: resp.Usage.PromptTokens,
CompletionTokens: resp.Usage.CompletionTokens,
TotalTokens: resp.Usage.TotalTokens,
}
global.GVA_LOG.Info("Chat completion successful", zap.String("requestID", req.RequestID))
return chatResp, nil
}
// StreamChatCompletion 流式聊天完成
func (l *LLMService) StreamChatCompletion(req request.ChatRequest, eventChan chan<- response.StreamEvent) error {
if l.client == nil {
if err := l.InitVolcengineClient(); err != nil {
return err
}
}
// 生成请求ID
if req.RequestID == "" {
req.RequestID = uuid.New().String()
}
// 转换消息格式
messages := make([]*model.ChatCompletionMessage, len(req.Messages))
for i, msg := range req.Messages {
messages[i] = &model.ChatCompletionMessage{
Role: msg.Role,
Content: &model.ChatCompletionMessageContent{
StringValue: &msg.Content,
},
}
}
// 设置模型
modelName := req.Model
if modelName == "" {
modelName = global.GVA_CONFIG.Volcengine.DefaultModel
}
// 创建流式请求
chatReq := &model.ChatCompletionRequest{
Model: modelName,
Messages: messages,
Stream: true,
}
// 设置可选参数
if req.Temperature > 0 {
chatReq.Temperature = float32(req.Temperature)
}
if req.MaxTokens > 0 {
chatReq.MaxTokens = req.MaxTokens
}
if req.TopP > 0 {
chatReq.TopP = float32(req.TopP)
}
// 创建上下文
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) // 流式响应需要更长时间
defer cancel()
// 存储取消函数
l.activeSessions.Store(req.RequestID, cancel)
defer l.activeSessions.Delete(req.RequestID)
// 调用流式API
stream_resp, err := l.client.CreateChatCompletionStream(ctx, chatReq)
if err != nil {
global.GVA_LOG.Error("Stream chat completion failed", zap.Error(err), zap.String("requestID", req.RequestID))
return fmt.Errorf("stream chat completion failed: %w", err)
}
defer stream_resp.Close()
// 处理流式响应
for {
select {
case <-ctx.Done():
// 上下文取消
eventChan <- response.StreamEvent{
Event: "error",
Data: response.ChatResponse{
ID: req.RequestID,
Error: &response.APIError{
Code: "context_cancelled",
Message: "Request was cancelled",
Type: "request_cancelled",
},
},
}
return ctx.Err()
default:
// 接收流式数据
recv, err := stream_resp.Recv()
if err != nil {
if err.Error() == "EOF" {
// 流结束
eventChan <- response.StreamEvent{
Event: "done",
Data: response.ChatResponse{
ID: req.RequestID,
Object: "chat.completion.chunk",
},
}
global.GVA_LOG.Info("Stream chat completion finished", zap.String("requestID", req.RequestID))
return nil
}
global.GVA_LOG.Error("Stream receive error", zap.Error(err), zap.String("requestID", req.RequestID))
eventChan <- response.StreamEvent{
Event: "error",
Data: response.ChatResponse{
ID: req.RequestID,
Error: &response.APIError{
Code: "stream_error",
Message: err.Error(),
Type: "stream_error",
},
},
}
return err
}
// 转换并发送响应
chatResp := response.ChatResponse{
ID: req.RequestID,
Object: "chat.completion.chunk",
Created: time.Now().Unix(),
Model: modelName,
Choices: make([]response.Choice, len(recv.Choices)),
}
for i, choice := range recv.Choices {
chatResp.Choices[i] = response.Choice{
Index: i,
Delta: response.Message{
Role: choice.Delta.Role,
Content: choice.Delta.Content,
},
}
if choice.FinishReason != "" {
chatResp.Choices[i].FinishReason = string(choice.FinishReason)
}
}
eventChan <- response.StreamEvent{
Event: "message",
Data: chatResp,
}
}
}
}
// StopGeneration 停止生成
func (l *LLMService) StopGeneration(req request.StopRequest) (*response.StopResponse, error) {
// 查找并取消对应的请求
if cancelFunc, ok := l.activeSessions.Load(req.RequestID); ok {
if cancel, ok := cancelFunc.(context.CancelFunc); ok {
cancel()
l.activeSessions.Delete(req.RequestID)
global.GVA_LOG.Info("Request stopped successfully", zap.String("requestID", req.RequestID))
return &response.StopResponse{
Success: true,
Message: "Request stopped successfully",
RequestID: req.RequestID,
StoppedAt: time.Now().Unix(),
}, nil
}
}
global.GVA_LOG.Warn("Request not found or already completed", zap.String("requestID", req.RequestID))
return &response.StopResponse{
Success: false,
Message: "Request not found or already completed",
RequestID: req.RequestID,
StoppedAt: time.Now().Unix(),
}, nil
}
// GetActiveSessionsCount 获取活跃会话数量
func (l *LLMService) GetActiveSessionsCount() int {
count := 0
l.activeSessions.Range(func(key, value interface{}) bool {
count++
return true
})
return count
}
@@ -2,13 +2,13 @@ package api
// ApiGroup 微信集成API组 // ApiGroup 微信集成API组
type ApiGroup struct { type ApiGroup struct {
WechatMiniApi WechatMiniApi // 微信小程序API MiniApi MiniApi // 微信小程序API
MpUserApi MpUserApi // 公众号用户API MpUserApi MpUserApi // 公众号用户API
MpMessageApi MpMessageApi // 公众号消息API MpMessageApi MpMessageApi // 公众号消息API
MpAutoReplyApi MpAutoReplyApi // 公众号自动回复API MpAutoReplyApi MpAutoReplyApi // 公众号自动回复API
MpMenuApi MpMenuApi // 公众号菜单API MpMenuApi MpMenuApi // 公众号菜单API
MpMaterialApi MpMaterialApi // 公众号素材API MpMaterialApi MpMaterialApi // 公众号素材API
WechatWebhookApi WechatWebhookApi // 微信Webhook API WebhookApi WebhookApi // 微信Webhook API
} }
// ApiGroupApp API组实例 // ApiGroupApp API组实例
@@ -8,13 +8,14 @@ import (
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request" "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response" wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service" "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
) )
type WechatMiniApi struct{} type MiniApi struct{}
var wechatMiniService = service.ServiceGroupApp.WechatMiniService var miniService = service.ServiceGroupApp.MiniService
// Login 小程序登录 // Login 小程序登录
// @Tags WechatMini // @Tags WechatMini
@@ -25,7 +26,7 @@ var wechatMiniService = service.ServiceGroupApp.WechatMiniService
// @Param data body request.MiniLoginRequest true "登录参数" // @Param data body request.MiniLoginRequest true "登录参数"
// @Success 200 {object} response.Response{data=response.MiniLoginResponse} "登录成功" // @Success 200 {object} response.Response{data=response.MiniLoginResponse} "登录成功"
// @Router /wechat/mini/login [post] // @Router /wechat/mini/login [post]
func (w *WechatMiniApi) Login(c *gin.Context) { func (w *MiniApi) Login(c *gin.Context) {
var req request.MiniLoginRequest var req request.MiniLoginRequest
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)
if err != nil { if err != nil {
@@ -38,18 +39,25 @@ func (w *WechatMiniApi) Login(c *gin.Context) {
return return
} }
user, err := wechatMiniService.Code2Session(req.Code) user, err := miniService.Code2Session(req.Code)
if err != nil { if err != nil {
global.GVA_LOG.Error("小程序登录失败!", zap.Error(err)) global.GVA_LOG.Error("小程序登录失败!", zap.Error(err))
response.FailWithMessage("登录失败: "+err.Error(), c) response.FailWithMessage("登录失败: "+err.Error(), c)
return return
} }
// 这里可以生成JWT token或其他认证信息 // 生成JWT token
token, _, err := utils.AppUserLoginToken(user)
if err != nil {
global.GVA_LOG.Error("生成token失败!", zap.Error(err))
response.FailWithMessage("登录失败: "+err.Error(), c)
return
}
resp := wechatResponse.MiniLoginResponse{ resp := wechatResponse.MiniLoginResponse{
OpenID: user.OpenID, OpenID: user.OpenID,
UnionID: user.UnionID, UnionID: user.UnionID,
UserID: user.UserID, Token: token,
} }
response.OkWithDetailed(resp, "登录成功", c) response.OkWithDetailed(resp, "登录成功", c)
@@ -64,14 +72,14 @@ func (w *WechatMiniApi) Login(c *gin.Context) {
// @Param openid query string true "用户openid" // @Param openid query string true "用户openid"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "获取成功" // @Success 200 {object} response.Response{data=model.WechatMiniUser} "获取成功"
// @Router /wechat/mini/userinfo [get] // @Router /wechat/mini/userinfo [get]
func (w *WechatMiniApi) GetUserInfo(c *gin.Context) { func (w *MiniApi) GetUserInfo(c *gin.Context) {
openid := c.Query("openid") openid := c.Query("openid")
if openid == "" { if openid == "" {
response.FailWithMessage("openid不能为空", c) response.FailWithMessage("openid不能为空", c)
return return
} }
user, err := wechatMiniService.GetUserInfo(openid) user, err := miniService.GetUserInfo(openid)
if err != nil { if err != nil {
global.GVA_LOG.Error("获取用户信息失败!", zap.Error(err)) global.GVA_LOG.Error("获取用户信息失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c) response.FailWithMessage("获取失败: "+err.Error(), c)
@@ -90,7 +98,7 @@ func (w *WechatMiniApi) GetUserInfo(c *gin.Context) {
// @Param data body request.UpdateUserInfoRequest true "用户信息" // @Param data body request.UpdateUserInfoRequest true "用户信息"
// @Success 200 {object} response.Response "更新成功" // @Success 200 {object} response.Response "更新成功"
// @Router /wechat/mini/userinfo [put] // @Router /wechat/mini/userinfo [put]
func (w *WechatMiniApi) UpdateUserInfo(c *gin.Context) { func (w *MiniApi) UpdateUserInfo(c *gin.Context) {
var req request.UpdateUserInfoRequest var req request.UpdateUserInfoRequest
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)
if err != nil { if err != nil {
@@ -103,7 +111,7 @@ func (w *WechatMiniApi) UpdateUserInfo(c *gin.Context) {
return return
} }
err = wechatMiniService.UpdateUserInfo(req.OpenID, req.UserInfo) err = miniService.UpdateUserInfo(req.OpenID, req.UserInfo)
if err != nil { if err != nil {
global.GVA_LOG.Error("更新用户信息失败!", zap.Error(err)) global.GVA_LOG.Error("更新用户信息失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c) response.FailWithMessage("更新失败: "+err.Error(), c)
@@ -122,7 +130,7 @@ func (w *WechatMiniApi) UpdateUserInfo(c *gin.Context) {
// @Param data body request.BindPhoneRequest true "绑定参数" // @Param data body request.BindPhoneRequest true "绑定参数"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "绑定成功" // @Success 200 {object} response.Response{data=model.WechatMiniUser} "绑定成功"
// @Router /wechat/mini/bind-phone [post] // @Router /wechat/mini/bind-phone [post]
func (w *WechatMiniApi) BindPhone(c *gin.Context) { func (w *MiniApi) BindPhone(c *gin.Context) {
var req request.BindPhoneRequest var req request.BindPhoneRequest
err := c.ShouldBindJSON(&req) err := c.ShouldBindJSON(&req)
if err != nil { if err != nil {
@@ -135,7 +143,7 @@ func (w *WechatMiniApi) BindPhone(c *gin.Context) {
return return
} }
user, err := wechatMiniService.BindPhoneAndLinkUser(req.OpenID, req.Phone) user, err := miniService.BindPhone(req.OpenID, req.Phone)
if err != nil { if err != nil {
global.GVA_LOG.Error("绑定手机号失败!", zap.Error(err)) global.GVA_LOG.Error("绑定手机号失败!", zap.Error(err))
response.FailWithMessage("绑定失败: "+err.Error(), c) response.FailWithMessage("绑定失败: "+err.Error(), c)
@@ -155,7 +163,7 @@ func (w *WechatMiniApi) BindPhone(c *gin.Context) {
// @Param pageSize query int false "每页数量" // @Param pageSize query int false "每页数量"
// @Success 200 {object} response.Response{data=response.PageResult} "获取成功" // @Success 200 {object} response.Response{data=response.PageResult} "获取成功"
// @Router /wechat/mini/users [get] // @Router /wechat/mini/users [get]
func (w *WechatMiniApi) GetUserList(c *gin.Context) { func (w *MiniApi) GetUserList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10")) pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
@@ -166,7 +174,7 @@ func (w *WechatMiniApi) GetUserList(c *gin.Context) {
pageSize = 10 pageSize = 10
} }
users, total, err := wechatMiniService.GetUserList(page, pageSize) users, total, err := miniService.GetUserList(page, pageSize)
if err != nil { if err != nil {
global.GVA_LOG.Error("获取用户列表失败!", zap.Error(err)) global.GVA_LOG.Error("获取用户列表失败!", zap.Error(err))
response.FailWithMessage("获取失败: "+err.Error(), c) response.FailWithMessage("获取失败: "+err.Error(), c)
@@ -190,14 +198,14 @@ func (w *WechatMiniApi) GetUserList(c *gin.Context) {
// @Param unionid query string true "UnionID" // @Param unionid query string true "UnionID"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "检查成功" // @Success 200 {object} response.Response{data=model.WechatMiniUser} "检查成功"
// @Router /wechat/mini/check-unionid [get] // @Router /wechat/mini/check-unionid [get]
func (w *WechatMiniApi) CheckUnionID(c *gin.Context) { func (w *MiniApi) CheckUnionID(c *gin.Context) {
unionid := c.Query("unionid") unionid := c.Query("unionid")
if unionid == "" { if unionid == "" {
response.FailWithMessage("unionid不能为空", c) response.FailWithMessage("unionid不能为空", c)
return return
} }
user, err := wechatMiniService.CheckUnionIDExists(unionid) user, err := miniService.CheckUnionIDExists(unionid)
if err != nil { if err != nil {
global.GVA_LOG.Error("检查UnionID失败!", zap.Error(err)) global.GVA_LOG.Error("检查UnionID失败!", zap.Error(err))
response.FailWithMessage("检查失败: "+err.Error(), c) response.FailWithMessage("检查失败: "+err.Error(), c)
@@ -301,7 +301,7 @@ func (m *MpConfigApi) ValidateConfig(c *gin.Context) {
} }
// 使用微信公众号服务验证配置 // 使用微信公众号服务验证配置
wechatMpService := service.ServiceGroupApp.WechatMpService wechatMpService := service.ServiceGroupApp.MpService
err = wechatMpService.ValidateConfig(config) err = wechatMpService.ValidateConfig(config)
if err != nil { if err != nil {
global.GVA_LOG.Error("配置连通性测试失败!", zap.Error(err)) global.GVA_LOG.Error("配置连通性测试失败!", zap.Error(err))
@@ -0,0 +1,271 @@
package user
import (
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/model/common/response"
systemReq "github.com/flipped-aurora/gin-vue-admin/server/model/system/request"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/request"
wechatResponse "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/service"
"github.com/flipped-aurora/gin-vue-admin/server/utils"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type MiniUserApi struct{}
var miniService = service.ServiceGroupApp.MiniService
// Login 小程序登录
// @Tags WechatMiniUser
// @Summary 小程序登录
// @Description 微信小程序登录接口
// @Accept json
// @Produce json
// @Param data body request.MiniLoginRequest true "登录参数"
// @Success 200 {object} wechatResponse.Response{data=wechatResponse.MiniLoginResponse} "登录成功"
// @Router /wechat/user/mini/login [post]
func (w *MiniUserApi) Login(c *gin.Context) {
var req request.MiniLoginRequest
err := c.ShouldBindJSON(&req)
if err != nil {
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_PARAM_INVALID, err.Error()))
return
}
if req.Code == "" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_PARAM_MISSING))
return
}
user, err := miniService.Code2Session(req.Code)
if err != nil {
global.GVA_LOG.Error("小程序登录失败!", zap.Error(err))
// 根据错误类型返回不同的错误码
if err.Error() == "获取用户 OpenID 失败" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_WX_OPENID_EMPTY))
} else if err.Error() == "微信小程序 Code2Session 失败" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_WX_SESSION_FAILED))
} else {
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_WX_CODE_INVALID, err.Error()))
}
return
}
// 生成JWT token
token, _, err := utils.AppUserLoginToken(user)
if err != nil {
global.GVA_LOG.Error("生成token失败!", zap.Error(err))
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_INTERNAL, "生成token失败"))
return
}
resp := wechatResponse.MiniLoginResponse{
OpenID: user.OpenID,
UnionID: user.UnionID,
Token: token,
}
c.JSON(200, wechatResponse.SuccessResponseWithMsg(resp, "登录成功"))
}
// GetUserInfo 获取用户信息
// @Tags WechatMiniUser
// @Summary 获取用户信息
// @Description 获取当前登录用户的详细信息
// @Accept json
// @Produce json
// @Success 200 {object} wechatResponse.Response{data=wechatResponse.MiniUserInfoResponse} "获取成功"
// @Router /user/wechat/mini/userinfo [get]
func (w *MiniUserApi) GetUserInfo(c *gin.Context) {
// 从JWT token中获取用户信息
claims, exists := c.Get("user_claims")
if !exists {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_UNAUTHORIZED))
return
}
userClaims, ok := claims.(*systemReq.AppUserClaims)
if !ok {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_TOKEN_INVALID))
return
}
openid := userClaims.AppBaseClaims.OpenID
if openid == "" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_WX_OPENID_EMPTY))
return
}
user, err := miniService.GetUserInfo(openid)
if err != nil {
global.GVA_LOG.Error("获取用户信息失败!", zap.Error(err))
if err.Error() == "用户不存在" {
c.JSON(200, wechatResponse.ErrorResponse(wechatResponse.ERROR_USER_NOT_FOUND))
} else {
c.JSON(200, wechatResponse.ErrorResponseWithMsg(wechatResponse.ERROR_INTERNAL, err.Error()))
}
return
}
// 构造响应数据
resp := wechatResponse.MiniUserInfoResponse{
NickName: user.Nickname,
Avatar: user.AvatarURL,
Gender: user.Gender,
City: user.City,
Province: user.Province,
Country: user.Country,
NeedPhoneAuth: user.Phone == nil || *user.Phone == "", // 如果没有手机号则需要授权
}
c.JSON(200, wechatResponse.SuccessResponseWithMsg(resp, "获取成功"))
}
// UpdateUserInfo 更新用户信息
// @Tags WechatMiniUser
// @Summary 更新用户信息
// @Description 更新小程序用户信息
// @Accept json
// @Produce json
// @Param data body request.UpdateUserInfoRequest true "用户信息"
// @Success 200 {object} response.Response "更新成功"
// @Router /wechat/user/mini/userinfo [put]
func (w *MiniUserApi) UpdateUserInfo(c *gin.Context) {
var req request.UpdateUserInfoRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" {
response.FailWithMessage("openid不能为空", c)
return
}
err = miniService.UpdateUserInfo(req.OpenID, req.UserInfo)
if err != nil {
global.GVA_LOG.Error("更新用户信息失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithMessage("更新成功", c)
}
// BindPhone 绑定手机号
// @Tags WechatMiniUser
// @Summary 绑定手机号
// @Description 绑定手机号并关联系统用户
// @Accept json
// @Produce json
// @Param data body request.BindPhoneRequest true "绑定参数"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "绑定成功"
// @Router /wechat/user/mini/bind-phone [post]
func (w *MiniUserApi) BindPhone(c *gin.Context) {
var req request.BindPhoneRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.OpenID == "" || req.Phone == "" {
response.FailWithMessage("openid和手机号不能为空", c)
return
}
user, err := miniService.BindPhone(req.OpenID, req.Phone)
if err != nil {
global.GVA_LOG.Error("绑定手机号失败!", zap.Error(err))
response.FailWithMessage("绑定失败: "+err.Error(), c)
return
}
response.OkWithDetailed(user, "绑定成功", c)
}
// UpdatePhoneNumber 更新用户手机号
// @Tags WechatMiniUser
// @Summary 更新用户手机号
// @Description 通过微信手机号授权更新用户手机号
// @Accept json
// @Produce json
// @Param data body request.WxPhoneLoginRequest true "手机号授权参数"
// @Success 200 {object} response.Response{data=string} "更新成功"
// @Router /user/wechat/mini/phone-update [post]
func (w *MiniUserApi) UpdatePhoneNumber(c *gin.Context) {
var req request.WxPhoneLoginRequest
err := c.ShouldBindJSON(&req)
if err != nil {
response.FailWithMessage(err.Error(), c)
return
}
if req.EncryptedData == "" || req.IV == "" {
response.FailWithMessage("encryptedData和iv不能为空", c)
return
}
// 从 JWT token 中获取用户信息
claims, exists := c.Get("user_claims")
if !exists {
response.FailWithMessage("获取用户信息失败", c)
return
}
// 获取当前用户的信息
userClaims, ok := claims.(*systemReq.AppUserClaims)
if !ok {
response.FailWithMessage("用户信息格式错误", c)
return
}
// 直接从 claims 中获取 openid
openid := userClaims.AppBaseClaims.OpenID
if openid == "" {
response.FailWithMessage("用户 OpenID 不存在", c)
return
}
phoneNumber, err := miniService.UpdatePhoneByEncryption(openid, req.EncryptedData, req.IV)
if err != nil {
global.GVA_LOG.Error("更新手机号失败!", zap.Error(err))
response.FailWithMessage("更新失败: "+err.Error(), c)
return
}
response.OkWithDetailed(phoneNumber, "手机号更新成功", c)
}
// CheckUnionID 检查UnionID是否存在(为APP登录预留)
// @Tags WechatMiniUser
// @Summary 检查UnionID
// @Description 检查UnionID是否已存在用户
// @Accept json
// @Produce json
// @Param unionid query string true "UnionID"
// @Success 200 {object} response.Response{data=model.WechatMiniUser} "检查成功"
// @Router /wechat/user/mini/check-unionid [get]
func (w *MiniUserApi) CheckUnionID(c *gin.Context) {
unionid := c.Query("unionid")
if unionid == "" {
response.FailWithMessage("unionid不能为空", c)
return
}
user, err := miniService.CheckUnionIDExists(unionid)
if err != nil {
global.GVA_LOG.Error("检查UnionID失败!", zap.Error(err))
response.FailWithMessage("检查失败: "+err.Error(), c)
return
}
if user == nil {
response.OkWithDetailed(nil, "UnionID不存在", c)
} else {
response.OkWithDetailed(user, "UnionID已存在", c)
}
}
@@ -16,7 +16,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
type WechatWebhookApi struct{} type WebhookApi struct{}
var webhookMessageService = service.ServiceGroupApp.MpMessageService var webhookMessageService = service.ServiceGroupApp.MpMessageService
@@ -32,7 +32,7 @@ var webhookMessageService = service.ServiceGroupApp.MpMessageService
// @Param echostr query string false "随机字符串" // @Param echostr query string false "随机字符串"
// @Success 200 {string} string "success" // @Success 200 {string} string "success"
// @Router /wechat/official/webhook [get,post] // @Router /wechat/official/webhook [get,post]
func (w *WechatWebhookApi) OfficialAccountWebhook(c *gin.Context) { func (w *WebhookApi) OfficialAccountWebhook(c *gin.Context) {
startTime := time.Now() startTime := time.Now()
// 获取参数 // 获取参数
@@ -133,7 +133,7 @@ func (w *WechatWebhookApi) OfficialAccountWebhook(c *gin.Context) {
} }
// verifySignature 验证微信签名 // verifySignature 验证微信签名
func (w *WechatWebhookApi) verifySignature(signature, timestamp, nonce string) bool { func (w *WebhookApi) verifySignature(signature, timestamp, nonce string) bool {
// 从数据库获取微信公众号配置 // 从数据库获取微信公众号配置
var mpConfig model.MpConfig var mpConfig model.MpConfig
err := global.GVA_DB.Where("config_type = ?", model.ConfigTypeMP).First(&mpConfig).Error err := global.GVA_DB.Where("config_type = ?", model.ConfigTypeMP).First(&mpConfig).Error
@@ -167,7 +167,7 @@ func (w *WechatWebhookApi) verifySignature(signature, timestamp, nonce string) b
} }
// saveWebhookLog 保存Webhook日志到数据库 // saveWebhookLog 保存Webhook日志到数据库
func (w *WechatWebhookApi) saveWebhookLog(log *model.MpWebhookLog) { func (w *WebhookApi) saveWebhookLog(log *model.MpWebhookLog) {
if err := global.GVA_DB.Create(log).Error; err != nil { if err := global.GVA_DB.Create(log).Error; err != nil {
global.GVA_LOG.Error("保存Webhook日志失败", zap.Error(err)) global.GVA_LOG.Error("保存Webhook日志失败", zap.Error(err))
// 数据库记录失败不影响webhook正常响应 // 数据库记录失败不影响webhook正常响应
@@ -5,6 +5,4 @@ type WechatConfig struct {
// 微信小程序配置 // 微信小程序配置
MiniAppID string `mapstructure:"mini-app-id" json:"miniAppId" yaml:"mini-app-id"` MiniAppID string `mapstructure:"mini-app-id" json:"miniAppId" yaml:"mini-app-id"`
MiniAppSecret string `mapstructure:"mini-app-secret" json:"miniAppSecret" yaml:"mini-app-secret"` MiniAppSecret string `mapstructure:"mini-app-secret" json:"miniAppSecret" yaml:"mini-app-secret"`
// 微信公众号配置现在从数据库获取,不再从配置文件读取
} }
@@ -12,31 +12,45 @@ import (
func Api(ctx context.Context) { func Api(ctx context.Context) {
// 注册微信集成相关API // 注册微信集成相关API
utils.RegisterApis( utils.RegisterApis(
// 小程序用户相关API // 小程序用户相关API(公开接口)
system.SysApi{ system.SysApi{
Path: "/wechat/mini/login", Path: "/wechat/user/mini/login",
Description: "小程序登录", Description: "小程序登录",
ApiGroup: "微信小程序", ApiGroup: "微信小程序",
Method: "POST", Method: "POST",
}, },
// 小程序用户相关API(需要认证)
system.SysApi{ system.SysApi{
Path: "/wechat/mini/userinfo", Path: "/user/wechat/mini/phone-update",
Description: "更新用户手机号",
ApiGroup: "微信小程序",
Method: "POST",
},
// 小程序用户相关API(需要认证)
system.SysApi{
Path: "/user/wechat/mini/userinfo",
Description: "获取小程序用户信息", Description: "获取小程序用户信息",
ApiGroup: "微信小程序", ApiGroup: "微信小程序",
Method: "GET", Method: "GET",
}, },
system.SysApi{ system.SysApi{
Path: "/wechat/mini/userinfo", Path: "/user/wechat/mini/userinfo",
Description: "更新小程序用户信息", Description: "更新小程序用户信息",
ApiGroup: "微信小程序", ApiGroup: "微信小程序",
Method: "PUT", Method: "PUT",
}, },
system.SysApi{ system.SysApi{
Path: "/wechat/mini/bind-phone", Path: "/user/wechat/mini/bind-phone",
Description: "绑定手机号", Description: "绑定手机号",
ApiGroup: "微信小程序", ApiGroup: "微信小程序",
Method: "POST", Method: "POST",
}, },
system.SysApi{
Path: "/user/wechat/mini/check-unionid",
Description: "检查UnionID",
ApiGroup: "微信小程序",
Method: "GET",
},
system.SysApi{ system.SysApi{
Path: "/wechat/mini/users", Path: "/wechat/mini/users",
Description: "获取小程序用户列表", Description: "获取小程序用户列表",
@@ -11,7 +11,7 @@ import (
func Gorm(ctx context.Context) { func Gorm(ctx context.Context) {
// 自动迁移数据库表 // 自动迁移数据库表
err := global.GVA_DB.WithContext(ctx).AutoMigrate( err := global.GVA_DB.WithContext(ctx).AutoMigrate(
&model.WechatMiniUser{}, &model.MiniUser{},
&model.MpUser{}, &model.MpUser{},
&model.MpMessage{}, &model.MpMessage{},
&model.MpMenu{}, &model.MpMenu{},
@@ -11,16 +11,21 @@ func Router(engine *gin.Engine) {
// 获取路由组,微信插件使用/api前缀与前端匹配 // 获取路由组,微信插件使用/api前缀与前端匹配
publicGroup := engine.Group("") publicGroup := engine.Group("")
privateGroup := engine.Group("") privateGroup := engine.Group("")
userGroup := engine.Group("")
// 添加JWT认证和Casbin权限中间件 // 添加JWT认证和Casbin权限中间件
privateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler()) privateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
userGroup.Use(middleware.UserJWTAuth())
// 初始化微信路由 // 初始化微信路由
wechatRouter := router.WechatRouter{} wechatRouter := router.Router{}
// 公开路由(不需要认证) // 公开路由(不需要认证)
wechatRouter.InitWechatPublicRouter(publicGroup) wechatRouter.InitWechatPublicRouter(publicGroup)
// 私有路由(需要认证) // 私有路由(需要认证)
wechatRouter.InitWechatRouter(privateGroup) wechatRouter.InitRouter(privateGroup)
// 用户端路由(用户认证)
wechatRouter.InitUserRouter(userGroup)
} }
@@ -4,14 +4,14 @@ import (
"time" "time"
"github.com/flipped-aurora/gin-vue-admin/server/global" "github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/google/uuid"
) )
// WechatMiniUser 微信小程序用户表 // MiniUser 微信小程序用户表
type WechatMiniUser struct { type MiniUser struct {
global.GVA_MODEL global.GVA_MODEL
OpenID string `json:"openid" gorm:"type:varchar(100);not null;uniqueIndex;comment:用户openid"` OpenID string `json:"openid" gorm:"type:varchar(100);not null;uniqueIndex;comment:用户openid"`
UnionID *string `json:"unionid" gorm:"type:varchar(64);index;comment:微信开放平台统一标识"` UnionID *string `json:"unionid" gorm:"type:varchar(64);index;comment:微信开放平台统一标识"`
UserID *uint `json:"userId" gorm:"index;comment:关联系统用户ID"`
SessionKey *string `json:"sessionKey" gorm:"type:varchar(255);comment:会话密钥"` SessionKey *string `json:"sessionKey" gorm:"type:varchar(255);comment:会话密钥"`
Nickname *string `json:"nickname" gorm:"type:varchar(50);comment:用户昵称"` Nickname *string `json:"nickname" gorm:"type:varchar(50);comment:用户昵称"`
AvatarURL *string `json:"avatarUrl" gorm:"type:varchar(1024);comment:用户头像"` AvatarURL *string `json:"avatarUrl" gorm:"type:varchar(1024);comment:用户头像"`
@@ -25,27 +25,93 @@ type WechatMiniUser struct {
} }
// TableName 指定表名 // TableName 指定表名
func (WechatMiniUser) TableName() string { func (MiniUser) TableName() string {
return "wechat_mini_users" return "mini_users"
} }
// IsLinkedToSystemUser 检查是否已关联系统用户 // 实现AppUserLogin接口
func (w *WechatMiniUser) IsLinkedToSystemUser() bool { func (u *MiniUser) GetUUID() uuid.UUID {
return w.UserID != nil && *w.UserID > 0 // 将uint ID转换为UUID,这里使用一个简单的方法
// 在实际项目中,你可能需要为用户添加一个专门的UUID字段
return uuid.New()
}
func (u *MiniUser) GetUserId() uint {
return u.ID
}
func (u *MiniUser) GetOpenID() string {
return u.OpenID
}
func (u *MiniUser) GetUnionID() string {
if u.UnionID != nil {
return *u.UnionID
}
return ""
}
func (u *MiniUser) GetNickname() string {
if u.Nickname != nil {
return *u.Nickname
}
return ""
}
func (u *MiniUser) GetAvatar() string {
if u.AvatarURL != nil {
return *u.AvatarURL
}
return ""
}
func (u *MiniUser) GetPhone() string {
if u.Phone != nil {
return *u.Phone
}
return ""
}
func (u *MiniUser) GetGender() int {
if u.Gender != nil {
return *u.Gender
}
return 0
}
func (u *MiniUser) GetCity() string {
if u.City != nil {
return *u.City
}
return ""
}
func (u *MiniUser) GetProvince() string {
if u.Province != nil {
return *u.Province
}
return ""
}
func (u *MiniUser) GetCountry() string {
if u.Country != nil {
return *u.Country
}
return ""
} }
// HasUnionID 检查是否有 UnionID // HasUnionID 检查是否有 UnionID
func (w *WechatMiniUser) HasUnionID() bool { func (w *MiniUser) HasUnionID() bool {
return w.UnionID != nil && *w.UnionID != "" return w.UnionID != nil && *w.UnionID != ""
} }
// HasPhone 检查是否已绑定手机号 // HasPhone 检查是否已绑定手机号
func (w *WechatMiniUser) HasPhone() bool { func (w *MiniUser) HasPhone() bool {
return w.Phone != nil && *w.Phone != "" return w.Phone != nil && *w.Phone != ""
} }
// GetDisplayName 获取显示名称 // GetDisplayName 获取显示名称
func (w *WechatMiniUser) GetDisplayName() string { func (w *MiniUser) GetDisplayName() string {
if w.Nickname != nil && *w.Nickname != "" { if w.Nickname != nil && *w.Nickname != "" {
return *w.Nickname return *w.Nickname
} }
@@ -53,7 +119,7 @@ func (w *WechatMiniUser) GetDisplayName() string {
} }
// GetAvatarURL 获取头像URL // GetAvatarURL 获取头像URL
func (w *WechatMiniUser) GetAvatarURL() string { func (w *MiniUser) GetAvatarURL() string {
if w.AvatarURL != nil { if w.AvatarURL != nil {
return *w.AvatarURL return *w.AvatarURL
} }
@@ -39,3 +39,10 @@ type SendImageMessageRequest struct {
OpenID string `json:"openid" binding:"required"` // 用户openid OpenID string `json:"openid" binding:"required"` // 用户openid
MediaID string `json:"mediaId" binding:"required"` // 媒体ID MediaID string `json:"mediaId" binding:"required"` // 媒体ID
} }
// WxPhoneLoginRequest 微信手机号授权登录请求
type WxPhoneLoginRequest struct {
Code string `json:"code" binding:"required"` // 微信登录凭证
EncryptedData string `json:"encryptedData" binding:"required"` // 加密的手机号数据
IV string `json:"iv" binding:"required"` // 初始向量
}
@@ -0,0 +1,159 @@
package response
// 微信小程序错误码定义
const (
// 成功
SUCCESS = 0
// 通用错误码 (1000-1999)
ERROR_COMMON = 1000 // 通用错误
ERROR_PARAM_INVALID = 1001 // 参数无效
ERROR_PARAM_MISSING = 1002 // 参数缺失
ERROR_INTERNAL = 1003 // 内部错误
ERROR_NETWORK = 1004 // 网络错误
ERROR_TIMEOUT = 1005 // 请求超时
// 认证相关错误码 (2000-2999)
ERROR_AUTH_FAILED = 2000 // 认证失败
ERROR_TOKEN_INVALID = 2001 // Token无效
ERROR_TOKEN_EXPIRED = 2002 // Token过期
ERROR_UNAUTHORIZED = 2003 // 未授权
ERROR_PERMISSION_DENIED = 2004 // 权限不足
// 微信相关错误码 (3000-3999)
ERROR_WX_CODE_INVALID = 3000 // 微信Code无效
ERROR_WX_SESSION_FAILED = 3001 // 获取微信Session失败
ERROR_WX_DECRYPT_FAILED = 3002 // 微信数据解密失败
ERROR_WX_OPENID_EMPTY = 3003 // 微信OpenID为空
ERROR_WX_SESSIONKEY_EMPTY = 3004 // 微信SessionKey为空
ERROR_WX_PHONE_DECRYPT_FAILED = 3005 // 微信手机号解密失败
ERROR_WX_PHONE_NOT_FOUND = 3006 // 解密数据中未找到手机号
// 用户相关错误码 (4000-4999)
ERROR_USER_NOT_FOUND = 4000 // 用户不存在
ERROR_USER_EXISTED = 4001 // 用户已存在
ERROR_USER_CREATE_FAILED = 4002 // 用户创建失败
ERROR_USER_UPDATE_FAILED = 4003 // 用户更新失败
ERROR_USER_DELETE_FAILED = 4004 // 用户删除失败
ERROR_USER_DISABLED = 4005 // 用户已禁用
)
// 错误信息映射
var ErrorMessages = map[int]string{
SUCCESS: "操作成功",
// 通用错误
ERROR_COMMON: "操作失败",
ERROR_PARAM_INVALID: "参数无效",
ERROR_PARAM_MISSING: "参数缺失",
ERROR_INTERNAL: "内部错误",
ERROR_NETWORK: "网络错误",
ERROR_TIMEOUT: "请求超时",
// 认证相关错误
ERROR_AUTH_FAILED: "认证失败",
ERROR_TOKEN_INVALID: "Token无效",
ERROR_TOKEN_EXPIRED: "Token已过期",
ERROR_UNAUTHORIZED: "未授权访问",
ERROR_PERMISSION_DENIED: "权限不足",
// 微信相关错误
ERROR_WX_CODE_INVALID: "微信Code无效",
ERROR_WX_SESSION_FAILED: "获取微信Session失败",
ERROR_WX_DECRYPT_FAILED: "微信数据解密失败",
ERROR_WX_OPENID_EMPTY: "微信OpenID为空",
ERROR_WX_SESSIONKEY_EMPTY: "微信SessionKey为空",
ERROR_WX_PHONE_DECRYPT_FAILED: "微信手机号解密失败",
ERROR_WX_PHONE_NOT_FOUND: "解密数据中未找到手机号",
// 用户相关错误
ERROR_USER_NOT_FOUND: "用户不存在",
ERROR_USER_EXISTED: "用户已存在",
ERROR_USER_CREATE_FAILED: "用户创建失败",
ERROR_USER_UPDATE_FAILED: "用户更新失败",
ERROR_USER_DELETE_FAILED: "用户删除失败",
ERROR_USER_DISABLED: "用户已禁用",
}
// GetErrorMessage 获取错误信息
func GetErrorMessage(code int) string {
if msg, exists := ErrorMessages[code]; exists {
return msg
}
return "未知错误"
}
// Response 标准响应结构
type Response struct {
Code int `json:"code"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
}
// SuccessResponse 成功响应
func SuccessResponse(data interface{}) Response {
return Response{
Code: SUCCESS,
Data: data,
Msg: GetErrorMessage(SUCCESS),
}
}
// SuccessResponseWithMsg 带自定义消息的成功响应
func SuccessResponseWithMsg(data interface{}, msg string) Response {
return Response{
Code: SUCCESS,
Data: data,
Msg: msg,
}
}
// ErrorResponse 错误响应
func ErrorResponse(code int) Response {
return Response{
Code: code,
Data: nil,
Msg: GetErrorMessage(code),
}
}
// ErrorResponseWithMsg 带自定义消息的错误响应
func ErrorResponseWithMsg(code int, msg string) Response {
return Response{
Code: code,
Data: nil,
Msg: msg,
}
}
// MiniLoginResponse 小程序登录响应
type MiniLoginResponse struct {
OpenID string `json:"openid"` // 用户openid
UnionID *string `json:"unionid"` // 用户unionid
Token string `json:"token"` // JWT token
}
// MiniUserInfoResponse 小程序用户信息响应
type MiniUserInfoResponse struct {
NickName *string `json:"nickName"` // 用户昵称
Avatar *string `json:"avatar"` // 用户头像
Phone *string `json:"phone"` // 用户手机号
Gender *int `json:"gender"` // 性别:0-未知,1-男,2-女
City *string `json:"city"` // 城市
Province *string `json:"province"` // 省份
Country *string `json:"country"` // 国家
NeedPhoneAuth bool `json:"needPhoneAuth"` // 是否需要授权手机号
}
// PhoneUpdateResponse 手机号更新响应
type PhoneUpdateResponse struct {
Phone string `json:"phone"` // 更新后的手机号
}
// PageResult 分页结果
type PageResult struct {
List interface{} `json:"list"` // 数据列表
Total int64 `json:"total"` // 总数
Page int `json:"page"` // 当前页
PageSize int `json:"pageSize"` // 每页数量
}
@@ -1,16 +0,0 @@
package response
// MiniLoginResponse 小程序登录响应
type MiniLoginResponse struct {
OpenID string `json:"openid"` // 用户openid
UnionID *string `json:"unionid"` // 用户unionid
UserID *uint `json:"userId,omitempty"` // 关联的系统用户ID
}
// PageResult 分页结果
type PageResult struct {
List interface{} `json:"list"` // 数据列表
Total int64 `json:"total"` // 总数
Page int `json:"page"` // 当前页
PageSize int `json:"pageSize"` // 每页数量
}
@@ -2,17 +2,18 @@ package router
import ( import (
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/api" "github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/api"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/api/user"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type WechatRouter struct{} type Router struct{}
// InitWechatRouter 初始化微信路由 // InitRouter 初始化微信路由
func (w *WechatRouter) InitWechatRouter(Router *gin.RouterGroup) { func (w *Router) InitRouter(Router *gin.RouterGroup) {
wechatRouter := Router.Group("wechat") wechatRouter := Router.Group("wechat")
// 初始化API实例 // 初始化API实例
wechatMiniApi := api.WechatMiniApi{} miniApi := api.MiniApi{}
mpUserApi := api.MpUserApi{} mpUserApi := api.MpUserApi{}
mpMessageApi := api.MpMessageApi{} mpMessageApi := api.MpMessageApi{}
mpMenuApi := api.MpMenuApi{} mpMenuApi := api.MpMenuApi{}
@@ -26,15 +27,10 @@ func (w *WechatRouter) InitWechatRouter(Router *gin.RouterGroup) {
mpDraftApi := api.MpDraftApi{} mpDraftApi := api.MpDraftApi{}
{ {
// 微信小程序路由 // 微信小程序管理端路由
miniGroup := wechatRouter.Group("mini") miniGroup := wechatRouter.Group("mini")
{ {
miniGroup.POST("login", wechatMiniApi.Login) // 小程序登录 miniGroup.GET("users", miniApi.GetUserList) // 获取用户列表(管理)
miniGroup.GET("userinfo", wechatMiniApi.GetUserInfo) // 获取用户信息
miniGroup.PUT("userinfo", wechatMiniApi.UpdateUserInfo) // 更新用户信息
miniGroup.POST("bind-phone", wechatMiniApi.BindPhone) // 绑定手机号
miniGroup.GET("users", wechatMiniApi.GetUserList) // 获取用户列表(管理)
miniGroup.GET("check-unionid", wechatMiniApi.CheckUnionID) // 检查UnionID
// 小程序统计 // 小程序统计
miniGroup.GET("statistics", miniStatisticsApi.GetMiniStatistics) // 获取基础统计数据 miniGroup.GET("statistics", miniStatisticsApi.GetMiniStatistics) // 获取基础统计数据
@@ -151,13 +147,44 @@ func (w *WechatRouter) InitWechatRouter(Router *gin.RouterGroup) {
} }
// InitWechatPublicRouter 初始化微信公开路由(不需要认证) // InitWechatPublicRouter 初始化微信公开路由(不需要认证)
func (w *WechatRouter) InitWechatPublicRouter(Router *gin.RouterGroup) { func (w *Router) InitWechatPublicRouter(Router *gin.RouterGroup) {
wechatPublicRouter := Router.Group("wechat") wechatPublicRouter := Router.Group("wechat")
webhookApi := api.WechatWebhookApi{} webhookApi := api.WebhookApi{}
// 用户端API实例
miniUserApi := user.MiniUserApi{}
{ {
// 用户端API路由(不需要认证)
userGroup := wechatPublicRouter.Group("user")
{
// 微信小程序用户端路由
userMiniGroup := userGroup.Group("mini")
{
userMiniGroup.POST("login", miniUserApi.Login) // 小程序登录(不需要认证)
}
}
// 微信公众号Webhook(公开接口,微信服务器调用) // 微信公众号Webhook(公开接口,微信服务器调用)
wechatPublicRouter.Any("official/webhook", webhookApi.OfficialAccountWebhook) wechatPublicRouter.Any("official/webhook", webhookApi.OfficialAccountWebhook)
} }
} }
// InitUserRouter 初始化用户端路由(需要用户认证)
func (w *Router) InitUserRouter(Router *gin.RouterGroup) {
wechatUserRouter := Router.Group("user/wechat")
// 用户端API实例
miniUserApi := user.MiniUserApi{}
// 用户端API路由(需要用户认证)
// 微信小程序用户端路由
userMiniGroup := wechatUserRouter.Group("mini")
{
userMiniGroup.GET("userinfo", miniUserApi.GetUserInfo) // 获取用户信息
userMiniGroup.PUT("userinfo", miniUserApi.UpdateUserInfo) // 更新用户信息
userMiniGroup.POST("bind-phone", miniUserApi.BindPhone) // 绑定手机号
userMiniGroup.POST("phone-update", miniUserApi.UpdatePhoneNumber) // 更新手机号(通过加密数据)
userMiniGroup.GET("check-unionid", miniUserApi.CheckUnionID) // 检查UnionID
}
}
@@ -2,8 +2,8 @@ package service
// ServiceGroup 微信集成服务组 // ServiceGroup 微信集成服务组
type ServiceGroup struct { type ServiceGroup struct {
WechatMiniService WechatMiniService // 微信小程序服务 MiniService MiniService // 微信小程序服务
WechatMpService WechatMpService // 微信公众号服务 MpService MpService // 微信公众号服务
MpUserService MpUserService // 公众号用户服务 MpUserService MpUserService // 公众号用户服务
MpMessageService MpMessageService // 公众号消息服务 MpMessageService MpMessageService // 公众号消息服务
MpMenuService MpMenuService // 公众号菜单服务 MpMenuService MpMenuService // 公众号菜单服务
@@ -13,10 +13,10 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type WechatMiniService struct{} type MiniService struct{}
// GetWechatMiniProgram 获取微信小程序实例 // GetWechatMiniProgram 获取微信小程序实例
func (w *WechatMiniService) GetWechatMiniProgram() (*miniprogram.MiniProgram, error) { func (w *MiniService) GetWechatMiniProgram() (*miniprogram.MiniProgram, error) {
if global.GVA_CONFIG.Wechat.MiniAppID == "" || global.GVA_CONFIG.Wechat.MiniAppSecret == "" { if global.GVA_CONFIG.Wechat.MiniAppID == "" || global.GVA_CONFIG.Wechat.MiniAppSecret == "" {
return nil, errors.New("微信小程序配置不完整") return nil, errors.New("微信小程序配置不完整")
} }
@@ -33,7 +33,7 @@ func (w *WechatMiniService) GetWechatMiniProgram() (*miniprogram.MiniProgram, er
} }
// Code2Session 小程序登录凭证校验 // Code2Session 小程序登录凭证校验
func (w *WechatMiniService) Code2Session(code string) (*model.WechatMiniUser, error) { func (w *MiniService) Code2Session(code string) (*model.MiniUser, error) {
mini, err := w.GetWechatMiniProgram() mini, err := w.GetWechatMiniProgram()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -51,14 +51,18 @@ func (w *WechatMiniService) Code2Session(code string) (*model.WechatMiniUser, er
} }
// 查找或创建用户 // 查找或创建用户
var user model.WechatMiniUser var user model.MiniUser
err = global.GVA_DB.Where("openid = ?", session.OpenID).First(&user).Error err = global.GVA_DB.Where("open_id = ?", session.OpenID).First(&user).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
// 创建新用户 // 创建新用户
user = model.WechatMiniUser{ // 生成默认昵称:萌宠爱 + OpenID的前6位
defaultNickname := "萌宠爱" + session.OpenID[:6]
user = model.MiniUser{
OpenID: session.OpenID, OpenID: session.OpenID,
SessionKey: &session.SessionKey, SessionKey: &session.SessionKey,
Nickname: &defaultNickname,
} }
if session.UnionID != "" { if session.UnionID != "" {
user.UnionID = &session.UnionID user.UnionID = &session.UnionID
@@ -67,12 +71,6 @@ func (w *WechatMiniService) Code2Session(code string) (*model.WechatMiniUser, er
now := time.Now() now := time.Now()
user.LastLoginTime = &now user.LastLoginTime = &now
// 尝试关联系统用户
err = w.linkSystemUser(&user)
if err != nil {
global.GVA_LOG.Error("关联系统用户失败: " + err.Error())
}
err = global.GVA_DB.Create(&user).Error err = global.GVA_DB.Create(&user).Error
if err != nil { if err != nil {
global.GVA_LOG.Error("创建微信小程序用户失败: " + err.Error()) global.GVA_LOG.Error("创建微信小程序用户失败: " + err.Error())
@@ -91,14 +89,6 @@ func (w *WechatMiniService) Code2Session(code string) (*model.WechatMiniUser, er
user.UnionID = &session.UnionID user.UnionID = &session.UnionID
} }
// 如果还没有关联系统用户,尝试关联
if user.UserID == nil {
err = w.linkSystemUser(&user)
if err != nil {
global.GVA_LOG.Error("关联系统用户失败: " + err.Error())
}
}
err = global.GVA_DB.Save(&user).Error err = global.GVA_DB.Save(&user).Error
if err != nil { if err != nil {
global.GVA_LOG.Error("更新微信小程序用户失败: " + err.Error()) global.GVA_LOG.Error("更新微信小程序用户失败: " + err.Error())
@@ -110,9 +100,22 @@ func (w *WechatMiniService) Code2Session(code string) (*model.WechatMiniUser, er
} }
// GetUserInfo 获取用户信息 // GetUserInfo 获取用户信息
func (w *WechatMiniService) GetUserInfo(openid string) (*model.WechatMiniUser, error) { func (w *MiniService) GetUserInfo(openid string) (*model.MiniUser, error) {
var user model.WechatMiniUser var user model.MiniUser
err := global.GVA_DB.Where("openid = ?", openid).First(&user).Error err := global.GVA_DB.Where("open_id = ?", openid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在")
}
return nil, err
}
return &user, nil
}
// GetUserByID 根据用户ID获取用户信息
func (w *MiniService) GetUserByID(userID uint) (*model.MiniUser, error) {
var user model.MiniUser
err := global.GVA_DB.Where("id = ?", userID).First(&user).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("用户不存在") return nil, errors.New("用户不存在")
@@ -123,7 +126,7 @@ func (w *WechatMiniService) GetUserInfo(openid string) (*model.WechatMiniUser, e
} }
// UpdateUserInfo 更新用户信息 // UpdateUserInfo 更新用户信息
func (w *WechatMiniService) UpdateUserInfo(openid string, userInfo map[string]interface{}) error { func (w *MiniService) UpdateUserInfo(openid string, userInfo map[string]interface{}) error {
updates := make(map[string]interface{}) updates := make(map[string]interface{})
if nickname, ok := userInfo["nickname"]; ok { if nickname, ok := userInfo["nickname"]; ok {
@@ -166,7 +169,7 @@ func (w *WechatMiniService) UpdateUserInfo(openid string, userInfo map[string]in
return errors.New("没有需要更新的信息") return errors.New("没有需要更新的信息")
} }
err := global.GVA_DB.Model(&model.WechatMiniUser{}).Where("openid = ?", openid).Updates(updates).Error err := global.GVA_DB.Model(&model.MiniUser{}).Where("open_id = ?", openid).Updates(updates).Error
if err != nil { if err != nil {
global.GVA_LOG.Error("更新微信小程序用户信息失败: " + err.Error()) global.GVA_LOG.Error("更新微信小程序用户信息失败: " + err.Error())
return err return err
@@ -176,11 +179,11 @@ func (w *WechatMiniService) UpdateUserInfo(openid string, userInfo map[string]in
} }
// GetUserList 获取用户列表 // GetUserList 获取用户列表
func (w *WechatMiniService) GetUserList(page, pageSize int) ([]model.WechatMiniUser, int64, error) { func (w *MiniService) GetUserList(page, pageSize int) ([]model.MiniUser, int64, error) {
var users []model.WechatMiniUser var users []model.MiniUser
var total int64 var total int64
db := global.GVA_DB.Model(&model.WechatMiniUser{}) db := global.GVA_DB.Model(&model.MiniUser{})
// 获取总数 // 获取总数
err := db.Count(&total).Error err := db.Count(&total).Error
@@ -198,34 +201,9 @@ func (w *WechatMiniService) GetUserList(page, pageSize int) ([]model.WechatMiniU
return users, total, nil return users, total, nil
} }
// linkSystemUser 关联系统用户 (通过 unionid 关联,为未来APP扩展预留)
func (w *WechatMiniService) linkSystemUser(user *model.WechatMiniUser) error {
if user.UnionID == nil || *user.UnionID == "" {
// 没有 unionid,无法跨平台关联,但小程序仍可正常使用
global.GVA_LOG.Warn("小程序用户没有 UnionID,无法跨平台关联")
return nil
}
// 查找是否有其他小程序用户(或未来的APP用户)已经关联了系统用户
var existingMiniUser model.WechatMiniUser
err := global.GVA_DB.Where("unionid = ? AND user_id IS NOT NULL AND openid != ?", *user.UnionID, user.OpenID).First(&existingMiniUser).Error
if err == nil {
// 找到了已关联系统用户的用户,使用相同的 user_id
user.UserID = existingMiniUser.UserID
global.GVA_LOG.Info("通过 UnionID 关联到已存在的系统用户: " + *user.UnionID)
return nil
}
// 如果没有找到,暂时不自动创建系统用户
// 等用户输入手机号后,通过业务逻辑关联或创建系统用户
global.GVA_LOG.Info("新用户,等待手机号验证后关联系统用户,unionid: " + *user.UnionID)
return nil
}
// GetUserByUnionID 根据 UnionID 获取用户信息 // GetUserByUnionID 根据 UnionID 获取用户信息
func (w *WechatMiniService) GetUserByUnionID(unionid string) (*model.WechatMiniUser, error) { func (w *MiniService) GetUserByUnionID(unionid string) (*model.MiniUser, error) {
var user model.WechatMiniUser var user model.MiniUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&user).Error err := global.GVA_DB.Where("unionid = ?", unionid).First(&user).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -236,51 +214,29 @@ func (w *WechatMiniService) GetUserByUnionID(unionid string) (*model.WechatMiniU
return &user, nil return &user, nil
} }
// LinkToSystemUser 手动关联系统用户 // BindPhone 绑定手机号
func (w *WechatMiniService) LinkToSystemUser(openid string, userID uint) error { func (w *MiniService) BindPhone(openid, phone string) (*model.MiniUser, error) {
err := global.GVA_DB.Model(&model.WechatMiniUser{}).Where("openid = ?", openid).Update("user_id", userID).Error // 更新小程序用户的手机号
if err != nil { err := global.GVA_DB.Model(&model.MiniUser{}).Where("open_id = ?", openid).Update("phone", phone).Error
global.GVA_LOG.Error("关联系统用户失败: " + err.Error())
return err
}
global.GVA_LOG.Info("成功关联系统用户")
return nil
}
// BindPhoneAndLinkUser 绑定手机号并关联系统用户
func (w *WechatMiniService) BindPhoneAndLinkUser(openid, phone string) (*model.WechatMiniUser, error) {
// 1. 更新小程序用户的手机号
err := global.GVA_DB.Model(&model.WechatMiniUser{}).Where("openid = ?", openid).Update("phone", phone).Error
if err != nil { if err != nil {
global.GVA_LOG.Error("更新手机号失败: " + err.Error()) global.GVA_LOG.Error("更新手机号失败: " + err.Error())
return nil, err return nil, err
} }
// 2. 获取更新后的用户信息 // 获取更新后的用户信息
var user model.WechatMiniUser var user model.MiniUser
err = global.GVA_DB.Where("openid = ?", openid).First(&user).Error err = global.GVA_DB.Where("open_id = ?", openid).First(&user).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
// 3. 根据手机号查找或创建系统用户 global.GVA_LOG.Info("用户绑定手机号成功: " + phone + ", openid: " + openid)
// 这里需要根据实际的系统用户表结构来实现
// 暂时先记录日志,实际项目中需要调用系统用户服务
global.GVA_LOG.Info("用户绑定手机号: " + phone + ", openid: " + openid)
// 4. 如果有 UnionID,同时更新所有相关的微信账号
if user.UnionID != nil && *user.UnionID != "" {
// 未来如果有APP用户,也会通过相同的 UnionID 关联到同一个系统用户
global.GVA_LOG.Info("用户有 UnionID,支持跨平台关联: " + *user.UnionID)
}
return &user, nil return &user, nil
} }
// GetUserByPhone 根据手机号获取用户 // GetUserByPhone 根据手机号获取用户
func (w *WechatMiniService) GetUserByPhone(phone string) (*model.WechatMiniUser, error) { func (w *MiniService) GetUserByPhone(phone string) (*model.MiniUser, error) {
var user model.WechatMiniUser var user model.MiniUser
err := global.GVA_DB.Where("phone = ?", phone).First(&user).Error err := global.GVA_DB.Where("phone = ?", phone).First(&user).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -292,13 +248,13 @@ func (w *WechatMiniService) GetUserByPhone(phone string) (*model.WechatMiniUser,
} }
// CheckUnionIDExists 检查 UnionID 是否已存在(为APP登录预留) // CheckUnionIDExists 检查 UnionID 是否已存在(为APP登录预留)
func (w *WechatMiniService) CheckUnionIDExists(unionid string) (*model.WechatMiniUser, error) { func (w *MiniService) CheckUnionIDExists(unionid string) (*model.MiniUser, error) {
if unionid == "" { if unionid == "" {
return nil, errors.New("unionid 不能为空") return nil, errors.New("unionid 不能为空")
} }
var user model.WechatMiniUser var user model.MiniUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&user).Error err := global.GVA_DB.Where("union_id = ?", unionid).First(&user).Error
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil // 不存在,返回 nil 但不报错 return nil, nil // 不存在,返回 nil 但不报错
@@ -307,3 +263,52 @@ func (w *WechatMiniService) CheckUnionIDExists(unionid string) (*model.WechatMin
} }
return &user, nil return &user, nil
} }
// UpdatePhoneByEncryption 通过加密数据更新用户手机号
func (w *MiniService) UpdatePhoneByEncryption(openid, encryptedData, iv string) (string, error) {
// 1. 获取用户信息
var user model.MiniUser
err := global.GVA_DB.Where("open_id = ?", openid).First(&user).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return "", errors.New("用户不存在")
}
return "", err
}
if user.SessionKey == nil || *user.SessionKey == "" {
return "", errors.New("用户 SessionKey 不存在,请重新登录")
}
// 2. 获取微信小程序实例
mini, err := w.GetWechatMiniProgram()
if err != nil {
return "", err
}
// 3. 使用 session_key 解密手机号
encryptor := mini.GetEncryptor()
plainData, err := encryptor.Decrypt(*user.SessionKey, encryptedData, iv)
if err != nil {
global.GVA_LOG.Error("解密手机号失败: " + err.Error())
return "", errors.New("解密手机号失败")
}
// 4. 从解密数据中获取手机号
phoneNumber := ""
if plainData.PhoneNumber != "" {
phoneNumber = plainData.PhoneNumber
} else {
return "", errors.New("解密数据中未找到手机号")
}
// 5. 更新用户手机号
err = global.GVA_DB.Model(&user).Update("phone", phoneNumber).Error
if err != nil {
global.GVA_LOG.Error("更新手机号失败: " + err.Error())
return "", err
}
global.GVA_LOG.Info("用户手机号更新成功: " + phoneNumber + ", openid: " + openid)
return phoneNumber, nil
}
@@ -14,7 +14,7 @@ type MiniStatisticsService struct{}
// getMiniProgram 获取微信小程序实例 // getMiniProgram 获取微信小程序实例
func (m *MiniStatisticsService) getMiniProgram() (*miniprogram.MiniProgram, error) { func (m *MiniStatisticsService) getMiniProgram() (*miniprogram.MiniProgram, error) {
// 使用微信小程序服务获取实例 // 使用微信小程序服务获取实例
return ServiceGroupApp.WechatMiniService.GetWechatMiniProgram() return ServiceGroupApp.MiniService.GetWechatMiniProgram()
} }
// GetMiniStatistics 获取小程序基础统计数据 // GetMiniStatistics 获取小程序基础统计数据
@@ -113,7 +113,7 @@ func (m *MpConfigService) testOfficialAccountConfig(config *model.MpConfig) erro
// testMiniProgramConfig 测试小程序配置 // testMiniProgramConfig 测试小程序配置
func (m *MpConfigService) testMiniProgramConfig(config *model.MpConfig) error { func (m *MpConfigService) testMiniProgramConfig(config *model.MpConfig) error {
// 使用ServiceGroupApp获取默认小程序实例进行测试 // 使用ServiceGroupApp获取默认小程序实例进行测试
mini, err := ServiceGroupApp.WechatMiniService.GetWechatMiniProgram() mini, err := ServiceGroupApp.MiniService.GetWechatMiniProgram()
if err != nil { if err != nil {
return fmt.Errorf("获取小程序实例失败: %v", err) return fmt.Errorf("获取小程序实例失败: %v", err)
} }
@@ -252,7 +252,7 @@ func (m *MpConfigService) GenerateQrCode(config *model.MpConfig) (string, error)
} }
// 使用微信公众号服务生成二维码 // 使用微信公众号服务生成二维码
wechatMpService := ServiceGroupApp.WechatMpService wechatMpService := ServiceGroupApp.MpService
return wechatMpService.GenerateQrCode(config) return wechatMpService.GenerateQrCode(config)
} }
@@ -263,6 +263,6 @@ func (m *MpConfigService) ClearQuota(config *model.MpConfig) error {
} }
// 使用微信公众号服务清空配额 // 使用微信公众号服务清空配额
wechatMpService := ServiceGroupApp.WechatMpService wechatMpService := ServiceGroupApp.MpService
return wechatMpService.ClearQuota(config) return wechatMpService.ClearQuota(config)
} }
@@ -13,10 +13,10 @@ import (
"github.com/silenceper/wechat/v2/officialaccount/config" "github.com/silenceper/wechat/v2/officialaccount/config"
) )
type WechatMpService struct{} type MpService struct{}
// GetWechatOfficialAccount 获取微信公众号实例 // GetWechatOfficialAccount 获取微信公众号实例
func (w *WechatMpService) GetWechatOfficialAccount(mpConfig *model.MpConfig) (*officialaccount.OfficialAccount, error) { func (w *MpService) GetWechatOfficialAccount(mpConfig *model.MpConfig) (*officialaccount.OfficialAccount, error) {
if mpConfig == nil { if mpConfig == nil {
return nil, errors.New("微信公众号配置不能为空") return nil, errors.New("微信公众号配置不能为空")
} }
@@ -46,7 +46,7 @@ func (w *WechatMpService) GetWechatOfficialAccount(mpConfig *model.MpConfig) (*o
} }
// GenerateQrCode 生成公众号二维码 // GenerateQrCode 生成公众号二维码
func (w *WechatMpService) GenerateQrCode(mpConfig *model.MpConfig) (string, error) { func (w *MpService) GenerateQrCode(mpConfig *model.MpConfig) (string, error) {
oa, err := w.GetWechatOfficialAccount(mpConfig) oa, err := w.GetWechatOfficialAccount(mpConfig)
if err != nil { if err != nil {
return "", err return "", err
@@ -73,7 +73,7 @@ func (w *WechatMpService) GenerateQrCode(mpConfig *model.MpConfig) (string, erro
} }
// ClearQuota 清空API配额 // ClearQuota 清空API配额
func (w *WechatMpService) ClearQuota(mpConfig *model.MpConfig) error { func (w *MpService) ClearQuota(mpConfig *model.MpConfig) error {
oa, err := w.GetWechatOfficialAccount(mpConfig) oa, err := w.GetWechatOfficialAccount(mpConfig)
if err != nil { if err != nil {
return err return err
@@ -92,7 +92,7 @@ func (w *WechatMpService) ClearQuota(mpConfig *model.MpConfig) error {
} }
// ValidateConfig 验证公众号配置 // ValidateConfig 验证公众号配置
func (w *WechatMpService) ValidateConfig(mpConfig *model.MpConfig) error { func (w *MpService) ValidateConfig(mpConfig *model.MpConfig) error {
oa, err := w.GetWechatOfficialAccount(mpConfig) oa, err := w.GetWechatOfficialAccount(mpConfig)
if err != nil { if err != nil {
return err return err
@@ -115,7 +115,7 @@ func (w *WechatMpService) ValidateConfig(mpConfig *model.MpConfig) error {
} }
// GetServerIPs 获取微信服务器IP地址 // GetServerIPs 获取微信服务器IP地址
func (w *WechatMpService) GetServerIPs(mpConfig *model.MpConfig) ([]string, error) { func (w *MpService) GetServerIPs(mpConfig *model.MpConfig) ([]string, error) {
oa, err := w.GetWechatOfficialAccount(mpConfig) oa, err := w.GetWechatOfficialAccount(mpConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -132,7 +132,7 @@ func (w *WechatMpService) GetServerIPs(mpConfig *model.MpConfig) ([]string, erro
} }
// GetUserInfo 获取用户基本信息 // GetUserInfo 获取用户基本信息
func (w *WechatMpService) GetUserInfo(mpConfig *model.MpConfig, openID string) (map[string]interface{}, error) { func (w *MpService) GetUserInfo(mpConfig *model.MpConfig, openID string) (map[string]interface{}, error) {
oa, err := w.GetWechatOfficialAccount(mpConfig) oa, err := w.GetWechatOfficialAccount(mpConfig)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -158,7 +158,7 @@ func (w *WechatMpService) GetUserInfo(mpConfig *model.MpConfig, openID string) (
} }
// SendTextMessage 发送文本消息 (暂时注释,需要正确的API调用方式) // SendTextMessage 发送文本消息 (暂时注释,需要正确的API调用方式)
// func (w *WechatMpService) SendTextMessage(mpConfig *model.MpConfig, openID, content string) error { // func (w *MpService) SendTextMessage(mpConfig *model.MpConfig, openID, content string) error {
// // TODO: 实现发送文本消息功能 // // TODO: 实现发送文本消息功能
// return errors.New("发送消息功能暂未实现") // return errors.New("发送消息功能暂未实现")
// } // }
@@ -1,234 +0,0 @@
package service
import (
"errors"
"github.com/flipped-aurora/gin-vue-admin/server/global"
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model"
)
type WechatUserService struct{}
// GetUnifiedUserInfo 获取统一的微信用户信息 (通过 unionid 关联)
func (w *WechatUserService) GetUnifiedUserInfo(unionid string) (*UnifiedWechatUser, error) {
if unionid == "" {
return nil, errors.New("unionid 不能为空")
}
var unifiedUser UnifiedWechatUser
// 查找小程序用户
var miniUser model.WechatMiniUser
err := global.GVA_DB.Where("unionid = ?", unionid).First(&miniUser).Error
if err == nil {
unifiedUser.MiniUser = &miniUser
unifiedUser.UserID = miniUser.UserID
}
// 查找公众号用户
var mpUser model.MpUser
err = global.GVA_DB.Where("unionid = ?", unionid).First(&mpUser).Error
if err == nil {
unifiedUser.MpUser = &mpUser
if unifiedUser.UserID == nil {
unifiedUser.UserID = mpUser.UserID
}
}
// 如果都没找到
if unifiedUser.MiniUser == nil && unifiedUser.MpUser == nil {
return nil, errors.New("未找到相关用户信息")
}
unifiedUser.UnionID = unionid
return &unifiedUser, nil
}
// GetUserBySystemUserID 根据系统用户ID获取微信用户信息
func (w *WechatUserService) GetUserBySystemUserID(userID uint) (*UnifiedWechatUser, error) {
var unifiedUser UnifiedWechatUser
unifiedUser.UserID = &userID
// 查找小程序用户
var miniUser model.WechatMiniUser
err := global.GVA_DB.Where("user_id = ?", userID).First(&miniUser).Error
if err == nil {
unifiedUser.MiniUser = &miniUser
if miniUser.UnionID != nil {
unifiedUser.UnionID = *miniUser.UnionID
}
}
// 查找公众号用户
var mpUser model.MpUser
err = global.GVA_DB.Where("user_id = ?", userID).First(&mpUser).Error
if err == nil {
unifiedUser.MpUser = &mpUser
if unifiedUser.UnionID == "" && mpUser.UnionID != nil {
unifiedUser.UnionID = *mpUser.UnionID
}
}
// 如果都没找到
if unifiedUser.MiniUser == nil && unifiedUser.MpUser == nil {
return nil, errors.New("未找到相关用户信息")
}
return &unifiedUser, nil
}
// LinkAllAccountsToSystemUser 将所有微信账号关联到系统用户
func (w *WechatUserService) LinkAllAccountsToSystemUser(unionid string, userID uint) error {
if unionid == "" {
return errors.New("unionid 不能为空")
}
// 开启事务
tx := global.GVA_DB.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
// 关联小程序用户
err := tx.Model(&model.WechatMiniUser{}).Where("unionid = ?", unionid).Update("user_id", userID).Error
if err != nil {
tx.Rollback()
global.GVA_LOG.Error("关联小程序用户失败: " + err.Error())
return err
}
// 关联公众号用户
err = tx.Model(&model.MpUser{}).Where("unionid = ?", unionid).Update("user_id", userID).Error
if err != nil {
tx.Rollback()
global.GVA_LOG.Error("关联公众号用户失败: " + err.Error())
return err
}
// 提交事务
err = tx.Commit().Error
if err != nil {
global.GVA_LOG.Error("提交事务失败: " + err.Error())
return err
}
global.GVA_LOG.Info("成功关联所有微信账号到系统用户")
return nil
}
// GetAllWechatUsers 获取所有微信用户列表 (分页)
func (w *WechatUserService) GetAllWechatUsers(page, pageSize int) ([]UnifiedWechatUser, int64, error) {
var users []UnifiedWechatUser
var total int64
// 这里需要复杂的查询逻辑,暂时简化实现
// 实际项目中可能需要使用 SQL 联合查询或者分别查询后合并
// 先获取所有有 unionid 的用户
var unionIDs []string
// 从小程序用户中获取 unionid
err := global.GVA_DB.Model(&model.WechatMiniUser{}).
Where("unionid IS NOT NULL AND unionid != ''").
Distinct("unionid").
Pluck("unionid", &unionIDs).Error
if err != nil {
return nil, 0, err
}
// 从公众号用户中获取 unionid
var mpUnionIDs []string
err = global.GVA_DB.Model(&model.MpUser{}).
Where("unionid IS NOT NULL AND unionid != ''").
Distinct("unionid").
Pluck("unionid", &mpUnionIDs).Error
if err != nil {
return nil, 0, err
}
// 合并去重
unionIDMap := make(map[string]bool)
for _, id := range unionIDs {
unionIDMap[id] = true
}
for _, id := range mpUnionIDs {
unionIDMap[id] = true
}
// 转换为切片
allUnionIDs := make([]string, 0, len(unionIDMap))
for id := range unionIDMap {
allUnionIDs = append(allUnionIDs, id)
}
total = int64(len(allUnionIDs))
// 分页处理
start := (page - 1) * pageSize
end := start + pageSize
if end > len(allUnionIDs) {
end = len(allUnionIDs)
}
if start >= len(allUnionIDs) {
return users, total, nil
}
pageUnionIDs := allUnionIDs[start:end]
// 获取每个 unionid 对应的用户信息
for _, unionID := range pageUnionIDs {
unifiedUser, err := w.GetUnifiedUserInfo(unionID)
if err == nil {
users = append(users, *unifiedUser)
}
}
return users, total, nil
}
// UnifiedWechatUser 统一的微信用户信息结构
type UnifiedWechatUser struct {
UnionID string `json:"unionid"`
UserID *uint `json:"userId"`
MiniUser *model.WechatMiniUser `json:"miniUser,omitempty"`
MpUser *model.MpUser `json:"mpUser,omitempty"`
}
// HasMiniProgram 是否有小程序账号
func (u *UnifiedWechatUser) HasMiniProgram() bool {
return u.MiniUser != nil
}
// HasOfficialAccount 是否有公众号账号
func (u *UnifiedWechatUser) HasOfficialAccount() bool {
return u.MpUser != nil
}
// IsLinkedToSystemUser 是否已关联系统用户
func (u *UnifiedWechatUser) IsLinkedToSystemUser() bool {
return u.UserID != nil && *u.UserID > 0
}
// GetDisplayName 获取显示名称
func (u *UnifiedWechatUser) GetDisplayName() string {
if u.MiniUser != nil && u.MiniUser.Nickname != nil {
return *u.MiniUser.Nickname
}
if u.MpUser != nil && u.MpUser.Nickname != nil {
return *u.MpUser.Nickname
}
return "未知用户"
}
// GetAvatarURL 获取头像URL
func (u *UnifiedWechatUser) GetAvatarURL() string {
if u.MiniUser != nil && u.MiniUser.AvatarURL != nil {
return *u.MiniUser.AvatarURL
}
if u.MpUser != nil && u.MpUser.HeadImageURL != nil {
return *u.MpUser.HeadImageURL
}
return ""
}
@@ -0,0 +1,102 @@
package utils
import (
"github.com/flipped-aurora/gin-vue-admin/server/plugin/wechat-integration/model/response"
"github.com/gin-gonic/gin"
)
// ResponseHelper 响应助手
type ResponseHelper struct{}
// Success 成功响应
func (r *ResponseHelper) Success(c *gin.Context, data interface{}) {
c.JSON(200, response.SuccessResponse(data))
}
// SuccessWithMsg 带消息的成功响应
func (r *ResponseHelper) SuccessWithMsg(c *gin.Context, data interface{}, msg string) {
c.JSON(200, response.SuccessResponseWithMsg(data, msg))
}
// Error 错误响应
func (r *ResponseHelper) Error(c *gin.Context, code int) {
c.JSON(200, response.ErrorResponse(code))
}
// ErrorWithMsg 带消息的错误响应
func (r *ResponseHelper) ErrorWithMsg(c *gin.Context, code int, msg string) {
c.JSON(200, response.ErrorResponseWithMsg(code, msg))
}
// HandleError 处理错误并返回相应的错误码
func (r *ResponseHelper) HandleError(c *gin.Context, err error) {
if err == nil {
return
}
errMsg := err.Error()
// 根据错误信息返回相应的错误码
switch errMsg {
case "用户不存在":
r.Error(c, response.ERROR_USER_NOT_FOUND)
case "获取用户 OpenID 失败":
r.Error(c, response.ERROR_WX_OPENID_EMPTY)
case "微信小程序 Code2Session 失败":
r.Error(c, response.ERROR_WX_SESSION_FAILED)
case "解密手机号失败":
r.Error(c, response.ERROR_WX_PHONE_DECRYPT_FAILED)
case "解密数据中未找到手机号":
r.Error(c, response.ERROR_WX_PHONE_NOT_FOUND)
case "用户 SessionKey 不存在,请重新登录":
r.Error(c, response.ERROR_WX_SESSIONKEY_EMPTY)
default:
// 检查是否是数据库相关错误
if isDBError(errMsg) {
r.ErrorWithMsg(c, response.ERROR_INTERNAL, "数据库操作失败")
} else {
r.ErrorWithMsg(c, response.ERROR_COMMON, errMsg)
}
}
}
// isDBError 判断是否是数据库错误
func isDBError(errMsg string) bool {
dbErrorKeywords := []string{
"Unknown column",
"Table doesn't exist",
"Duplicate entry",
"Data too long",
"cannot be null",
"foreign key constraint",
}
for _, keyword := range dbErrorKeywords {
if contains(errMsg, keyword) {
return true
}
}
return false
}
// contains 检查字符串是否包含子字符串
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr ||
(len(s) > len(substr) &&
(s[:len(substr)] == substr ||
s[len(s)-len(substr):] == substr ||
indexOf(s, substr) >= 0)))
}
// indexOf 查找子字符串的位置
func indexOf(s, substr string) int {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return i
}
}
return -1
}
// 全局响应助手实例
var ResponseUtil = &ResponseHelper{}
@@ -110,7 +110,7 @@ getDataSourceFunc()
<el-date-picker <el-date-picker
v-model="searchInfo.createdAtRange" v-model="searchInfo.createdAtRange"
class="w-[380px]" class="!w-380px"
type="datetimerange" type="datetimerange"
range-separator="至" range-separator="至"
start-placeholder="开始时间" start-placeholder="开始时间"
@@ -161,7 +161,7 @@ getDataSourceFunc()
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
{{ if .GvaModel }} {{ 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> <template #default="scope">{{ "{{ formatDate(scope.row.CreatedAt) }}" }}</template>
</el-table-column> </el-table-column>
{{ end }} {{ end }}
@@ -393,8 +393,8 @@ const searchInfo = ref({})
// 排序 // 排序
const sortChange = ({ prop, order }) => { const sortChange = ({ prop, order }) => {
const sortMap = { const sortMap = {
CreatedAt:"CreatedAt", CreatedAt:"created_at",
ID:"ID", ID:"id",
{{- range .Fields}} {{- range .Fields}}
{{- if .Table}} {{- if .Table}}
{{- if and .Sort}} {{- if and .Sort}}
+4 -4
View File
@@ -108,7 +108,7 @@ getDataSourceFunc()
</template> </template>
<el-date-picker <el-date-picker
v-model="searchInfo.createdAtRange" v-model="searchInfo.createdAtRange"
class="w-[380px]" class="!w-380px"
type="datetimerange" type="datetimerange"
range-separator="至" range-separator="至"
start-placeholder="开始时间" start-placeholder="开始时间"
@@ -158,7 +158,7 @@ getDataSourceFunc()
> >
<el-table-column type="selection" width="55" /> <el-table-column type="selection" width="55" />
{{ if .GvaModel }} {{ 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> <template #default="scope">{{ "{{ formatDate(scope.row.CreatedAt) }}" }}</template>
</el-table-column> </el-table-column>
{{ end }} {{ end }}
@@ -388,8 +388,8 @@ const searchInfo = ref({})
// 排序 // 排序
const sortChange = ({ prop, order }) => { const sortChange = ({ prop, order }) => {
const sortMap = { const sortMap = {
CreatedAt:"CreatedAt", CreatedAt:"created_at",
ID:"ID", ID:"id",
{{- range .Fields}} {{- range .Fields}}
{{- if .Table}} {{- if .Table}}
{{- if and .Sort}} {{- if and .Sort}}
+2
View File
@@ -13,6 +13,7 @@ type RouterGroup struct {
PetFamilyPetsRouter PetFamilyPetsRouter
PetPetsRouter PetPetsRouter
PetRecordsRouter PetRecordsRouter
PetAssistantRouter
} }
var ( var (
@@ -26,4 +27,5 @@ var (
petFamilyPetsApi = api.ApiGroupApp.PetApiGroup.PetFamilyPetsApi petFamilyPetsApi = api.ApiGroupApp.PetApiGroup.PetFamilyPetsApi
petPetsApi = api.ApiGroupApp.PetApiGroup.PetPetsApi petPetsApi = api.ApiGroupApp.PetApiGroup.PetPetsApi
petRecordsApi = api.ApiGroupApp.PetApiGroup.PetRecordsApi petRecordsApi = api.ApiGroupApp.PetApiGroup.PetRecordsApi
petUserApiGroup = api.ApiGroupApp.PetApiGroup.PetUserApiGroup
) )
@@ -22,7 +22,5 @@ func (s *PetAdoptionApplicationsRouter) InitPetAdoptionApplicationsRouter(Router
petAdoptionApplicationsRouterWithoutRecord.GET("findPetAdoptionApplications", petAdoptionApplicationsApi.FindPetAdoptionApplications) // 根据ID获取petAdoptionApplications表 petAdoptionApplicationsRouterWithoutRecord.GET("findPetAdoptionApplications", petAdoptionApplicationsApi.FindPetAdoptionApplications) // 根据ID获取petAdoptionApplications表
petAdoptionApplicationsRouterWithoutRecord.GET("getPetAdoptionApplicationsList", petAdoptionApplicationsApi.GetPetAdoptionApplicationsList) // 获取petAdoptionApplications表列表 petAdoptionApplicationsRouterWithoutRecord.GET("getPetAdoptionApplicationsList", petAdoptionApplicationsApi.GetPetAdoptionApplicationsList) // 获取petAdoptionApplications表列表
} }
{
petAdoptionApplicationsRouterWithoutAuth.GET("getPetAdoptionApplicationsPublic", petAdoptionApplicationsApi.GetPetAdoptionApplicationsPublic) // petAdoptionApplications表开放接口
}
} }
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetAdoptionPostsRouter) InitPetAdoptionPostsRouter(Router *gin.RouterGr
petAdoptionPostsRouterWithoutRecord.GET("findPetAdoptionPosts", petAdoptionPostsApi.FindPetAdoptionPosts) // 根据ID获取petAdoptionPosts表 petAdoptionPostsRouterWithoutRecord.GET("findPetAdoptionPosts", petAdoptionPostsApi.FindPetAdoptionPosts) // 根据ID获取petAdoptionPosts表
petAdoptionPostsRouterWithoutRecord.GET("getPetAdoptionPostsList", petAdoptionPostsApi.GetPetAdoptionPostsList) // 获取petAdoptionPosts表列表 petAdoptionPostsRouterWithoutRecord.GET("getPetAdoptionPostsList", petAdoptionPostsApi.GetPetAdoptionPostsList) // 获取petAdoptionPosts表列表
} }
{
petAdoptionPostsRouterWithoutAuth.GET("getPetAdoptionPostsPublic", petAdoptionPostsApi.GetPetAdoptionPostsPublic) // petAdoptionPosts表开放接口
}
} }
@@ -22,7 +22,5 @@ func (s *PetAiAssistantConversationsRouter) InitPetAiAssistantConversationsRoute
petAiAssistantConversationsRouterWithoutRecord.GET("findPetAiAssistantConversations", petAiAssistantConversationsApi.FindPetAiAssistantConversations) // 根据ID获取petAiAssistantConversations表 petAiAssistantConversationsRouterWithoutRecord.GET("findPetAiAssistantConversations", petAiAssistantConversationsApi.FindPetAiAssistantConversations) // 根据ID获取petAiAssistantConversations表
petAiAssistantConversationsRouterWithoutRecord.GET("getPetAiAssistantConversationsList", petAiAssistantConversationsApi.GetPetAiAssistantConversationsList) // 获取petAiAssistantConversations表列表 petAiAssistantConversationsRouterWithoutRecord.GET("getPetAiAssistantConversationsList", petAiAssistantConversationsApi.GetPetAiAssistantConversationsList) // 获取petAiAssistantConversations表列表
} }
{
petAiAssistantConversationsRouterWithoutAuth.GET("getPetAiAssistantConversationsPublic", petAiAssistantConversationsApi.GetPetAiAssistantConversationsPublic) // petAiAssistantConversations表开放接口
}
} }
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetAiConversationsRouter) InitPetAiConversationsRouter(Router *gin.Rout
petAiConversationsRouterWithoutRecord.GET("findPetAiConversations", petAiConversationsApi.FindPetAiConversations) // 根据ID获取petAiConversations表 petAiConversationsRouterWithoutRecord.GET("findPetAiConversations", petAiConversationsApi.FindPetAiConversations) // 根据ID获取petAiConversations表
petAiConversationsRouterWithoutRecord.GET("getPetAiConversationsList", petAiConversationsApi.GetPetAiConversationsList) // 获取petAiConversations表列表 petAiConversationsRouterWithoutRecord.GET("getPetAiConversationsList", petAiConversationsApi.GetPetAiConversationsList) // 获取petAiConversations表列表
} }
{
petAiConversationsRouterWithoutAuth.GET("getPetAiConversationsPublic", petAiConversationsApi.GetPetAiConversationsPublic) // petAiConversations表开放接口
}
} }
+25
View File
@@ -0,0 +1,25 @@
package pet
import (
"github.com/gin-gonic/gin"
)
type PetAssistantRouter struct{}
// InitPetAssistantRouter 初始化宠物助手路由信息
func (p *PetAssistantRouter) InitPetAssistantRouter(UserRouter *gin.RouterGroup, PublicRouter *gin.RouterGroup) {
// 宠物助手路由组,UserRouter已经应用了UserJWTAuth中间件
petAssistantRouter := UserRouter.Group("pet/user/assistant")
// 获取宠物助手API实例
petAssistantApi := petUserApiGroup.PetAssistantApi
{
// 宠物助手问答相关路由
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) // 获取宠物助手会话列表
}
}
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetFamiliesRouter) InitPetFamiliesRouter(Router *gin.RouterGroup, Publi
petFamiliesRouterWithoutRecord.GET("findPetFamilies", petFamiliesApi.FindPetFamilies) // 根据ID获取petFamilies表 petFamiliesRouterWithoutRecord.GET("findPetFamilies", petFamiliesApi.FindPetFamilies) // 根据ID获取petFamilies表
petFamiliesRouterWithoutRecord.GET("getPetFamiliesList", petFamiliesApi.GetPetFamiliesList) // 获取petFamilies表列表 petFamiliesRouterWithoutRecord.GET("getPetFamiliesList", petFamiliesApi.GetPetFamiliesList) // 获取petFamilies表列表
} }
{
petFamiliesRouterWithoutAuth.GET("getPetFamiliesPublic", petFamiliesApi.GetPetFamiliesPublic) // petFamilies表开放接口
}
} }
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetFamilyInvitationsRouter) InitPetFamilyInvitationsRouter(Router *gin.
petFamilyInvitationsRouterWithoutRecord.GET("findPetFamilyInvitations", petFamilyInvitationsApi.FindPetFamilyInvitations) // 根据ID获取petFamilyInvitations表 petFamilyInvitationsRouterWithoutRecord.GET("findPetFamilyInvitations", petFamilyInvitationsApi.FindPetFamilyInvitations) // 根据ID获取petFamilyInvitations表
petFamilyInvitationsRouterWithoutRecord.GET("getPetFamilyInvitationsList", petFamilyInvitationsApi.GetPetFamilyInvitationsList) // 获取petFamilyInvitations表列表 petFamilyInvitationsRouterWithoutRecord.GET("getPetFamilyInvitationsList", petFamilyInvitationsApi.GetPetFamilyInvitationsList) // 获取petFamilyInvitations表列表
} }
{
petFamilyInvitationsRouterWithoutAuth.GET("getPetFamilyInvitationsPublic", petFamilyInvitationsApi.GetPetFamilyInvitationsPublic) // petFamilyInvitations表开放接口
}
} }
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetFamilyMembersRouter) InitPetFamilyMembersRouter(Router *gin.RouterGr
petFamilyMembersRouterWithoutRecord.GET("findPetFamilyMembers", petFamilyMembersApi.FindPetFamilyMembers) // 根据ID获取petFamilyMembers表 petFamilyMembersRouterWithoutRecord.GET("findPetFamilyMembers", petFamilyMembersApi.FindPetFamilyMembers) // 根据ID获取petFamilyMembers表
petFamilyMembersRouterWithoutRecord.GET("getPetFamilyMembersList", petFamilyMembersApi.GetPetFamilyMembersList) // 获取petFamilyMembers表列表 petFamilyMembersRouterWithoutRecord.GET("getPetFamilyMembersList", petFamilyMembersApi.GetPetFamilyMembersList) // 获取petFamilyMembers表列表
} }
{
petFamilyMembersRouterWithoutAuth.GET("getPetFamilyMembersPublic", petFamilyMembersApi.GetPetFamilyMembersPublic) // petFamilyMembers表开放接口
}
} }
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetFamilyPetsRouter) InitPetFamilyPetsRouter(Router *gin.RouterGroup, P
petFamilyPetsRouterWithoutRecord.GET("findPetFamilyPets", petFamilyPetsApi.FindPetFamilyPets) // 根据ID获取petFamilyPets表 petFamilyPetsRouterWithoutRecord.GET("findPetFamilyPets", petFamilyPetsApi.FindPetFamilyPets) // 根据ID获取petFamilyPets表
petFamilyPetsRouterWithoutRecord.GET("getPetFamilyPetsList", petFamilyPetsApi.GetPetFamilyPetsList) // 获取petFamilyPets表列表 petFamilyPetsRouterWithoutRecord.GET("getPetFamilyPetsList", petFamilyPetsApi.GetPetFamilyPetsList) // 获取petFamilyPets表列表
} }
{
petFamilyPetsRouterWithoutAuth.GET("getPetFamilyPetsPublic", petFamilyPetsApi.GetPetFamilyPetsPublic) // petFamilyPets表开放接口
}
} }
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetPetsRouter) InitPetPetsRouter(Router *gin.RouterGroup, PublicRouter
petPetsRouterWithoutRecord.GET("findPetPets", petPetsApi.FindPetPets) // 根据ID获取petPets表 petPetsRouterWithoutRecord.GET("findPetPets", petPetsApi.FindPetPets) // 根据ID获取petPets表
petPetsRouterWithoutRecord.GET("getPetPetsList", petPetsApi.GetPetPetsList) // 获取petPets表列表 petPetsRouterWithoutRecord.GET("getPetPetsList", petPetsApi.GetPetPetsList) // 获取petPets表列表
} }
{
petPetsRouterWithoutAuth.GET("getPetPetsPublic", petPetsApi.GetPetPetsPublic) // petPets表开放接口
}
} }
+1 -3
View File
@@ -22,7 +22,5 @@ func (s *PetRecordsRouter) InitPetRecordsRouter(Router *gin.RouterGroup, PublicR
petRecordsRouterWithoutRecord.GET("findPetRecords", petRecordsApi.FindPetRecords) // 根据ID获取petRecords表 petRecordsRouterWithoutRecord.GET("findPetRecords", petRecordsApi.FindPetRecords) // 根据ID获取petRecords表
petRecordsRouterWithoutRecord.GET("getPetRecordsList", petRecordsApi.GetPetRecordsList) // 获取petRecords表列表 petRecordsRouterWithoutRecord.GET("getPetRecordsList", petRecordsApi.GetPetRecordsList) // 获取petRecords表列表
} }
{
petRecordsRouterWithoutAuth.GET("getPetRecordsPublic", petRecordsApi.GetPetRecordsPublic) // petRecords表开放接口
}
} }
+1 -1
View File
@@ -18,7 +18,7 @@ func (s *UserRouter) InitUserRouter(Router *gin.RouterGroup) {
userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息 userRouter.PUT("setUserInfo", baseApi.SetUserInfo) // 设置用户信息
userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息 userRouter.PUT("setSelfInfo", baseApi.SetSelfInfo) // 设置自身信息
userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组 userRouter.POST("setUserAuthorities", baseApi.SetUserAuthorities) // 设置用户权限组
userRouter.POST("resetPassword", baseApi.ResetPassword) // 设置用户权限组 userRouter.POST("resetPassword", baseApi.ResetPassword) // 重置用户密码
userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置 userRouter.PUT("setSelfSetting", baseApi.SetSelfSetting) // 用户界面配置
} }
{ {
+1
View File
@@ -11,4 +11,5 @@ type ServiceGroup struct {
PetFamilyPetsService PetFamilyPetsService
PetPetsService PetPetsService
PetRecordsService PetRecordsService
PetChatService
} }
@@ -69,7 +69,3 @@ func (petAdoptionApplicationsService *PetAdoptionApplicationsService) GetPetAdop
err = db.Find(&petAdoptionApplicationss).Error err = db.Find(&petAdoptionApplicationss).Error
return petAdoptionApplicationss, total, err return petAdoptionApplicationss, total, err
} }
func (petAdoptionApplicationsService *PetAdoptionApplicationsService) GetPetAdoptionApplicationsPublic(ctx context.Context) {
// 此方法为获取数据源定义的数据
// 请自行实现
}
-4
View File
@@ -69,7 +69,3 @@ func (petAdoptionPostsService *PetAdoptionPostsService) GetPetAdoptionPostsInfoL
err = db.Find(&petAdoptionPostss).Error err = db.Find(&petAdoptionPostss).Error
return petAdoptionPostss, total, err return petAdoptionPostss, total, err
} }
func (petAdoptionPostsService *PetAdoptionPostsService) GetPetAdoptionPostsPublic(ctx context.Context) {
// 此方法为获取数据源定义的数据
// 请自行实现
}

Some files were not shown because too many files have changed in this diff Show More