Files

266 lines
8.0 KiB
Go
Raw Permalink Normal View History

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
}