初始化
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
* linguist-language=GO
|
||||
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
.buildpath
|
||||
.hgignore.swp
|
||||
.project
|
||||
.orig
|
||||
.swp
|
||||
.idea/
|
||||
.settings/
|
||||
.vscode/
|
||||
bin/
|
||||
**/.DS_Store
|
||||
gf
|
||||
main
|
||||
main.exe
|
||||
output/
|
||||
manifest/output/
|
||||
temp/
|
||||
temp.yaml
|
||||
bin
|
||||
**/config/config.yaml
|
||||
7
Makefile
Normal file
7
Makefile
Normal file
@@ -0,0 +1,7 @@
|
||||
ROOT_DIR = $(shell pwd)
|
||||
NAMESPACE = "default"
|
||||
DEPLOY_NAME = "template-single"
|
||||
DOCKER_NAME = "template-single"
|
||||
|
||||
include ./hack/hack-cli.mk
|
||||
include ./hack/hack.mk
|
||||
4
README.MD
Normal file
4
README.MD
Normal file
@@ -0,0 +1,4 @@
|
||||
# GoFrame Template For SingleRepo
|
||||
|
||||
Quick Start:
|
||||
- https://goframe.org/quick
|
||||
235
api/admin/v1/admin.go
Normal file
235
api/admin/v1/admin.go
Normal file
@@ -0,0 +1,235 @@
|
||||
package v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// ===== 登录 =====
|
||||
|
||||
type LoginReq struct {
|
||||
g.Meta `path:"/login" method:"post" tags:"admin" summary:"管理员登录"`
|
||||
Account string `v:"required#请输入账号" json:"account"`
|
||||
Password string `v:"required#请输入密码" json:"password"`
|
||||
Remember bool `json:"remember"`
|
||||
}
|
||||
type LoginRes struct {
|
||||
Token string `json:"token"`
|
||||
NickName string `json:"nick_name"`
|
||||
RoleName string `json:"role_name"`
|
||||
RoleValue string `json:"role_value"`
|
||||
}
|
||||
|
||||
type MyInfoReq struct {
|
||||
g.Meta `path:"/user/my-info" method:"get" tags:"admin" summary:"当前管理员信息"`
|
||||
}
|
||||
type MyInfoRes struct {
|
||||
Id int64 `json:"id"`
|
||||
Account string `json:"account"`
|
||||
NickName string `json:"nick_name"`
|
||||
RoleName string `json:"role_name"`
|
||||
RoleValue string `json:"role_value"`
|
||||
}
|
||||
|
||||
// ===== 用户管理 =====
|
||||
|
||||
type AdminUserItem struct {
|
||||
Id int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
Openid string `json:"openid"`
|
||||
LevelKey string `json:"level_key"`
|
||||
LevelName string `json:"level_name"`
|
||||
Status int `json:"status"`
|
||||
UsageCount int64 `json:"usage_count"`
|
||||
LastUsedAt string `json:"last_used_at"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type UserListReq struct {
|
||||
g.Meta `path:"/user/list" method:"get" tags:"admin" summary:"用户列表"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Keyword string `json:"keyword"`
|
||||
}
|
||||
type UserListRes struct {
|
||||
List []AdminUserItem `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type UserSetLevelReq struct {
|
||||
g.Meta `path:"/user/set-level" method:"post" tags:"admin" summary:"设置用户等级"`
|
||||
UserId int64 `v:"required" json:"userId"`
|
||||
LevelKey string `v:"required" json:"levelKey"`
|
||||
}
|
||||
type UserSetLevelRes struct{}
|
||||
|
||||
type UserStatusReq struct {
|
||||
g.Meta `path:"/user/status" method:"post" tags:"admin" summary:"启停用户"`
|
||||
UserId int64 `v:"required" json:"userId"`
|
||||
Status int `json:"status"`
|
||||
}
|
||||
type UserStatusRes struct{}
|
||||
|
||||
// ===== 等级管理 =====
|
||||
|
||||
type LevelItem struct {
|
||||
Id int64 `json:"id"`
|
||||
LevelKey string `json:"level_key"`
|
||||
Name string `json:"name"`
|
||||
Modules []string `json:"modules"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"is_enabled"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type LevelListReq struct {
|
||||
g.Meta `path:"/level/list" method:"get" tags:"admin" summary:"等级列表"`
|
||||
}
|
||||
type LevelListRes struct {
|
||||
List []LevelItem `json:"list"`
|
||||
}
|
||||
|
||||
type LevelSaveReq struct {
|
||||
g.Meta `path:"/level/save" method:"post" tags:"admin" summary:"保存等级"`
|
||||
Id int64 `json:"id"`
|
||||
LevelKey string `v:"required-if:id,0#等级标识不能为空" json:"levelKey"`
|
||||
Name string `v:"required#等级名称不能为空" json:"name"`
|
||||
Modules []string `v:"required#至少勾选一个模块" json:"modules"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"isEnabled"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
type LevelSaveRes struct{}
|
||||
|
||||
// ===== 模块管理 =====
|
||||
|
||||
type ModuleItem struct {
|
||||
Id int64 `json:"id"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"is_enabled"`
|
||||
ToolCount int `json:"tool_count"`
|
||||
}
|
||||
|
||||
type ModuleListReq struct {
|
||||
g.Meta `path:"/module/list" method:"get" tags:"admin" summary:"模块列表"`
|
||||
}
|
||||
type ModuleListRes struct {
|
||||
List []ModuleItem `json:"list"`
|
||||
}
|
||||
|
||||
type ModuleSaveReq struct {
|
||||
g.Meta `path:"/module/save" method:"post" tags:"admin" summary:"保存模块"`
|
||||
Id int64 `json:"id"`
|
||||
ModuleKey string `v:"required-if:id,0#模块标识不能为空" json:"moduleKey"`
|
||||
Name string `v:"required#模块名称不能为空" json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"isEnabled"`
|
||||
}
|
||||
type ModuleSaveRes struct{}
|
||||
|
||||
// ===== 工具管理 =====
|
||||
|
||||
type AdminToolItem struct {
|
||||
Id int64 `json:"id"`
|
||||
ToolKey string `json:"tool_key"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
ModuleName string `json:"module_name"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"is_enabled"`
|
||||
IsHot int `json:"is_hot"`
|
||||
UsageCount int64 `json:"usage_count"`
|
||||
}
|
||||
|
||||
type ToolsListReq struct {
|
||||
g.Meta `path:"/tools/all" method:"get" tags:"admin" summary:"工具列表(全量)"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
ModuleKey string `json:"moduleKey"`
|
||||
Keyword string `json:"keyword"`
|
||||
IncludeDisabled bool `json:"includeDisabled"`
|
||||
}
|
||||
type ToolsListRes struct {
|
||||
List []AdminToolItem `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type ToolsSaveReq struct {
|
||||
g.Meta `path:"/tools/save" method:"post" tags:"admin" summary:"保存工具"`
|
||||
Id int64 `json:"id"`
|
||||
ToolKey string `v:"required-if:id,0#工具标识不能为空" json:"toolKey"`
|
||||
ModuleKey string `v:"required#所属模块不能为空" json:"moduleKey"`
|
||||
Name string `v:"required#工具名称不能为空" json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"isEnabled"`
|
||||
IsHot int `json:"isHot"`
|
||||
}
|
||||
type ToolsSaveRes struct{}
|
||||
|
||||
type ToolsToggleReq struct {
|
||||
g.Meta `path:"/tools/toggle" method:"post" tags:"admin" summary:"工具开关"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
Field string `v:"required|in:is_enabled,is_hot" json:"field"`
|
||||
Value int `json:"value"`
|
||||
}
|
||||
type ToolsToggleRes struct{}
|
||||
|
||||
// ===== 数据看板 =====
|
||||
|
||||
type TrendItem struct {
|
||||
Date string `json:"date"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type TopItem struct {
|
||||
ToolKey string `json:"tool_key"`
|
||||
Name string `json:"name"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
type DashboardStatsReq struct {
|
||||
g.Meta `path:"/dashboard/stats" method:"get" tags:"admin" summary:"看板统计"`
|
||||
}
|
||||
type DashboardStatsRes struct {
|
||||
UserCount int64 `json:"userCount"`
|
||||
TodayActive int64 `json:"todayActive"`
|
||||
TotalUsage int64 `json:"totalUsage"`
|
||||
WeekTrend []TrendItem `json:"weekTrend"`
|
||||
ToolsTop []TopItem `json:"toolsTop"`
|
||||
}
|
||||
|
||||
// ===== 反馈管理 =====
|
||||
|
||||
type FeedbackItem struct {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
Contact string `json:"contact"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
type FeedbackListReq struct {
|
||||
g.Meta `path:"/feedback/list" method:"get" tags:"admin" summary:"反馈列表"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"pageSize"`
|
||||
Status *int `json:"status"`
|
||||
}
|
||||
type FeedbackListRes struct {
|
||||
List []FeedbackItem `json:"list"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
type FeedbackHandleReq struct {
|
||||
g.Meta `path:"/feedback/handle" method:"post" tags:"admin" summary:"标记已处理"`
|
||||
Id int64 `v:"required" json:"id"`
|
||||
}
|
||||
type FeedbackHandleRes struct{}
|
||||
148
api/user/v1/user.go
Normal file
148
api/user/v1/user.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package v1
|
||||
|
||||
import "github.com/gogf/gf/v2/frame/g"
|
||||
|
||||
// ===== 公共结构 =====
|
||||
|
||||
type UserInfo struct {
|
||||
Id int64 `json:"id"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
type LevelInfo struct {
|
||||
LevelKey string `json:"level_key"`
|
||||
Name string `json:"name"`
|
||||
Modules []string `json:"modules"`
|
||||
}
|
||||
|
||||
// ===== 登录 =====
|
||||
|
||||
type WxLoginReq struct {
|
||||
g.Meta `path:"/user/wx-login" method:"post" tags:"user" summary:"微信登录"`
|
||||
Code string `v:"required#code不能为空" json:"code"`
|
||||
}
|
||||
type WxLoginRes struct {
|
||||
Token string `json:"token"`
|
||||
User *UserInfo `json:"user"`
|
||||
Level *LevelInfo `json:"level"`
|
||||
}
|
||||
|
||||
// DevLogin 仅 debug 配置开启时可用(H5/联调)
|
||||
type DevLoginReq struct {
|
||||
g.Meta `path:"/user/dev-login" method:"post" tags:"user" summary:"开发登录"`
|
||||
}
|
||||
type DevLoginRes = WxLoginRes
|
||||
|
||||
// ===== H5 账号注册/登录 =====
|
||||
|
||||
// Register H5 独立账号注册(注册即登录)
|
||||
type RegisterReq struct {
|
||||
g.Meta `path:"/user/register" method:"post" tags:"user" summary:"H5 账号注册"`
|
||||
Username string `v:"required|length:3,32#用户名不能为空|用户名长度为 3-32 位" json:"username"`
|
||||
Password string `v:"required|length:6,64#密码不能为空|密码长度为 6-64 位" json:"password"`
|
||||
Nickname string `v:"length:0,32#昵称最长 32 位" json:"nickname"`
|
||||
}
|
||||
type RegisterRes = WxLoginRes
|
||||
|
||||
// AccountLogin H5 账号密码登录
|
||||
type AccountLoginReq struct {
|
||||
g.Meta `path:"/user/login" method:"post" tags:"user" summary:"H5 账号登录"`
|
||||
Username string `v:"required#用户名不能为空" json:"username"`
|
||||
Password string `v:"required#密码不能为空" json:"password"`
|
||||
}
|
||||
type AccountLoginRes = WxLoginRes
|
||||
|
||||
// BindAccount 将一个 H5 独立账号合并进当前登录账号(两端共享数据)
|
||||
type BindAccountReq struct {
|
||||
g.Meta `path:"/user/bind-account" method:"post" tags:"user" summary:"绑定(合并)H5 账号"`
|
||||
Username string `v:"required#用户名不能为空" json:"username"`
|
||||
Password string `v:"required#密码不能为空" json:"password"`
|
||||
}
|
||||
type BindAccountRes struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// ===== 个人信息 =====
|
||||
|
||||
// ProfileUpdate 更新昵称 / 头像(avatarUrl 支持远端 URL 或预置头像 key:avatar:rocket 等)
|
||||
type ProfileUpdateReq struct {
|
||||
g.Meta `path:"/user/profile" method:"put" tags:"user" summary:"更新个人信息"`
|
||||
Nickname string `v:"required|length:1,32#昵称不能为空|昵称最长 32 位" json:"nickname"`
|
||||
AvatarUrl string `v:"length:0,512" json:"avatar_url"`
|
||||
}
|
||||
type ProfileUpdateRes struct {
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
// ===== 等级 =====
|
||||
|
||||
type LevelReq struct {
|
||||
g.Meta `path:"/user/level" method:"get" tags:"user" summary:"我的等级与模块"`
|
||||
}
|
||||
type LevelRes = LevelInfo
|
||||
|
||||
// UserInfoGet 当前登录用户信息(token 校验后恢复 user 用)
|
||||
type UserInfoGetReq struct {
|
||||
g.Meta `path:"/user/info" method:"get" tags:"user" summary:"我的信息"`
|
||||
}
|
||||
type UserInfoGetRes = UserInfo
|
||||
|
||||
// ===== 工具目录 =====
|
||||
|
||||
type ModuleOut struct {
|
||||
ModuleKey string `json:"module_key"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Locked bool `json:"locked"`
|
||||
EnabledToolsCount int `json:"enabled_tools_count"`
|
||||
}
|
||||
|
||||
type ToolOut struct {
|
||||
ToolKey string `json:"tool_key"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
IsHot bool `json:"is_hot"`
|
||||
Locked bool `json:"locked"`
|
||||
}
|
||||
|
||||
type ToolsListReq struct {
|
||||
g.Meta `path:"/tools/list" method:"get" tags:"user" summary:"工具目录(含锁定态)"`
|
||||
}
|
||||
type ToolsListRes struct {
|
||||
Modules []ModuleOut `json:"modules"`
|
||||
List []ToolOut `json:"list"`
|
||||
}
|
||||
|
||||
type ToolsUsedReq struct {
|
||||
g.Meta `path:"/tools/used" method:"post" tags:"user" summary:"工具使用上报"`
|
||||
ToolKey string `v:"required" json:"tool_key"`
|
||||
}
|
||||
type ToolsUsedRes struct{}
|
||||
|
||||
// ===== 工作台 =====
|
||||
|
||||
type WorkbenchGetReq struct {
|
||||
g.Meta `path:"/workbench/get" method:"get" tags:"user" summary:"我的工作台"`
|
||||
}
|
||||
type WorkbenchGetRes struct {
|
||||
ToolKeys []string `json:"tool_keys"`
|
||||
}
|
||||
|
||||
type WorkbenchSaveReq struct {
|
||||
g.Meta `path:"/workbench/save" method:"post" tags:"user" summary:"保存工作台"`
|
||||
ToolKeys []string `v:"required" json:"tool_keys"`
|
||||
}
|
||||
type WorkbenchSaveRes struct{}
|
||||
|
||||
// ===== 反馈 =====
|
||||
|
||||
type FeedbackSubmitReq struct {
|
||||
g.Meta `path:"/feedback/submit" method:"post" tags:"user" summary:"提交反馈"`
|
||||
Content string `v:"required#反馈内容不能为空" json:"content"`
|
||||
Contact string `json:"contact"`
|
||||
}
|
||||
type FeedbackSubmitRes struct{}
|
||||
41
go.mod
Normal file
41
go.mod
Normal file
@@ -0,0 +1,41 @@
|
||||
module tool-api
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0
|
||||
github.com/gogf/gf/v2 v2.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
golang.org/x/crypto v0.55.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.10 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/olekukonko/errors v1.1.0 // indirect
|
||||
github.com/olekukonko/ll v0.0.9 // indirect
|
||||
github.com/olekukonko/tablewriter v1.1.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.38.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
89
go.sum
Normal file
89
go.sum
Normal file
@@ -0,0 +1,89 @@
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha h1:dwFlh8pBg1VMOXWGipNMRt8v96dKAIvBehtCt6OtunU=
|
||||
github.com/emirpasic/gods/v2 v2.0.0-alpha/go.mod h1:W0y4M2dtBB9U5z3YlghmpuUhiaZT2h6yoeE+C1sCp6A=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0 h1:UvqxwinkelKxwdwnKUfdy51/ls4RL7MCeJqAZOVAy0I=
|
||||
github.com/gogf/gf/contrib/drivers/mysql/v2 v2.10.0/go.mod h1:6v7oGBF9wv59WERJIOJxXmLhkUcxwON3tPYW3AZ7wbY=
|
||||
github.com/gogf/gf/v2 v2.10.0 h1:rzDROlyqGMe/eM6dCalSR8dZOuMIdLhmxKSH1DGhbFs=
|
||||
github.com/gogf/gf/v2 v2.10.0/go.mod h1:Svl1N+E8G/QshU2DUbh/3J/AJauqCgUnxHurXWR4Qx0=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
|
||||
github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
|
||||
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
|
||||
github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
|
||||
github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
|
||||
github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
|
||||
github.com/olekukonko/tablewriter v1.1.0 h1:N0LHrshF4T39KvI96fn6GT8HEjXRXYNDrDjKFDB7RIY=
|
||||
github.com/olekukonko/tablewriter v1.1.0/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
27
hack/config.yaml
Normal file
27
hack/config.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
|
||||
# ============================================================
|
||||
# GoFrame CLI 工具配置(仅开发环境使用,gf 命令读取)
|
||||
# 文档:https://goframe.org/docs/cli
|
||||
# ============================================================
|
||||
|
||||
gfcli:
|
||||
gen:
|
||||
dao:
|
||||
# 生成 dao/entity/do 的数据库连接
|
||||
# 用法:cd tool-api && gf gen dao
|
||||
# 注意:与 manifest/config/config.yaml 的数据库保持同一套(当前:root/root@nl_toolbox)
|
||||
- link: "mysql:root:root@tcp(127.0.0.1:3306)/nl_toolbox"
|
||||
# 实体字段附带数据库字段描述注释
|
||||
descriptionTag: true
|
||||
# 只生成指定表(逗号分隔,留空 = 全部表),如 "users,tools"
|
||||
# tables: ""
|
||||
# 去除表名公共前缀,如表名 tb_users 生成 Users 时配 "tb_"
|
||||
# removePrefix: ""
|
||||
|
||||
docker:
|
||||
# docker 镜像构建参数:
|
||||
# -a 架构(amd64/arm64) -s 系统(linux) -p 临时目录前缀 -ew 排除文件
|
||||
build: "-a amd64 -s linux -p temp -ew"
|
||||
# 镜像 tag 前缀(推送到私有仓库时改成自己的 registry)
|
||||
tagPrefixes:
|
||||
- my.image.pub/my-app
|
||||
18
hack/hack-cli.mk
Normal file
18
hack/hack-cli.mk
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
# Install/Update to the latest CLI tool.
|
||||
.PHONY: cli
|
||||
cli:
|
||||
@set -e; \
|
||||
echo "go install github.com/gogf/gf/cmd/gf/v2@latest"; \
|
||||
go install github.com/gogf/gf/cmd/gf/v2@latest; \
|
||||
echo "GoFame CLI installed successfully!"
|
||||
|
||||
|
||||
# Check and install CLI tool.
|
||||
.PHONY: cli.install
|
||||
cli.install:
|
||||
@set -e; \
|
||||
gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \
|
||||
echo "GoFame CLI is not installed, start proceeding auto installation..."; \
|
||||
make cli; \
|
||||
fi;
|
||||
75
hack/hack.mk
Normal file
75
hack/hack.mk
Normal file
@@ -0,0 +1,75 @@
|
||||
.DEFAULT_GOAL := build
|
||||
|
||||
# Update GoFrame and its CLI to latest stable version.
|
||||
.PHONY: up
|
||||
up: cli.install
|
||||
@gf up -a
|
||||
|
||||
# Build binary using configuration from hack/config.yaml.
|
||||
.PHONY: build
|
||||
build: cli.install
|
||||
@gf build -ew
|
||||
|
||||
# Parse api and generate controller/sdk.
|
||||
.PHONY: ctrl
|
||||
ctrl: cli.install
|
||||
@gf gen ctrl
|
||||
|
||||
# Generate Go files for DAO/DO/Entity.
|
||||
.PHONY: dao
|
||||
dao: cli.install
|
||||
@gf gen dao
|
||||
|
||||
# Parse current project go files and generate enums go file.
|
||||
.PHONY: enums
|
||||
enums: cli.install
|
||||
@gf gen enums
|
||||
|
||||
# Generate Go files for Service.
|
||||
.PHONY: service
|
||||
service: cli.install
|
||||
@gf gen service
|
||||
|
||||
|
||||
# Build docker image.
|
||||
.PHONY: image
|
||||
image: cli.install
|
||||
$(eval _TAG = $(shell git rev-parse --short HEAD))
|
||||
ifneq (, $(shell git status --porcelain 2>/dev/null))
|
||||
$(eval _TAG = $(_TAG).dirty)
|
||||
endif
|
||||
$(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG)))
|
||||
$(eval _PUSH = $(if ${PUSH}, ${PUSH}, ))
|
||||
@gf docker ${_PUSH} -tn $(DOCKER_NAME):${_TAG};
|
||||
|
||||
|
||||
# Build docker image and automatically push to docker repo.
|
||||
.PHONY: image.push
|
||||
image.push: cli.install
|
||||
@make image PUSH=-p;
|
||||
|
||||
|
||||
# Deploy image and yaml to current kubectl environment.
|
||||
.PHONY: deploy
|
||||
deploy: cli.install
|
||||
$(eval _TAG = $(if ${TAG}, ${TAG}, develop))
|
||||
|
||||
@set -e; \
|
||||
mkdir -p $(ROOT_DIR)/temp/kustomize;\
|
||||
cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\
|
||||
kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\
|
||||
kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \
|
||||
if [ $(DEPLOY_NAME) != "" ]; then \
|
||||
kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; \
|
||||
fi;
|
||||
|
||||
|
||||
# Parsing protobuf files and generating go files.
|
||||
.PHONY: pb
|
||||
pb: cli.install
|
||||
@gf gen pb
|
||||
|
||||
# Generate protobuf files for database tables.
|
||||
.PHONY: pbentity
|
||||
pbentity: cli.install
|
||||
@gf gen pbentity
|
||||
44
internal/cmd/cmd.go
Normal file
44
internal/cmd/cmd.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
_ "github.com/gogf/gf/contrib/drivers/mysql/v2"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gcmd"
|
||||
|
||||
"tool-api/internal/controller"
|
||||
"tool-api/internal/logic"
|
||||
)
|
||||
|
||||
var (
|
||||
Main = gcmd.Command{
|
||||
Name: "main",
|
||||
Usage: "main",
|
||||
Brief: "start http server",
|
||||
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
|
||||
// 启动时幂等写入种子数据(管理员/模块/等级/工具),数据库不可用时仅告警
|
||||
logic.Seed(ctx)
|
||||
|
||||
s := g.Server()
|
||||
// 公开接口:小程序登录、开发登录、管理端登录
|
||||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(logic.CORS, logic.Response)
|
||||
group.Bind(controller.UserPub, controller.AdminPub)
|
||||
})
|
||||
// 小程序用户接口
|
||||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(logic.CORS, logic.Response, logic.UserAuth)
|
||||
group.Bind(controller.UserAuth)
|
||||
})
|
||||
// 管理端接口
|
||||
s.Group("/", func(group *ghttp.RouterGroup) {
|
||||
group.Middleware(logic.CORS, logic.Response, logic.AdminAuth)
|
||||
group.Bind(controller.AdminAuth)
|
||||
})
|
||||
s.Run()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
)
|
||||
31
internal/consts/consts.go
Normal file
31
internal/consts/consts.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package consts
|
||||
|
||||
// JWT audience
|
||||
const (
|
||||
AudUser = "user"
|
||||
AudAdmin = "admin"
|
||||
)
|
||||
|
||||
// 请求上下文变量(中间件 SetCtxVar)
|
||||
const (
|
||||
CtxUserId = "userId"
|
||||
CtxLevelKey = "levelKey"
|
||||
CtxAdminId = "adminId"
|
||||
)
|
||||
|
||||
// 业务错误码(响应 code 字段)
|
||||
const (
|
||||
CodeLocked = 4001 // 模块未授权(等级锁定)
|
||||
)
|
||||
|
||||
// 表名
|
||||
const (
|
||||
TableUsers = "users"
|
||||
TableModules = "modules"
|
||||
TableLevels = "levels"
|
||||
TableTools = "tools"
|
||||
TableUsageLogs = "tool_usage_logs"
|
||||
TableWorkbench = "user_workbench"
|
||||
TableAdminUsers = "admin_users"
|
||||
TableFeedbacks = "feedbacks"
|
||||
)
|
||||
79
internal/controller/admin.go
Normal file
79
internal/controller/admin.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "tool-api/api/admin/v1"
|
||||
"tool-api/internal/logic"
|
||||
)
|
||||
|
||||
// AdminPub 管理端公开接口(登录)
|
||||
type cAdminPub struct{}
|
||||
|
||||
// AdminAuth 管理端需登录接口
|
||||
type cAdminAuth struct{}
|
||||
|
||||
var (
|
||||
AdminPub = &cAdminPub{}
|
||||
AdminAuth = &cAdminAuth{}
|
||||
)
|
||||
|
||||
func (c *cAdminPub) Login(ctx context.Context, req *v1.LoginReq) (res *v1.LoginRes, err error) {
|
||||
return logic.AdminLogin(ctx, req.Account, req.Password)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) MyInfo(ctx context.Context, req *v1.MyInfoReq) (res *v1.MyInfoRes, err error) {
|
||||
return logic.AdminMyInfo(ctx)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) UserList(ctx context.Context, req *v1.UserListReq) (res *v1.UserListRes, err error) {
|
||||
return logic.AdminUserList(ctx, req)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) UserSetLevel(ctx context.Context, req *v1.UserSetLevelReq) (res *v1.UserSetLevelRes, err error) {
|
||||
return logic.AdminUserSetLevel(ctx, req.UserId, req.LevelKey)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) UserStatus(ctx context.Context, req *v1.UserStatusReq) (res *v1.UserStatusRes, err error) {
|
||||
return logic.AdminUserStatus(ctx, req.UserId, req.Status)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) LevelList(ctx context.Context, req *v1.LevelListReq) (res *v1.LevelListRes, err error) {
|
||||
return logic.AdminLevelList(ctx)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) LevelSave(ctx context.Context, req *v1.LevelSaveReq) (res *v1.LevelSaveRes, err error) {
|
||||
return logic.AdminLevelSave(ctx, req)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) ModuleList(ctx context.Context, req *v1.ModuleListReq) (res *v1.ModuleListRes, err error) {
|
||||
return logic.AdminModuleList(ctx)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) ModuleSave(ctx context.Context, req *v1.ModuleSaveReq) (res *v1.ModuleSaveRes, err error) {
|
||||
return logic.AdminModuleSave(ctx, req)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) ToolsList(ctx context.Context, req *v1.ToolsListReq) (res *v1.ToolsListRes, err error) {
|
||||
return logic.AdminToolsList(ctx, req)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) ToolsSave(ctx context.Context, req *v1.ToolsSaveReq) (res *v1.ToolsSaveRes, err error) {
|
||||
return logic.AdminToolsSave(ctx, req)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) ToolsToggle(ctx context.Context, req *v1.ToolsToggleReq) (res *v1.ToolsToggleRes, err error) {
|
||||
return logic.AdminToolsToggle(ctx, req.Id, req.Field, req.Value)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) DashboardStats(ctx context.Context, req *v1.DashboardStatsReq) (res *v1.DashboardStatsRes, err error) {
|
||||
return logic.AdminDashboardStats(ctx)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) FeedbackList(ctx context.Context, req *v1.FeedbackListReq) (res *v1.FeedbackListRes, err error) {
|
||||
return logic.AdminFeedbackList(ctx, req)
|
||||
}
|
||||
|
||||
func (c *cAdminAuth) FeedbackHandle(ctx context.Context, req *v1.FeedbackHandleReq) (res *v1.FeedbackHandleRes, err error) {
|
||||
return logic.AdminFeedbackHandle(ctx, req.Id)
|
||||
}
|
||||
71
internal/controller/user.go
Normal file
71
internal/controller/user.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
v1 "tool-api/api/user/v1"
|
||||
"tool-api/internal/logic"
|
||||
)
|
||||
|
||||
// UserPub 公开接口(免登录)
|
||||
type cUserPub struct{}
|
||||
|
||||
// UserAuth 需要用户登录的接口
|
||||
type cUserAuth struct{}
|
||||
|
||||
var (
|
||||
UserPub = &cUserPub{}
|
||||
UserAuth = &cUserAuth{}
|
||||
)
|
||||
|
||||
func (c *cUserPub) WxLogin(ctx context.Context, req *v1.WxLoginReq) (res *v1.WxLoginRes, err error) {
|
||||
return logic.WxLogin(ctx, req.Code)
|
||||
}
|
||||
|
||||
func (c *cUserPub) DevLogin(ctx context.Context, req *v1.DevLoginReq) (res *v1.DevLoginRes, err error) {
|
||||
return logic.DevLogin(ctx)
|
||||
}
|
||||
|
||||
func (c *cUserPub) Register(ctx context.Context, req *v1.RegisterReq) (res *v1.RegisterRes, err error) {
|
||||
return logic.Register(ctx, req.Username, req.Password, req.Nickname)
|
||||
}
|
||||
|
||||
func (c *cUserPub) AccountLogin(ctx context.Context, req *v1.AccountLoginReq) (res *v1.AccountLoginRes, err error) {
|
||||
return logic.AccountLogin(ctx, req.Username, req.Password)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) BindAccount(ctx context.Context, req *v1.BindAccountReq) (res *v1.BindAccountRes, err error) {
|
||||
return logic.BindAccount(ctx, req.Username, req.Password)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) ProfileUpdate(ctx context.Context, req *v1.ProfileUpdateReq) (res *v1.ProfileUpdateRes, err error) {
|
||||
return logic.ProfileUpdate(ctx, req.Nickname, req.AvatarUrl)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) Level(ctx context.Context, req *v1.LevelReq) (res *v1.LevelRes, err error) {
|
||||
return logic.UserLevel(ctx)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) UserInfoGet(ctx context.Context, req *v1.UserInfoGetReq) (res *v1.UserInfoGetRes, err error) {
|
||||
return logic.GetUserInfo(ctx)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) ToolsList(ctx context.Context, req *v1.ToolsListReq) (res *v1.ToolsListRes, err error) {
|
||||
return logic.ToolsList(ctx)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) ToolsUsed(ctx context.Context, req *v1.ToolsUsedReq) (res *v1.ToolsUsedRes, err error) {
|
||||
return logic.ToolsUsed(ctx, req.ToolKey)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) WorkbenchGet(ctx context.Context, req *v1.WorkbenchGetReq) (res *v1.WorkbenchGetRes, err error) {
|
||||
return logic.WorkbenchGet(ctx)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) WorkbenchSave(ctx context.Context, req *v1.WorkbenchSaveReq) (res *v1.WorkbenchSaveRes, err error) {
|
||||
return logic.WorkbenchSave(ctx, req.ToolKeys)
|
||||
}
|
||||
|
||||
func (c *cUserAuth) FeedbackSubmit(ctx context.Context, req *v1.FeedbackSubmitReq) (res *v1.FeedbackSubmitRes, err error) {
|
||||
return logic.FeedbackSubmit(ctx, req.Content, req.Contact)
|
||||
}
|
||||
0
internal/dao/.gitkeep
Normal file
0
internal/dao/.gitkeep
Normal file
476
internal/logic/admin.go
Normal file
476
internal/logic/admin.go
Normal file
@@ -0,0 +1,476 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
v1 "tool-api/api/admin/v1"
|
||||
"tool-api/internal/consts"
|
||||
)
|
||||
|
||||
// ===== 登录 =====
|
||||
|
||||
func AdminLogin(ctx context.Context, account, password string) (*v1.LoginRes, error) {
|
||||
record, err := g.Model(consts.TableAdminUsers).Where("account", account).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
return nil, gerror.New("账号或密码错误")
|
||||
}
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(record["password"].String()), []byte(password)); err != nil {
|
||||
return nil, gerror.New("账号或密码错误")
|
||||
}
|
||||
nickName := record["nick_name"].String()
|
||||
roleName := record["role_name"].String()
|
||||
roleValue := record["role_value"].String()
|
||||
token, err := IssueToken(ctx, consts.AudAdmin, record["id"].Int64(), map[string]string{
|
||||
"nickName": nickName,
|
||||
"roleName": roleName,
|
||||
"roleValue": roleValue,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.LoginRes{Token: token, NickName: nickName, RoleName: roleName, RoleValue: roleValue}, nil
|
||||
}
|
||||
|
||||
func AdminMyInfo(ctx context.Context) (*v1.MyInfoRes, error) {
|
||||
record, err := g.Model(consts.TableAdminUsers).Where("id", CtxAdminId(ctx)).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
return nil, gerror.NewCode(gcodeUnauthorized(), "管理员不存在")
|
||||
}
|
||||
return &v1.MyInfoRes{
|
||||
Id: record["id"].Int64(),
|
||||
Account: record["account"].String(),
|
||||
NickName: record["nick_name"].String(),
|
||||
RoleName: record["role_name"].String(),
|
||||
RoleValue: record["role_value"].String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ===== 用户管理 =====
|
||||
|
||||
func AdminUserList(ctx context.Context, req *v1.UserListReq) (*v1.UserListRes, error) {
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
m := g.Model(consts.TableUsers)
|
||||
if req.Keyword != "" {
|
||||
kw := "%" + req.Keyword + "%"
|
||||
m = m.Where("nickname LIKE ? OR openid LIKE ?", kw, kw)
|
||||
}
|
||||
total, err := m.Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records, err := m.Page(req.Page, req.PageSize).Order("id desc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
levelNames, err := levelNameMap(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]v1.AdminUserItem, 0, len(records))
|
||||
for _, r := range records {
|
||||
item := v1.AdminUserItem{
|
||||
Id: r["id"].Int64(),
|
||||
Nickname: r["nickname"].String(),
|
||||
AvatarUrl: r["avatar_url"].String(),
|
||||
Openid: r["openid"].String(),
|
||||
LevelKey: r["level_key"].String(),
|
||||
LevelName: levelNames[r["level_key"].String()],
|
||||
Status: r["status"].Int(),
|
||||
CreatedAt: r["created_at"].String(),
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
// 附加使用统计
|
||||
if len(list) > 0 {
|
||||
ids := make([]int64, 0, len(list))
|
||||
for _, u := range list {
|
||||
ids = append(ids, u.Id)
|
||||
}
|
||||
stats, err := g.Model(consts.TableUsageLogs).
|
||||
Fields("user_id, COUNT(*) AS c, MAX(used_at) AS last_used").
|
||||
WhereIn("user_id", ids).Group("user_id").All()
|
||||
if err == nil {
|
||||
statMap := map[int64]gdb.Record{}
|
||||
for _, s := range stats {
|
||||
statMap[s["user_id"].Int64()] = s
|
||||
}
|
||||
for i := range list {
|
||||
if s, ok := statMap[list[i].Id]; ok {
|
||||
list[i].UsageCount = s["c"].Int64()
|
||||
list[i].LastUsedAt = s["last_used"].String()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &v1.UserListRes{List: list, Total: total}, nil
|
||||
}
|
||||
|
||||
func levelNameMap(ctx context.Context) (map[string]string, error) {
|
||||
records, err := g.Model(consts.TableLevels).All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]string{}
|
||||
for _, r := range records {
|
||||
m[r["level_key"].String()] = r["name"].String()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func AdminUserSetLevel(ctx context.Context, userId int64, levelKey string) (*v1.UserSetLevelRes, error) {
|
||||
level, err := g.Model(consts.TableLevels).Where("level_key", levelKey).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if level.IsEmpty() {
|
||||
return nil, gerror.New("等级不存在")
|
||||
}
|
||||
if _, err = g.Model(consts.TableUsers).Where("id", userId).
|
||||
Data(g.Map{"level_key": levelKey}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.UserSetLevelRes{}, nil
|
||||
}
|
||||
|
||||
func AdminUserStatus(ctx context.Context, userId int64, status int) (*v1.UserStatusRes, error) {
|
||||
if _, err := g.Model(consts.TableUsers).Where("id", userId).
|
||||
Data(g.Map{"status": status}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.UserStatusRes{}, nil
|
||||
}
|
||||
|
||||
// ===== 等级管理 =====
|
||||
|
||||
func AdminLevelList(ctx context.Context) (*v1.LevelListRes, error) {
|
||||
records, err := g.Model(consts.TableLevels).Order("sort asc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]v1.LevelItem, 0, len(records))
|
||||
for _, r := range records {
|
||||
modules := []string{}
|
||||
_ = json.Unmarshal([]byte(r["modules"].String()), &modules)
|
||||
list = append(list, v1.LevelItem{
|
||||
Id: r["id"].Int64(),
|
||||
LevelKey: r["level_key"].String(),
|
||||
Name: r["name"].String(),
|
||||
Modules: modules,
|
||||
Sort: r["sort"].Int(),
|
||||
IsEnabled: r["is_enabled"].Int(),
|
||||
Remark: r["remark"].String(),
|
||||
})
|
||||
}
|
||||
return &v1.LevelListRes{List: list}, nil
|
||||
}
|
||||
|
||||
func AdminLevelSave(ctx context.Context, req *v1.LevelSaveReq) (*v1.LevelSaveRes, error) {
|
||||
modulesJson, _ := json.Marshal(req.Modules)
|
||||
data := g.Map{
|
||||
"name": req.Name,
|
||||
"modules": string(modulesJson),
|
||||
"sort": req.Sort,
|
||||
"is_enabled": req.IsEnabled,
|
||||
"remark": req.Remark,
|
||||
}
|
||||
if req.Id > 0 {
|
||||
// 编辑:level_key 只读,不修改
|
||||
if _, err := g.Model(consts.TableLevels).Where("id", req.Id).Data(data).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
count, err := g.Model(consts.TableLevels).Where("level_key", req.LevelKey).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, gerror.New("等级标识已存在")
|
||||
}
|
||||
data["level_key"] = req.LevelKey
|
||||
if _, err := g.Model(consts.TableLevels).Data(data).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &v1.LevelSaveRes{}, nil
|
||||
}
|
||||
|
||||
// ===== 模块管理 =====
|
||||
|
||||
func AdminModuleList(ctx context.Context) (*v1.ModuleListRes, error) {
|
||||
records, err := g.Model(consts.TableModules).Order("sort asc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts, err := g.Model(consts.TableTools).Fields("module_key, COUNT(*) AS c").Group("module_key").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
countMap := map[string]int{}
|
||||
for _, c := range counts {
|
||||
countMap[c["module_key"].String()] = c["c"].Int()
|
||||
}
|
||||
list := make([]v1.ModuleItem, 0, len(records))
|
||||
for _, r := range records {
|
||||
key := r["module_key"].String()
|
||||
list = append(list, v1.ModuleItem{
|
||||
Id: r["id"].Int64(),
|
||||
ModuleKey: key,
|
||||
Name: r["name"].String(),
|
||||
Icon: r["icon"].String(),
|
||||
Description: r["description"].String(),
|
||||
Sort: r["sort"].Int(),
|
||||
IsEnabled: r["is_enabled"].Int(),
|
||||
ToolCount: countMap[key],
|
||||
})
|
||||
}
|
||||
return &v1.ModuleListRes{List: list}, nil
|
||||
}
|
||||
|
||||
func AdminModuleSave(ctx context.Context, req *v1.ModuleSaveReq) (*v1.ModuleSaveRes, error) {
|
||||
data := g.Map{
|
||||
"name": req.Name,
|
||||
"icon": req.Icon,
|
||||
"description": req.Description,
|
||||
"sort": req.Sort,
|
||||
"is_enabled": req.IsEnabled,
|
||||
}
|
||||
if req.Id > 0 {
|
||||
if _, err := g.Model(consts.TableModules).Where("id", req.Id).Data(data).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
count, err := g.Model(consts.TableModules).Where("module_key", req.ModuleKey).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, gerror.New("模块标识已存在")
|
||||
}
|
||||
data["module_key"] = req.ModuleKey
|
||||
if _, err := g.Model(consts.TableModules).Data(data).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &v1.ModuleSaveRes{}, nil
|
||||
}
|
||||
|
||||
// ===== 工具管理 =====
|
||||
|
||||
func AdminToolsList(ctx context.Context, req *v1.ToolsListReq) (*v1.ToolsListRes, error) {
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 50
|
||||
}
|
||||
m := g.Model(consts.TableTools)
|
||||
if req.ModuleKey != "" {
|
||||
m = m.Where("module_key", req.ModuleKey)
|
||||
}
|
||||
if req.Keyword != "" {
|
||||
kw := "%" + req.Keyword + "%"
|
||||
m = m.Where("name LIKE ? OR tool_key LIKE ?", kw, kw)
|
||||
}
|
||||
if !req.IncludeDisabled {
|
||||
m = m.Where("is_enabled", 1)
|
||||
}
|
||||
total, err := m.Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records, err := m.Page(req.Page, req.PageSize).Order("sort asc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
moduleNames, err := moduleNameMap(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]v1.AdminToolItem, 0, len(records))
|
||||
for _, r := range records {
|
||||
mk := r["module_key"].String()
|
||||
list = append(list, v1.AdminToolItem{
|
||||
Id: r["id"].Int64(),
|
||||
ToolKey: r["tool_key"].String(),
|
||||
ModuleKey: mk,
|
||||
ModuleName: moduleNames[mk],
|
||||
Name: r["name"].String(),
|
||||
Icon: r["icon"].String(),
|
||||
Description: r["description"].String(),
|
||||
Sort: r["sort"].Int(),
|
||||
IsEnabled: r["is_enabled"].Int(),
|
||||
IsHot: r["is_hot"].Int(),
|
||||
UsageCount: r["usage_count"].Int64(),
|
||||
})
|
||||
}
|
||||
return &v1.ToolsListRes{List: list, Total: total}, nil
|
||||
}
|
||||
|
||||
func moduleNameMap(ctx context.Context) (map[string]string, error) {
|
||||
records, err := g.Model(consts.TableModules).All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]string{}
|
||||
for _, r := range records {
|
||||
m[r["module_key"].String()] = r["name"].String()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func AdminToolsSave(ctx context.Context, req *v1.ToolsSaveReq) (*v1.ToolsSaveRes, error) {
|
||||
data := g.Map{
|
||||
"module_key": req.ModuleKey,
|
||||
"name": req.Name,
|
||||
"icon": req.Icon,
|
||||
"description": req.Description,
|
||||
"sort": req.Sort,
|
||||
"is_enabled": req.IsEnabled,
|
||||
"is_hot": req.IsHot,
|
||||
}
|
||||
if req.Id > 0 {
|
||||
// 编辑:tool_key 只读,不修改
|
||||
if _, err := g.Model(consts.TableTools).Where("id", req.Id).Data(data).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
count, err := g.Model(consts.TableTools).Where("tool_key", req.ToolKey).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, gerror.New("工具标识已存在")
|
||||
}
|
||||
data["tool_key"] = req.ToolKey
|
||||
if _, err := g.Model(consts.TableTools).Data(data).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &v1.ToolsSaveRes{}, nil
|
||||
}
|
||||
|
||||
func AdminToolsToggle(ctx context.Context, id int64, field string, value int) (*v1.ToolsToggleRes, error) {
|
||||
if field != "is_enabled" && field != "is_hot" {
|
||||
return nil, gerror.New("不支持的字段")
|
||||
}
|
||||
if _, err := g.Model(consts.TableTools).Where("id", id).Data(g.Map{field: value}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.ToolsToggleRes{}, nil
|
||||
}
|
||||
|
||||
// ===== 数据看板 =====
|
||||
|
||||
func AdminDashboardStats(ctx context.Context) (*v1.DashboardStatsRes, error) {
|
||||
res := &v1.DashboardStatsRes{WeekTrend: []v1.TrendItem{}, ToolsTop: []v1.TopItem{}}
|
||||
if c, err := g.Model(consts.TableUsers).Count(); err == nil {
|
||||
res.UserCount = int64(c)
|
||||
}
|
||||
if c, err := g.Model(consts.TableUsageLogs).Count(); err == nil {
|
||||
res.TotalUsage = int64(c)
|
||||
}
|
||||
// 今日活跃:今天有使用记录或登录过的用户数
|
||||
record, err := g.DB().GetOne(ctx,
|
||||
"SELECT COUNT(*) AS c FROM users u WHERE DATE(u.last_login_at) = CURDATE() "+
|
||||
"OR EXISTS (SELECT 1 FROM tool_usage_logs l WHERE l.user_id = u.id AND DATE(l.used_at) = CURDATE())")
|
||||
if err == nil && !record.IsEmpty() {
|
||||
res.TodayActive = record["c"].Int64()
|
||||
}
|
||||
// 7 日使用趋势(缺失日期补 0)
|
||||
trend, err := g.DB().GetAll(ctx,
|
||||
"SELECT DATE(used_at) AS d, COUNT(*) AS c FROM tool_usage_logs "+
|
||||
"WHERE used_at >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) GROUP BY d")
|
||||
if err == nil {
|
||||
countByDate := map[string]int64{}
|
||||
for _, r := range trend {
|
||||
countByDate[r["d"].String()] = r["c"].Int64()
|
||||
}
|
||||
for i := 6; i >= 0; i-- {
|
||||
day := time.Now().AddDate(0, 0, -i).Format("2006-01-02")
|
||||
res.WeekTrend = append(res.WeekTrend, v1.TrendItem{Date: day, Count: countByDate[day]})
|
||||
}
|
||||
}
|
||||
// 工具使用 TOP10
|
||||
top, err := g.DB().GetAll(ctx,
|
||||
"SELECT tool_key, COUNT(*) AS c FROM tool_usage_logs GROUP BY tool_key ORDER BY c DESC LIMIT 10")
|
||||
if err == nil {
|
||||
names, _ := toolNameMap(ctx)
|
||||
for _, r := range top {
|
||||
key := r["tool_key"].String()
|
||||
res.ToolsTop = append(res.ToolsTop, v1.TopItem{ToolKey: key, Name: names[key], Count: r["c"].Int64()})
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func toolNameMap(ctx context.Context) (map[string]string, error) {
|
||||
records, err := g.Model(consts.TableTools).All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := map[string]string{}
|
||||
for _, r := range records {
|
||||
m[r["tool_key"].String()] = r["name"].String()
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ===== 反馈管理 =====
|
||||
|
||||
func AdminFeedbackList(ctx context.Context, req *v1.FeedbackListReq) (*v1.FeedbackListRes, error) {
|
||||
if req.Page <= 0 {
|
||||
req.Page = 1
|
||||
}
|
||||
if req.PageSize <= 0 {
|
||||
req.PageSize = 20
|
||||
}
|
||||
m := g.Model(consts.TableFeedbacks)
|
||||
if req.Status != nil {
|
||||
m = m.Where("status", *req.Status)
|
||||
}
|
||||
total, err := m.Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
records, err := m.Page(req.Page, req.PageSize).Order("id desc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
list := make([]v1.FeedbackItem, 0, len(records))
|
||||
for _, r := range records {
|
||||
list = append(list, v1.FeedbackItem{
|
||||
Id: r["id"].Int64(),
|
||||
UserId: r["user_id"].Int64(),
|
||||
Content: r["content"].String(),
|
||||
Contact: r["contact"].String(),
|
||||
Status: r["status"].Int(),
|
||||
CreatedAt: r["created_at"].String(),
|
||||
})
|
||||
}
|
||||
return &v1.FeedbackListRes{List: list, Total: total}, nil
|
||||
}
|
||||
|
||||
func AdminFeedbackHandle(ctx context.Context, id int64) (*v1.FeedbackHandleRes, error) {
|
||||
if _, err := g.Model(consts.TableFeedbacks).Where("id", id).Data(g.Map{"status": 1}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.FeedbackHandleRes{}, nil
|
||||
}
|
||||
121
internal/logic/jwt.go
Normal file
121
internal/logic/jwt.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
|
||||
"tool-api/internal/consts"
|
||||
)
|
||||
|
||||
type jwtClaims struct {
|
||||
UserId int64 `json:"userId,omitempty"`
|
||||
LevelKey string `json:"levelKey,omitempty"`
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
RoleName string `json:"roleName,omitempty"`
|
||||
RoleValue string `json:"roleValue,omitempty"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func jwtSecret(ctx context.Context) []byte {
|
||||
return []byte(g.Cfg().MustGet(ctx, "jwt.secret", "tool-box-secret-change-me").String())
|
||||
}
|
||||
|
||||
// IssueToken 签发 JWT;aud 区分 user / admin
|
||||
func IssueToken(ctx context.Context, aud string, userId int64, extra map[string]string) (string, error) {
|
||||
hours := g.Cfg().MustGet(ctx, "jwt.expireHours", 168).Int()
|
||||
claims := jwtClaims{
|
||||
UserId: userId,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Audience: jwt.ClaimStrings{aud},
|
||||
Issuer: "tool-api",
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(hours) * time.Hour)),
|
||||
},
|
||||
}
|
||||
for k, v := range extra {
|
||||
switch k {
|
||||
case "levelKey":
|
||||
claims.LevelKey = v
|
||||
case "nickName":
|
||||
claims.NickName = v
|
||||
case "roleName":
|
||||
claims.RoleName = v
|
||||
case "roleValue":
|
||||
claims.RoleValue = v
|
||||
}
|
||||
}
|
||||
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString(jwtSecret(ctx))
|
||||
}
|
||||
|
||||
func ParseToken(tokenStr, aud string) (*jwtClaims, error) {
|
||||
claims := &jwtClaims{}
|
||||
_, err := jwt.ParseWithClaims(tokenStr, claims, func(t *jwt.Token) (interface{}, error) {
|
||||
return jwtSecret(gctx.GetInitCtx()), nil
|
||||
}, jwt.WithValidMethods([]string{"HS256"}), jwt.WithAudience(aud))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func parseBearer(r *ghttp.Request, aud string) (*jwtClaims, error) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
tokenStr := strings.TrimPrefix(auth, "Bearer ")
|
||||
if tokenStr == "" {
|
||||
return nil, errors.New("empty token")
|
||||
}
|
||||
return ParseToken(tokenStr, aud)
|
||||
}
|
||||
|
||||
func writeAuthFail(r *ghttp.Request) {
|
||||
r.Response.WriteJson(g.Map{"code": 401, "message": "登录已失效,请重新登录", "result": nil})
|
||||
r.Exit()
|
||||
}
|
||||
|
||||
// UserAuth 小程序用户鉴权
|
||||
func UserAuth(r *ghttp.Request) {
|
||||
claims, err := parseBearer(r, consts.AudUser)
|
||||
if err != nil {
|
||||
writeAuthFail(r)
|
||||
return
|
||||
}
|
||||
r.SetCtxVar(consts.CtxUserId, claims.UserId)
|
||||
r.SetCtxVar(consts.CtxLevelKey, claims.LevelKey)
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// AdminAuth 管理端鉴权
|
||||
func AdminAuth(r *ghttp.Request) {
|
||||
claims, err := parseBearer(r, consts.AudAdmin)
|
||||
if err != nil {
|
||||
writeAuthFail(r)
|
||||
return
|
||||
}
|
||||
r.SetCtxVar(consts.CtxAdminId, claims.UserId)
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// ===== 业务上下文取值 =====
|
||||
|
||||
func CtxUserId(ctx context.Context) int64 {
|
||||
return g.RequestFromCtx(ctx).GetCtxVar(consts.CtxUserId).Int64()
|
||||
}
|
||||
|
||||
func CtxLevelKey(ctx context.Context) string {
|
||||
return g.RequestFromCtx(ctx).GetCtxVar(consts.CtxLevelKey).String()
|
||||
}
|
||||
|
||||
func CtxAdminId(ctx context.Context) int64 {
|
||||
return g.RequestFromCtx(ctx).GetCtxVar(consts.CtxAdminId).Int64()
|
||||
}
|
||||
|
||||
func IsDebug(ctx context.Context) bool {
|
||||
return g.Cfg().MustGet(ctx, "debug", false).Bool()
|
||||
}
|
||||
44
internal/logic/middleware.go
Normal file
44
internal/logic/middleware.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/net/ghttp"
|
||||
)
|
||||
|
||||
// CORS 允许跨域(管理后台与 H5 联调)
|
||||
func CORS(r *ghttp.Request) {
|
||||
r.Response.CORSDefault()
|
||||
if r.Method == "OPTIONS" {
|
||||
r.Exit()
|
||||
}
|
||||
r.Middleware.Next()
|
||||
}
|
||||
|
||||
// Response 统一响应封装:{code, message, result}
|
||||
// 前端约定:code=0 成功;401 未登录;4001 模块未授权。
|
||||
func Response(r *ghttp.Request) {
|
||||
r.Middleware.Next()
|
||||
// 鉴权中间件等已直接写响应(如 401),不再二次包装
|
||||
if r.Response.BufferLength() > 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
err = r.GetError()
|
||||
res = r.GetHandlerResponse()
|
||||
)
|
||||
if err != nil {
|
||||
code := gerror.Code(err)
|
||||
codeInt := code.Code()
|
||||
if code == gcode.CodeNil {
|
||||
codeInt = 500
|
||||
} else if code == gcode.CodeValidationFailed {
|
||||
codeInt = 422
|
||||
}
|
||||
r.Response.WriteJson(g.Map{"code": codeInt, "message": err.Error(), "result": nil})
|
||||
return
|
||||
}
|
||||
r.Response.WriteJson(g.Map{"code": 0, "message": "ok", "result": res})
|
||||
}
|
||||
100
internal/logic/seed.go
Normal file
100
internal/logic/seed.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"tool-api/internal/consts"
|
||||
)
|
||||
|
||||
// Seed 启动时幂等写入种子数据:管理员、模块、等级、工具目录。
|
||||
// 数据库未就绪时仅告警,不阻断服务启动。
|
||||
func Seed(ctx context.Context) {
|
||||
if err := seedAdmin(ctx); err != nil {
|
||||
g.Log().Warning(ctx, "[seed] admin:", err)
|
||||
}
|
||||
if err := seedRows(ctx, consts.TableModules, "module_key", seedModules()); err != nil {
|
||||
g.Log().Warning(ctx, "[seed] modules:", err)
|
||||
}
|
||||
if err := seedRows(ctx, consts.TableLevels, "level_key", seedLevels()); err != nil {
|
||||
g.Log().Warning(ctx, "[seed] levels:", err)
|
||||
}
|
||||
if err := seedRows(ctx, consts.TableTools, "tool_key", seedTools()); err != nil {
|
||||
g.Log().Warning(ctx, "[seed] tools:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func seedAdmin(ctx context.Context) error {
|
||||
count, err := g.Model(consts.TableAdminUsers).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = g.Model(consts.TableAdminUsers).Data(g.Map{
|
||||
"account": "admin",
|
||||
"password": string(hash),
|
||||
"nick_name": "管理员",
|
||||
"role_name": "超级管理员",
|
||||
"role_value": "super",
|
||||
}).Insert()
|
||||
return err
|
||||
}
|
||||
|
||||
// seedRows 按 uniqueKey 幂等插入(存在即跳过该行)
|
||||
func seedRows(ctx context.Context, table, uniqueKey string, rows []g.Map) error {
|
||||
for _, row := range rows {
|
||||
count, err := g.Model(table).Where(uniqueKey, row[uniqueKey]).Count()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
continue
|
||||
}
|
||||
if _, err := g.Model(table).Data(row).Insert(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedModules() []g.Map {
|
||||
return []g.Map{
|
||||
{"module_key": "image", "name": "图片处理", "icon": "picture", "description": "压缩、转换、抠图等图片工具", "sort": 1},
|
||||
{"module_key": "doc", "name": "文档处理", "icon": "description", "description": "文档转换与处理(规划中)", "sort": 2},
|
||||
{"module_key": "calc", "name": "计算换算", "icon": "computer", "description": "单位换算、房贷计算等", "sort": 3},
|
||||
{"module_key": "time", "name": "时间效率", "icon": "time", "description": "番茄钟、倒数日等", "sort": 4},
|
||||
{"module_key": "create", "name": "生成制作", "icon": "edit-outline", "description": "二维码、随机决定等", "sort": 5},
|
||||
{"module_key": "life", "name": "生活查询", "icon": "home", "description": "日期计算、汇率等", "sort": 6},
|
||||
}
|
||||
}
|
||||
|
||||
func seedLevels() []g.Map {
|
||||
return []g.Map{
|
||||
{"level_key": "v1", "name": "基础版", "modules": `["image"]`, "sort": 1, "remark": "默认等级:仅图片处理模块"},
|
||||
{"level_key": "v2", "name": "进阶版", "modules": `["image","doc"]`, "sort": 2, "remark": "示例:追加文档处理模块"},
|
||||
}
|
||||
}
|
||||
|
||||
func seedTools() []g.Map {
|
||||
return []g.Map{
|
||||
{"tool_key": "img-compress", "module_key": "image", "name": "图片压缩", "icon": "picture", "description": "批量压缩图片体积,画质几乎无损", "sort": 1, "is_hot": 1},
|
||||
{"tool_key": "img-convert", "module_key": "image", "name": "格式转换", "icon": "picture", "description": "PNG / JPG / WebP 互转", "sort": 2, "is_hot": 1},
|
||||
{"tool_key": "img-resize", "module_key": "image", "name": "修改尺寸", "icon": "picture", "description": "按宽高、比例或最长边缩放", "sort": 3},
|
||||
{"tool_key": "img-crop", "module_key": "image", "name": "裁剪旋转", "icon": "picture", "description": "自由裁剪、旋转与镜像", "sort": 4},
|
||||
{"tool_key": "img-watermark", "module_key": "image", "name": "文字水印", "icon": "picture", "description": "给图片添加平铺或居中水印", "sort": 5},
|
||||
{"tool_key": "img-matting", "module_key": "image", "name": "去除背景", "icon": "picture", "description": "一键抠除纯色背景", "sort": 6, "is_hot": 1},
|
||||
{"tool_key": "img-avatarfx", "module_key": "image", "name": "头像特效", "icon": "picture", "description": "国庆红旗圈、节日边框等头像特效制作", "sort": 7, "is_hot": 1},
|
||||
{"tool_key": "img-grid9", "module_key": "image", "name": "九宫格切图", "icon": "picture", "description": "照片切成 9 张发朋友圈", "sort": 8},
|
||||
{"tool_key": "img-collage", "module_key": "image", "name": "图片拼图", "icon": "picture", "description": "2-4 张照片拼成一张", "sort": 9},
|
||||
{"tool_key": "img-qrcode", "module_key": "image", "name": "二维码生成", "icon": "picture", "description": "文本链接生成二维码图片", "sort": 10},
|
||||
{"tool_key": "img-annotate", "module_key": "image", "name": "图片标注", "icon": "picture", "description": "涂鸦、文字、马赛克标注", "sort": 11},
|
||||
}
|
||||
}
|
||||
197
internal/logic/tools.go
Normal file
197
internal/logic/tools.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gogf/gf/v2/errors/gcode"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
v1 "tool-api/api/user/v1"
|
||||
"tool-api/internal/consts"
|
||||
)
|
||||
|
||||
func gcodeUnauthorized() gcode.Code {
|
||||
return gcode.New(401, "", nil)
|
||||
}
|
||||
|
||||
func gcodeLocked() gcode.Code {
|
||||
return gcode.New(consts.CodeLocked, "", nil)
|
||||
}
|
||||
|
||||
// ToolsList 工具目录(含模块与工具的锁定态)
|
||||
func ToolsList(ctx context.Context) (*v1.ToolsListRes, error) {
|
||||
user, err := getUserById(ctx, CtxUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
level, err := GetLevelByKey(ctx, user.LevelKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
toolRecords, err := g.Model(consts.TableTools).Where("is_enabled", 1).Order("sort asc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modRecords, err := g.Model(consts.TableModules).Where("is_enabled", 1).Order("sort asc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := &v1.ToolsListRes{Modules: []v1.ModuleOut{}, List: []v1.ToolOut{}}
|
||||
countByModule := map[string]int{}
|
||||
for _, t := range toolRecords {
|
||||
moduleKey := t["module_key"].String()
|
||||
countByModule[moduleKey]++
|
||||
res.List = append(res.List, v1.ToolOut{
|
||||
ToolKey: t["tool_key"].String(),
|
||||
ModuleKey: moduleKey,
|
||||
Name: t["name"].String(),
|
||||
Icon: t["icon"].String(),
|
||||
Description: t["description"].String(),
|
||||
IsHot: t["is_hot"].Int() == 1,
|
||||
Locked: !slicesContain(level.Modules, moduleKey),
|
||||
})
|
||||
}
|
||||
for _, m := range modRecords {
|
||||
moduleKey := m["module_key"].String()
|
||||
res.Modules = append(res.Modules, v1.ModuleOut{
|
||||
ModuleKey: moduleKey,
|
||||
Name: m["name"].String(),
|
||||
Icon: m["icon"].String(),
|
||||
Locked: !slicesContain(level.Modules, moduleKey),
|
||||
EnabledToolsCount: countByModule[moduleKey],
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func slicesContain(list []string, s string) bool {
|
||||
for _, v := range list {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ToolsUsed 工具使用上报(锁定/下架时拒绝)
|
||||
func ToolsUsed(ctx context.Context, toolKey string) (*v1.ToolsUsedRes, error) {
|
||||
user, err := getUserById(ctx, CtxUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
level, err := GetLevelByKey(ctx, user.LevelKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tool, err := g.Model(consts.TableTools).Where("tool_key", toolKey).Where("is_enabled", 1).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tool.IsEmpty() {
|
||||
return nil, gerror.New("工具不存在或已下架")
|
||||
}
|
||||
if !slicesContain(level.Modules, tool["module_key"].String()) {
|
||||
return nil, gerror.NewCode(gcodeLocked(), "该功能需要升级等级后使用")
|
||||
}
|
||||
if _, err = g.Model(consts.TableUsageLogs).Data(g.Map{
|
||||
"user_id": user.Id,
|
||||
"tool_key": toolKey,
|
||||
"used_at": gtime.Now(),
|
||||
}).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = g.Model(consts.TableTools).Where("tool_key", toolKey).
|
||||
Increment("usage_count", 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.ToolsUsedRes{}, nil
|
||||
}
|
||||
|
||||
// WorkbenchGet 我的工作台;未配置时返回热推工具作为默认组合
|
||||
func WorkbenchGet(ctx context.Context) (*v1.WorkbenchGetRes, error) {
|
||||
user, err := getUserById(ctx, CtxUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record, err := g.Model(consts.TableWorkbench).Where("user_id", user.Id).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !record.IsEmpty() {
|
||||
keys := []string{}
|
||||
_ = json.Unmarshal([]byte(record["tool_keys"].String()), &keys)
|
||||
return &v1.WorkbenchGetRes{ToolKeys: keys}, nil
|
||||
}
|
||||
// 首次进入:预置已授权模块下的热推工具
|
||||
level, err := GetLevelByKey(ctx, user.LevelKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hot, err := g.Model(consts.TableTools).Where("is_enabled", 1).Where("is_hot", 1).Order("sort asc").All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys := make([]string, 0, 4)
|
||||
for _, t := range hot {
|
||||
if slicesContain(level.Modules, t["module_key"].String()) {
|
||||
keys = append(keys, t["tool_key"].String())
|
||||
}
|
||||
}
|
||||
return &v1.WorkbenchGetRes{ToolKeys: keys}, nil
|
||||
}
|
||||
|
||||
// WorkbenchSave 保存工作台(校验启用 + 已授权)
|
||||
func WorkbenchSave(ctx context.Context, toolKeys []string) (*v1.WorkbenchSaveRes, error) {
|
||||
user, err := getUserById(ctx, CtxUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
level, err := GetLevelByKey(ctx, user.LevelKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(toolKeys) == 0 {
|
||||
return nil, gerror.New("工作台至少保留一个工具")
|
||||
}
|
||||
for _, key := range toolKeys {
|
||||
tool, err := g.Model(consts.TableTools).Where("tool_key", key).Where("is_enabled", 1).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tool.IsEmpty() {
|
||||
return nil, gerror.Newf("工具 %s 不存在或已下架", key)
|
||||
}
|
||||
if !slicesContain(level.Modules, tool["module_key"].String()) {
|
||||
return nil, gerror.NewCode(gcodeLocked(), "包含未解锁的工具,请升级等级")
|
||||
}
|
||||
}
|
||||
jsonStr, err := json.Marshal(toolKeys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err = g.Model(consts.TableWorkbench).Data(g.Map{
|
||||
"user_id": user.Id,
|
||||
"tool_keys": string(jsonStr),
|
||||
"updated_at": gtime.Now(),
|
||||
}).Save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.WorkbenchSaveRes{}, nil
|
||||
}
|
||||
|
||||
// FeedbackSubmit 提交反馈
|
||||
func FeedbackSubmit(ctx context.Context, content, contact string) (*v1.FeedbackSubmitRes, error) {
|
||||
if _, err := g.Model(consts.TableFeedbacks).Data(g.Map{
|
||||
"user_id": CtxUserId(ctx),
|
||||
"content": content,
|
||||
"contact": contact,
|
||||
}).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.FeedbackSubmitRes{}, nil
|
||||
}
|
||||
341
internal/logic/user.go
Normal file
341
internal/logic/user.go
Normal file
@@ -0,0 +1,341 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
"github.com/gogf/gf/v2/util/guid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
v1 "tool-api/api/user/v1"
|
||||
"tool-api/internal/consts"
|
||||
"tool-api/internal/model/entity"
|
||||
)
|
||||
|
||||
// WxLogin 微信登录(code 换 openid)
|
||||
func WxLogin(ctx context.Context, code string) (*v1.WxLoginRes, error) {
|
||||
openid, err := resolveOpenid(ctx, code)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loginByOpenid(ctx, openid)
|
||||
}
|
||||
|
||||
// DevLogin 开发联调登录(仅 debug 配置开放)
|
||||
func DevLogin(ctx context.Context) (*v1.WxLoginRes, error) {
|
||||
if !IsDebug(ctx) {
|
||||
return nil, gerror.New("当前环境未开启 debug,禁止开发登录")
|
||||
}
|
||||
return loginByOpenid(ctx, "dev-user")
|
||||
}
|
||||
|
||||
// UserLevel 我的等级与已授权模块
|
||||
func UserLevel(ctx context.Context) (*v1.LevelRes, error) {
|
||||
user, err := getUserById(ctx, CtxUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetLevelByKey(ctx, user.LevelKey)
|
||||
}
|
||||
|
||||
// GetUserInfo 当前登录用户信息
|
||||
func GetUserInfo(ctx context.Context) (*v1.UserInfo, error) {
|
||||
user, err := getUserById(ctx, CtxUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.UserInfo{Id: user.Id, Nickname: user.Nickname, AvatarUrl: user.AvatarUrl}, nil
|
||||
}
|
||||
|
||||
func resolveOpenid(ctx context.Context, code string) (string, error) {
|
||||
// 联调快捷通道:debug 模式下 code 传 "dev"
|
||||
if code == "dev" && IsDebug(ctx) {
|
||||
return "dev-user", nil
|
||||
}
|
||||
appId := g.Cfg().MustGet(ctx, "wx.appId").String()
|
||||
secret := g.Cfg().MustGet(ctx, "wx.appSecret").String()
|
||||
if appId == "" || secret == "" {
|
||||
return "", gerror.New("服务端未配置微信 appId/appSecret,请填写 manifest/config/config.yaml 后重启")
|
||||
}
|
||||
url := fmt.Sprintf(
|
||||
"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code",
|
||||
appId, secret, code,
|
||||
)
|
||||
resp, err := g.Client().Get(ctx, url)
|
||||
if err != nil {
|
||||
return "", gerror.Newf("请求微信接口失败: %v", err)
|
||||
}
|
||||
defer resp.Close()
|
||||
var out struct {
|
||||
Openid string `json:"openid"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.ReadAll(), &out); err != nil {
|
||||
return "", gerror.Newf("解析微信响应失败: %v", err)
|
||||
}
|
||||
if out.ErrCode != 0 || out.Openid == "" {
|
||||
return "", gerror.Newf("微信登录失败: %d %s", out.ErrCode, out.ErrMsg)
|
||||
}
|
||||
return out.Openid, nil
|
||||
}
|
||||
|
||||
func loginByOpenid(ctx context.Context, openid string) (*v1.WxLoginRes, error) {
|
||||
// 原子 upsert(原生 SQL):小程序启动时 onLaunch 与页面 onShow 会并发各发一次登录,
|
||||
// 冲突时仅更新登录时间。不依赖 ORM 的 OnDuplicate 拼接,行为跨版本稳定。
|
||||
now := gtime.Now()
|
||||
if _, err := g.DB().Exec(ctx,
|
||||
"INSERT INTO `users`(`openid`, `nickname`, `level_key`, `status`, `created_at`, `last_login_at`, `updated_at`) "+
|
||||
"VALUES(?, ?, ?, ?, ?, ?, ?) "+
|
||||
"ON DUPLICATE KEY UPDATE `last_login_at` = VALUES(`last_login_at`)",
|
||||
openid, "微信用户", "v1", 1, now, now, now,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record, err := g.Model(consts.TableUsers).Where("openid", openid).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := &entity.Users{}
|
||||
if err = record.Struct(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.Status != 1 {
|
||||
return nil, gerror.New("账号已被禁用,请联系管理员")
|
||||
}
|
||||
return loginResult(ctx, user)
|
||||
}
|
||||
|
||||
// ===== H5 账号:注册 / 登录 / 绑定合并 =====
|
||||
|
||||
// placeholderOpenid H5 独立账号的 openid 占位(openid 列 NOT NULL UNIQUE,微信 openid 不会以 h5- 开头)
|
||||
func placeholderOpenid() string {
|
||||
return "h5-" + guid.S()
|
||||
}
|
||||
|
||||
// Register H5 独立账号注册(注册即登录)
|
||||
func Register(ctx context.Context, username, password, nickname string) (*v1.WxLoginRes, error) {
|
||||
count, err := g.Model(consts.TableUsers).Where("username", username).Count()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count > 0 {
|
||||
return nil, gerror.New("用户名已被占用")
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if nickname == "" {
|
||||
nickname = username
|
||||
}
|
||||
now := gtime.Now()
|
||||
if _, err = g.Model(consts.TableUsers).Data(g.Map{
|
||||
"openid": placeholderOpenid(),
|
||||
"username": username,
|
||||
"password_hash": string(hash),
|
||||
"nickname": nickname,
|
||||
"level_key": "v1",
|
||||
"status": 1,
|
||||
"created_at": now,
|
||||
"last_login_at": now,
|
||||
}).Insert(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return loginByUsername(ctx, username, password)
|
||||
}
|
||||
|
||||
// AccountLogin H5 账号密码登录
|
||||
func AccountLogin(ctx context.Context, username, password string) (*v1.WxLoginRes, error) {
|
||||
return loginByUsername(ctx, username, password)
|
||||
}
|
||||
|
||||
func loginByUsername(ctx context.Context, username, password string) (*v1.WxLoginRes, error) {
|
||||
record, err := g.Model(consts.TableUsers).Where("username", username).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
return nil, gerror.New("用户名或密码错误")
|
||||
}
|
||||
user := &entity.Users{}
|
||||
if err = record.Struct(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user.PasswordHash == "" || bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) != nil {
|
||||
return nil, gerror.New("用户名或密码错误")
|
||||
}
|
||||
if user.Status != 1 {
|
||||
return nil, gerror.New("账号已被禁用,请联系管理员")
|
||||
}
|
||||
// 更新登录时间(fire-and-forget 语义,失败不影响登录)
|
||||
_, _ = g.Model(consts.TableUsers).Where("id", user.Id).Data(g.Map{"last_login_at": gtime.Now()}).Update()
|
||||
return loginResult(ctx, user)
|
||||
}
|
||||
|
||||
// BindAccount 把一个 H5 独立账号合并进当前登录账号:
|
||||
// 迁移其工作台配置 → 将用户名/密码挂到当前账号 → 目标账号禁用保留历史。
|
||||
// 合并后两端使用同一份用户数据(工作台/等级/反馈)。
|
||||
func BindAccount(ctx context.Context, username, password string) (*v1.BindAccountRes, error) {
|
||||
current, err := getUserById(ctx, CtxUserId(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
record, err := g.Model(consts.TableUsers).Where("username", username).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
return nil, gerror.New("该账号不存在,请先在 H5 端注册")
|
||||
}
|
||||
target := &entity.Users{}
|
||||
if err = record.Struct(target); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 已是当前账号:视为重复操作
|
||||
if target.Id == current.Id {
|
||||
return nil, gerror.New("该账号已是当前账号")
|
||||
}
|
||||
// 只允许合并 H5 独立账号(openid 为 h5- 占位),微信账号不可被合并
|
||||
if target.Openid == current.Openid || !(len(target.Openid) > 3 && target.Openid[:3] == "h5-") {
|
||||
return nil, gerror.New("该账号已绑定微信,无法合并")
|
||||
}
|
||||
if target.PasswordHash == "" || bcrypt.CompareHashAndPassword([]byte(target.PasswordHash), []byte(password)) != nil {
|
||||
return nil, gerror.New("用户名或密码错误")
|
||||
}
|
||||
if current.Username != "" {
|
||||
return nil, gerror.New("当前账号已绑定过 H5 账号")
|
||||
}
|
||||
|
||||
// 迁移工作台:目标 keys 追加到当前 keys 之后(去重,上限 20)
|
||||
targetKeys := workbenchKeysOf(ctx, target.Id)
|
||||
if len(targetKeys) > 0 {
|
||||
currentKeys := workbenchKeysOf(ctx, current.Id)
|
||||
seen := map[string]bool{}
|
||||
merged := make([]string, 0, len(currentKeys)+len(targetKeys))
|
||||
for _, k := range append(append([]string{}, currentKeys...), targetKeys...) {
|
||||
if !seen[k] {
|
||||
seen[k] = true
|
||||
merged = append(merged, k)
|
||||
}
|
||||
}
|
||||
if len(merged) > 20 {
|
||||
merged = merged[:20]
|
||||
}
|
||||
if err = saveWorkbenchRaw(ctx, current.Id, merged); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 先释放目标账号的用户名(username 可空,唯一索引不冲突),并禁用保留历史
|
||||
if _, err = g.Model(consts.TableUsers).Where("id", target.Id).Data(g.Map{
|
||||
"username": nil,
|
||||
"status": 0,
|
||||
}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 凭证挂到当前账号
|
||||
if _, err = g.Model(consts.TableUsers).Where("id", current.Id).Data(g.Map{
|
||||
"username": username,
|
||||
"password_hash": target.PasswordHash,
|
||||
}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.BindAccountRes{Username: username}, nil
|
||||
}
|
||||
|
||||
// ProfileUpdate 更新当前用户昵称 / 头像
|
||||
func ProfileUpdate(ctx context.Context, nickname, avatarUrl string) (*v1.ProfileUpdateRes, error) {
|
||||
userId := CtxUserId(ctx)
|
||||
if _, err := g.Model(consts.TableUsers).Where("id", userId).Data(g.Map{
|
||||
"nickname": nickname,
|
||||
"avatar_url": avatarUrl,
|
||||
}).Update(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.ProfileUpdateRes{Nickname: nickname, AvatarUrl: avatarUrl}, nil
|
||||
}
|
||||
|
||||
func workbenchKeysOf(ctx context.Context, userId int64) []string {
|
||||
record, err := g.Model(consts.TableWorkbench).Where("user_id", userId).One()
|
||||
if err != nil || record.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
keys := []string{}
|
||||
_ = json.Unmarshal([]byte(record["tool_keys"].String()), &keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// saveWorkbenchRaw 直接写入工作台配置(绑定合并用,不做逐个工具校验)
|
||||
func saveWorkbenchRaw(ctx context.Context, userId int64, keys []string) error {
|
||||
jsonStr, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = g.Model(consts.TableWorkbench).Data(g.Map{
|
||||
"user_id": userId,
|
||||
"tool_keys": string(jsonStr),
|
||||
"updated_at": gtime.Now(),
|
||||
}).Save()
|
||||
return err
|
||||
}
|
||||
|
||||
// loginResult 统一组装登录返回(token + 用户 + 等级)
|
||||
func loginResult(ctx context.Context, user *entity.Users) (*v1.WxLoginRes, error) {
|
||||
level, err := GetLevelByKey(ctx, user.LevelKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
token, err := IssueToken(ctx, consts.AudUser, user.Id, map[string]string{"levelKey": user.LevelKey})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.WxLoginRes{
|
||||
Token: token,
|
||||
User: &v1.UserInfo{Id: user.Id, Nickname: user.Nickname, AvatarUrl: user.AvatarUrl},
|
||||
Level: level,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getUserById(ctx context.Context, id int64) (*entity.Users, error) {
|
||||
record, err := g.Model(consts.TableUsers).Where("id", id).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
return nil, gerror.NewCode(gcodeUnauthorized(), "用户不存在")
|
||||
}
|
||||
user := &entity.Users{}
|
||||
if err = record.Struct(user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetLevelByKey 查询等级(含已授权模块 key 列表)
|
||||
func GetLevelByKey(ctx context.Context, key string) (*v1.LevelInfo, error) {
|
||||
record, err := g.Model(consts.TableLevels).Where("level_key", key).Where("is_enabled", 1).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if record.IsEmpty() {
|
||||
// 等级被停用或不存在:回落为空权限,避免越权
|
||||
return &v1.LevelInfo{LevelKey: key, Name: "未授权", Modules: []string{}}, nil
|
||||
}
|
||||
return levelInfoFromRecord(record), nil
|
||||
}
|
||||
|
||||
func levelInfoFromRecord(record gdb.Record) *v1.LevelInfo {
|
||||
modules := []string{}
|
||||
_ = json.Unmarshal([]byte(record["modules"].String()), &modules)
|
||||
return &v1.LevelInfo{
|
||||
LevelKey: record["level_key"].String(),
|
||||
Name: record["name"].String(),
|
||||
Modules: modules,
|
||||
}
|
||||
}
|
||||
0
internal/model/.gitkeep
Normal file
0
internal/model/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/do/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
0
internal/model/entity/.gitkeep
Normal file
14
internal/model/entity/entity_admin_users.go
Normal file
14
internal/model/entity/entity_admin_users.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// AdminUsers 后台管理员
|
||||
type AdminUsers struct {
|
||||
Id int64 `json:"id"`
|
||||
Account string `json:"account"`
|
||||
Password string `json:"-"`
|
||||
NickName string `json:"nick_name"`
|
||||
RoleName string `json:"role_name"`
|
||||
RoleValue string `json:"role_value"`
|
||||
CreatedAt *gtime.Time `json:"created_at"`
|
||||
}
|
||||
13
internal/model/entity/entity_feedbacks.go
Normal file
13
internal/model/entity/entity_feedbacks.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// Feedbacks 用户反馈
|
||||
type Feedbacks struct {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
Content string `json:"content"`
|
||||
Contact string `json:"contact"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt *gtime.Time `json:"created_at"`
|
||||
}
|
||||
16
internal/model/entity/entity_levels.go
Normal file
16
internal/model/entity/entity_levels.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// Levels 用户等级(模块权限)
|
||||
type Levels struct {
|
||||
Id int64 `json:"id"`
|
||||
LevelKey string `json:"level_key"`
|
||||
Name string `json:"name"`
|
||||
Modules string `json:"modules"` // JSON: ["image","doc"]
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"is_enabled"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt *gtime.Time `json:"created_at"`
|
||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||
}
|
||||
16
internal/model/entity/entity_modules.go
Normal file
16
internal/model/entity/entity_modules.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// Modules 功能模块
|
||||
type Modules struct {
|
||||
Id int64 `json:"id"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"is_enabled"`
|
||||
CreatedAt *gtime.Time `json:"created_at"`
|
||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||
}
|
||||
11
internal/model/entity/entity_tool_usage_logs.go
Normal file
11
internal/model/entity/entity_tool_usage_logs.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// ToolUsageLogs 工具使用日志
|
||||
type ToolUsageLogs struct {
|
||||
Id int64 `json:"id"`
|
||||
UserId int64 `json:"user_id"`
|
||||
ToolKey string `json:"tool_key"`
|
||||
UsedAt *gtime.Time `json:"used_at"`
|
||||
}
|
||||
19
internal/model/entity/entity_tools.go
Normal file
19
internal/model/entity/entity_tools.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// Tools 工具目录
|
||||
type Tools struct {
|
||||
Id int64 `json:"id"`
|
||||
ToolKey string `json:"tool_key"`
|
||||
ModuleKey string `json:"module_key"`
|
||||
Name string `json:"name"`
|
||||
Icon string `json:"icon"`
|
||||
Description string `json:"description"`
|
||||
Sort int `json:"sort"`
|
||||
IsEnabled int `json:"is_enabled"`
|
||||
IsHot int `json:"is_hot"`
|
||||
UsageCount int64 `json:"usage_count"`
|
||||
CreatedAt *gtime.Time `json:"created_at"`
|
||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||
}
|
||||
10
internal/model/entity/entity_user_workbench.go
Normal file
10
internal/model/entity/entity_user_workbench.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// UserWorkbench 工作台配置
|
||||
type UserWorkbench struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
ToolKeys string `json:"tool_keys"` // JSON: ["img-compress",...]
|
||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||
}
|
||||
18
internal/model/entity/entity_users.go
Normal file
18
internal/model/entity/entity_users.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package entity
|
||||
|
||||
import "github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
// Users 小程序/H5 用户
|
||||
type Users struct {
|
||||
Id int64 `json:"id"`
|
||||
Openid string `json:"openid"`
|
||||
Username string `json:"username"`
|
||||
PasswordHash string `json:"password_hash"`
|
||||
Nickname string `json:"nickname"`
|
||||
AvatarUrl string `json:"avatar_url"`
|
||||
LevelKey string `json:"level_key"`
|
||||
Status int `json:"status"`
|
||||
CreatedAt *gtime.Time `json:"created_at"`
|
||||
LastLoginAt *gtime.Time `json:"last_login_at"`
|
||||
UpdatedAt *gtime.Time `json:"updated_at"`
|
||||
}
|
||||
0
internal/service/.gitkeep
Normal file
0
internal/service/.gitkeep
Normal file
11
main.go
Normal file
11
main.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gogf/gf/v2/os/gctx"
|
||||
|
||||
"tool-api/internal/cmd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cmd.Main.Run(gctx.GetInitCtx())
|
||||
}
|
||||
BIN
main_test.exe
Normal file
BIN
main_test.exe
Normal file
Binary file not shown.
21
manifest/deploy/kustomize/base/deployment.yaml
Normal file
21
manifest/deploy/kustomize/base/deployment.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: template-single
|
||||
labels:
|
||||
app: template-single
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: template-single
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: template-single
|
||||
spec:
|
||||
containers:
|
||||
- name : main
|
||||
image: template-single
|
||||
imagePullPolicy: Always
|
||||
|
||||
8
manifest/deploy/kustomize/base/kustomization.yaml
Normal file
8
manifest/deploy/kustomize/base/kustomization.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
|
||||
|
||||
|
||||
12
manifest/deploy/kustomize/base/service.yaml
Normal file
12
manifest/deploy/kustomize/base/service.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: template-single
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
protocol: TCP
|
||||
targetPort: 8000
|
||||
selector:
|
||||
app: template-single
|
||||
|
||||
14
manifest/deploy/kustomize/overlays/develop/configmap.yaml
Normal file
14
manifest/deploy/kustomize/overlays/develop/configmap.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: template-single-configmap
|
||||
data:
|
||||
config.yaml: |
|
||||
server:
|
||||
address: ":8000"
|
||||
openapiPath: "/api.json"
|
||||
swaggerPath: "/swagger"
|
||||
|
||||
logger:
|
||||
level : "all"
|
||||
stdout: true
|
||||
10
manifest/deploy/kustomize/overlays/develop/deployment.yaml
Normal file
10
manifest/deploy/kustomize/overlays/develop/deployment.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: template-single
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name : main
|
||||
image: template-single:develop
|
||||
@@ -0,0 +1,14 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- ../../base
|
||||
- configmap.yaml
|
||||
|
||||
patchesStrategicMerge:
|
||||
- deployment.yaml
|
||||
|
||||
namespace: default
|
||||
|
||||
|
||||
|
||||
16
manifest/docker/Dockerfile
Normal file
16
manifest/docker/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM loads/alpine:3.8
|
||||
|
||||
###############################################################################
|
||||
# INSTALLATION
|
||||
###############################################################################
|
||||
|
||||
ENV WORKDIR /app
|
||||
ADD resource $WORKDIR/
|
||||
ADD ./temp/linux_amd64/main $WORKDIR/main
|
||||
RUN chmod +x $WORKDIR/main
|
||||
|
||||
###############################################################################
|
||||
# START
|
||||
###############################################################################
|
||||
WORKDIR $WORKDIR
|
||||
CMD ./main
|
||||
8
manifest/docker/docker.sh
Normal file
8
manifest/docker/docker.sh
Normal file
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This shell is executed before docker build.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
0
manifest/i18n/.gitkeep
Normal file
0
manifest/i18n/.gitkeep
Normal file
0
manifest/protobuf/.keep-if-necessary
Normal file
0
manifest/protobuf/.keep-if-necessary
Normal file
122
manifest/sql/migrate.sql
Normal file
122
manifest/sql/migrate.sql
Normal file
@@ -0,0 +1,122 @@
|
||||
-- 工具箱项目数据库初始化脚本
|
||||
-- 种子数据(管理员/模块/等级/工具)由服务启动时自动写入(logic.Seed),无需手工插入。
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `nl_toolbox` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
|
||||
USE `nl_toolbox`;
|
||||
|
||||
-- 功能模块
|
||||
CREATE TABLE IF NOT EXISTS `modules` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`module_key` VARCHAR(32) NOT NULL COMMENT 'image/doc/calc/time/create/life',
|
||||
`name` VARCHAR(64) NOT NULL COMMENT '模块名称',
|
||||
`icon` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '图标 key(wot 图标名)',
|
||||
`description` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sort` INT NOT NULL DEFAULT 0,
|
||||
`is_enabled` TINYINT NOT NULL DEFAULT 1,
|
||||
`created_at` DATETIME NULL DEFAULT NULL,
|
||||
`updated_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_module_key` (`module_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='功能模块';
|
||||
|
||||
-- 用户等级(模块权限)
|
||||
CREATE TABLE IF NOT EXISTS `levels` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`level_key` VARCHAR(32) NOT NULL COMMENT 'v1/v2/v3...',
|
||||
`name` VARCHAR(64) NOT NULL COMMENT '展示名:基础版/进阶版',
|
||||
`modules` JSON NOT NULL COMMENT '["image","doc"] 模块 key 数组',
|
||||
`sort` INT NOT NULL DEFAULT 0,
|
||||
`is_enabled` TINYINT NOT NULL DEFAULT 1,
|
||||
`remark` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`created_at` DATETIME NULL DEFAULT NULL,
|
||||
`updated_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_level_key` (`level_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户等级(模块权限)';
|
||||
|
||||
-- 小程序/H5 用户
|
||||
-- username/password_hash:H5 账号登录用;微信账号绑定 H5 账号后同样有值(合并账号)
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`openid` VARCHAR(64) NOT NULL COMMENT '微信openid;H5 独立账号为 h5- 前缀占位',
|
||||
`username` VARCHAR(64) NULL DEFAULT NULL COMMENT 'H5 登录名(唯一,可空)',
|
||||
`password_hash` VARCHAR(255) NULL DEFAULT NULL COMMENT 'bcrypt',
|
||||
`nickname` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`avatar_url` VARCHAR(512) NOT NULL DEFAULT '',
|
||||
`level_key` VARCHAR(32) NOT NULL DEFAULT 'v1',
|
||||
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0禁用',
|
||||
`created_at` DATETIME NULL DEFAULT NULL,
|
||||
`last_login_at` DATETIME NULL DEFAULT NULL,
|
||||
`updated_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_openid` (`openid`),
|
||||
UNIQUE KEY `uk_username` (`username`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='小程序用户';
|
||||
|
||||
-- ===== v0.2 升级(已有库手动执行;MySQL 8.0 不支持 ADD COLUMN IF NOT EXISTS) =====
|
||||
-- ALTER TABLE `users`
|
||||
-- ADD COLUMN `username` VARCHAR(64) NULL DEFAULT NULL COMMENT 'H5 登录名(唯一,可空)' AFTER `openid`,
|
||||
-- ADD COLUMN `password_hash` VARCHAR(255) NULL DEFAULT NULL COMMENT 'bcrypt' AFTER `username`,
|
||||
-- ADD UNIQUE KEY `uk_username` (`username`);
|
||||
|
||||
-- 工具目录
|
||||
CREATE TABLE IF NOT EXISTS `tools` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`tool_key` VARCHAR(64) NOT NULL COMMENT '与前端注册表 src/config/tools.ts 强约定一致',
|
||||
`module_key` VARCHAR(32) NOT NULL COMMENT '所属模块',
|
||||
`name` VARCHAR(64) NOT NULL,
|
||||
`icon` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`description` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`sort` INT NOT NULL DEFAULT 0,
|
||||
`is_enabled` TINYINT NOT NULL DEFAULT 1,
|
||||
`is_hot` TINYINT NOT NULL DEFAULT 0 COMMENT '工作台推荐位',
|
||||
`usage_count` BIGINT NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME NULL DEFAULT NULL,
|
||||
`updated_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_tool_key` (`tool_key`),
|
||||
KEY `idx_module_key` (`module_key`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具目录';
|
||||
|
||||
-- 工具使用日志
|
||||
CREATE TABLE IF NOT EXISTS `tool_usage_logs` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`tool_key` VARCHAR(64) NOT NULL,
|
||||
`used_at` DATETIME NOT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_tool_time` (`user_id`, `tool_key`, `used_at`),
|
||||
KEY `idx_used_at` (`used_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工具使用日志';
|
||||
|
||||
-- 工作台配置
|
||||
CREATE TABLE IF NOT EXISTS `user_workbench` (
|
||||
`user_id` BIGINT UNSIGNED NOT NULL,
|
||||
`tool_keys` JSON NOT NULL COMMENT '["img-compress",...] 有序',
|
||||
`updated_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工作台配置';
|
||||
|
||||
-- 后台管理员
|
||||
CREATE TABLE IF NOT EXISTS `admin_users` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`account` VARCHAR(64) NOT NULL,
|
||||
`password` VARCHAR(255) NOT NULL COMMENT 'bcrypt',
|
||||
`nick_name` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`role_name` VARCHAR(64) NOT NULL DEFAULT '超级管理员',
|
||||
`role_value` VARCHAR(32) NOT NULL DEFAULT 'super',
|
||||
`created_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_account` (`account`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='后台管理员';
|
||||
|
||||
-- 用户反馈
|
||||
CREATE TABLE IF NOT EXISTS `feedbacks` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`user_id` BIGINT UNSIGNED NULL,
|
||||
`content` VARCHAR(1000) NOT NULL,
|
||||
`contact` VARCHAR(128) NOT NULL DEFAULT '',
|
||||
`status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1已处理',
|
||||
`created_at` DATETIME NULL DEFAULT NULL,
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户反馈';
|
||||
0
resource/public/html/.gitkeep
Normal file
0
resource/public/html/.gitkeep
Normal file
0
resource/public/plugin/.gitkeep
Normal file
0
resource/public/plugin/.gitkeep
Normal file
0
resource/public/resource/css/.gitkeep
Normal file
0
resource/public/resource/css/.gitkeep
Normal file
0
resource/public/resource/image/.gitkeep
Normal file
0
resource/public/resource/image/.gitkeep
Normal file
0
resource/public/resource/js/.gitkeep
Normal file
0
resource/public/resource/js/.gitkeep
Normal file
0
resource/template/.gitkeep
Normal file
0
resource/template/.gitkeep
Normal file
0
utility/.gitkeep
Normal file
0
utility/.gitkeep
Normal file
Reference in New Issue
Block a user