2026-09-17 14:42:44 +08:00
|
|
|
|
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"
|
2026-09-17 17:38:45 +08:00
|
|
|
|
"tool-api/internal/model/entity"
|
2026-09-17 14:42:44 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-09-17 17:38:45 +08:00
|
|
|
|
level, err := EffectiveLevelInfo(ctx, user)
|
2026-09-17 14:42:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-29 10:57:01 +08:00
|
|
|
|
toolRecords, err := g.Model(consts.TableTools).Where("is_enabled", 1).WhereNull("deleted_at").Order("sort asc").All()
|
2026-09-17 14:42:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 17:38:45 +08:00
|
|
|
|
// 额度快照:一次算完所有工具,避免前端再为每个工具单独请求
|
|
|
|
|
|
toolEntities := make([]*entity.Tools, 0, len(toolRecords))
|
|
|
|
|
|
for _, r := range toolRecords {
|
|
|
|
|
|
tool := &entity.Tools{}
|
|
|
|
|
|
if err = r.Struct(tool); err == nil {
|
|
|
|
|
|
toolEntities = append(toolEntities, tool)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
quotaSnapshot, err := QuotaSnapshotForTools(ctx, user, toolEntities)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 额度查询失败不该拖垮工具目录:降级为不返回额度
|
|
|
|
|
|
g.Log().Warningf(ctx, "[quota] 工具额度快照失败: %v", err)
|
|
|
|
|
|
quotaSnapshot = map[string]*v1.QuotaToolOut{}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 14:42:44 +08:00
|
|
|
|
res := &v1.ToolsListRes{Modules: []v1.ModuleOut{}, List: []v1.ToolOut{}}
|
|
|
|
|
|
countByModule := map[string]int{}
|
|
|
|
|
|
for _, t := range toolRecords {
|
|
|
|
|
|
moduleKey := t["module_key"].String()
|
|
|
|
|
|
countByModule[moduleKey]++
|
2026-09-17 17:38:45 +08:00
|
|
|
|
toolKey := t["tool_key"].String()
|
2026-09-17 14:42:44 +08:00
|
|
|
|
res.List = append(res.List, v1.ToolOut{
|
2026-09-17 17:38:45 +08:00
|
|
|
|
ToolKey: toolKey,
|
2026-09-17 14:42:44 +08:00
|
|
|
|
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),
|
2026-09-17 17:38:45 +08:00
|
|
|
|
Quota: quotaSnapshot[toolKey],
|
2026-09-17 14:42:44 +08:00
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-17 17:38:45 +08:00
|
|
|
|
// ToolsUsed 工具使用上报(打开工具页时调用,锁定/下架时拒绝)。
|
|
|
|
|
|
//
|
|
|
|
|
|
// ⚠️ 这里只记统计,**不扣额度**。额度扣减走 POST /quota/consume,
|
|
|
|
|
|
// 时机是「产出结果」(保存/导出成功)而不是打开页面 —— 用户看了看没产出就不该扣。
|
2026-09-17 14:42:44 +08:00
|
|
|
|
func ToolsUsed(ctx context.Context, toolKey string) (*v1.ToolsUsedRes, error) {
|
|
|
|
|
|
user, err := getUserById(ctx, CtxUserId(ctx))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-09-17 17:38:45 +08:00
|
|
|
|
level, err := EffectiveLevelInfo(ctx, user)
|
2026-09-17 14:42:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-09-29 10:57:01 +08:00
|
|
|
|
tool, err := g.Model(consts.TableTools).Where("tool_key", toolKey).Where("is_enabled", 1).WhereNull("deleted_at").One()
|
2026-09-17 14:42:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
// 首次进入:预置已授权模块下的热推工具
|
2026-09-17 17:38:45 +08:00
|
|
|
|
level, err := EffectiveLevelInfo(ctx, user)
|
2026-09-17 14:42:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-09-29 10:57:01 +08:00
|
|
|
|
hot, err := g.Model(consts.TableTools).Where("is_enabled", 1).Where("is_hot", 1).WhereNull("deleted_at").Order("sort asc").All()
|
2026-09-17 14:42:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-09-17 17:38:45 +08:00
|
|
|
|
level, err := EffectiveLevelInfo(ctx, user)
|
2026-09-17 14:42:44 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(toolKeys) == 0 {
|
|
|
|
|
|
return nil, gerror.New("工作台至少保留一个工具")
|
|
|
|
|
|
}
|
|
|
|
|
|
for _, key := range toolKeys {
|
2026-09-29 10:57:01 +08:00
|
|
|
|
tool, err := g.Model(consts.TableTools).Where("tool_key", key).Where("is_enabled", 1).WhereNull("deleted_at").One()
|
2026-09-17 14:42:44 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-09-29 15:48:13 +08:00
|
|
|
|
|
|
|
|
|
|
// FeedbackMine 我的反馈(最近 N 条 + 未读官方回复数)。
|
|
|
|
|
|
// 不在此处置已读:红点需要「先看到未读数 → 打开列表 → 再清零」的两步语义,已读走 FeedbackMarkRead。
|
|
|
|
|
|
func FeedbackMine(ctx context.Context, limit int) (*v1.FeedbackMineRes, error) {
|
|
|
|
|
|
if limit <= 0 || limit > 50 {
|
|
|
|
|
|
limit = 20
|
|
|
|
|
|
}
|
|
|
|
|
|
userId := CtxUserId(ctx)
|
|
|
|
|
|
records, err := g.Model(consts.TableFeedbacks).
|
|
|
|
|
|
Where("user_id", userId).
|
|
|
|
|
|
OrderDesc("id").
|
|
|
|
|
|
Limit(limit).
|
|
|
|
|
|
All()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
list := make([]v1.FeedbackMineItem, 0, len(records))
|
|
|
|
|
|
unread := 0
|
|
|
|
|
|
for _, r := range records {
|
|
|
|
|
|
item := v1.FeedbackMineItem{
|
|
|
|
|
|
Id: r["id"].Int64(),
|
|
|
|
|
|
Content: r["content"].String(),
|
|
|
|
|
|
Status: r["status"].Int(),
|
|
|
|
|
|
ReplyContent: r["reply_content"].String(),
|
|
|
|
|
|
ReplyAt: r["reply_at"].String(),
|
|
|
|
|
|
ReplyRead: r["reply_read"].Int(),
|
|
|
|
|
|
CreatedAt: r["created_at"].String(),
|
|
|
|
|
|
}
|
|
|
|
|
|
// 有官方回复且未读(老数据 reply_read 默认 0,但无回复内容不算未读)
|
|
|
|
|
|
if item.ReplyContent != "" && r["reply_read"].Int() == 0 {
|
|
|
|
|
|
unread++
|
|
|
|
|
|
}
|
|
|
|
|
|
list = append(list, item)
|
|
|
|
|
|
}
|
|
|
|
|
|
return &v1.FeedbackMineRes{List: list, Unread: unread}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// FeedbackMarkRead 把当前用户的全部反馈回复置为已读(清红点)。
|
|
|
|
|
|
func FeedbackMarkRead(ctx context.Context) (*v1.FeedbackReadRes, error) {
|
|
|
|
|
|
if _, err := g.Model(consts.TableFeedbacks).
|
|
|
|
|
|
Where("user_id", CtxUserId(ctx)).
|
|
|
|
|
|
Where("reply_read", 0).
|
|
|
|
|
|
Data(g.Map{"reply_read": 1}).Update(); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return &v1.FeedbackReadRes{}, nil
|
|
|
|
|
|
}
|