初始化
This commit is contained in:
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
Reference in New Issue
Block a user