初始化

This commit is contained in:
2026-09-17 14:42:44 +08:00
commit 7c106b69d2
54 changed files with 2521 additions and 0 deletions

44
internal/cmd/cmd.go Normal file
View 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
View 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"
)

View 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)
}

View 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
View File

476
internal/logic/admin.go Normal file
View 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
View 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()
}

View 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
View 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
View 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
View 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
View File

View File

View File

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View 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"`
}

View File