diff --git a/api/core/app_server.go b/api/core/app_server.go index d633fc18..2fca4ea7 100644 --- a/api/core/app_server.go +++ b/api/core/app_server.go @@ -217,6 +217,7 @@ func needLogin(c *gin.Context) bool { c.Request.URL.Path == "/api/sd/client" || c.Request.URL.Path == "/api/config/get" || c.Request.URL.Path == "/api/product/list" || + c.Request.URL.Path == "/api/menu/list" || strings.HasPrefix(c.Request.URL.Path, "/api/test") || strings.HasPrefix(c.Request.URL.Path, "/api/function/") || strings.HasPrefix(c.Request.URL.Path, "/api/sms/") || diff --git a/api/core/types/config.go b/api/core/types/config.go index 324c3cad..24a94e78 100644 --- a/api/core/types/config.go +++ b/api/core/types/config.go @@ -144,13 +144,13 @@ type SystemConfig struct { PowerPrice float64 `json:"power_price,omitempty"` // 算力单价 OrderPayTimeout int `json:"order_pay_timeout,omitempty"` //订单支付超时时间 - VipInfoText string `json:"vip_info_text"` // 会员页面充值说明 + VipInfoText string `json:"vip_info_text,omitempty"` // 会员页面充值说明 DefaultModels []int `json:"default_models,omitempty"` // 默认开通的 AI 模型 - MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 - MjActionPower int `json:"mj_action_power"` // MJ 操作(放大,变换)消耗算力 - SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 - DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力 + MjPower int `json:"mj_power,omitempty"` // MJ 绘画消耗算力 + MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力 + SdPower int `json:"sd_power,omitempty"` // SD 绘画消耗算力 + DallPower int `json:"dall_power,omitempty"` // DALLE3 绘图消耗算力 WechatCardURL string `json:"wechat_card_url,omitempty"` // 微信客服地址 diff --git a/api/handler/admin/menu_handler.go b/api/handler/admin/menu_handler.go new file mode 100644 index 00000000..7358ffa9 --- /dev/null +++ b/api/handler/admin/menu_handler.go @@ -0,0 +1,121 @@ +package admin + +import ( + "chatplus/core" + "chatplus/core/types" + "chatplus/handler" + "chatplus/store/model" + "chatplus/store/vo" + "chatplus/utils" + "chatplus/utils/resp" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type MenuHandler struct { + handler.BaseHandler +} + +func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler { + return &MenuHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}} +} + +func (h *MenuHandler) Save(c *gin.Context) { + var data struct { + Id uint `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + URL string `json:"url"` + SortNum int `json:"sort_num"` + Enabled bool `json:"enabled"` + } + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + res := h.DB.Save(&model.Menu{ + Id: data.Id, + Name: data.Name, + Icon: data.Icon, + URL: data.URL, + SortNum: data.SortNum, + Enabled: data.Enabled, + }) + if res.Error != nil { + resp.ERROR(c, "更新数据库失败!") + return + } + resp.SUCCESS(c) +} + +// List 数据列表 +func (h *MenuHandler) List(c *gin.Context) { + var items []model.Menu + var list = make([]vo.Menu, 0) + res := h.DB.Order("sort_num ASC").Find(&items) + if res.Error == nil { + for _, item := range items { + var product vo.Menu + err := utils.CopyObject(item, &product) + if err == nil { + list = append(list, product) + } + } + } + resp.SUCCESS(c, list) +} + +func (h *MenuHandler) Enable(c *gin.Context) { + var data struct { + Id uint `json:"id"` + Enabled bool `json:"enabled"` + } + + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + res := h.DB.Model(&model.Menu{}).Where("id", data.Id).UpdateColumn("enabled", data.Enabled) + if res.Error != nil { + resp.ERROR(c, "更新数据库失败!") + return + } + resp.SUCCESS(c) +} + +func (h *MenuHandler) Sort(c *gin.Context) { + var data struct { + Ids []uint `json:"ids"` + Sorts []int `json:"sorts"` + } + + if err := c.ShouldBindJSON(&data); err != nil { + resp.ERROR(c, types.InvalidArgs) + return + } + + for index, id := range data.Ids { + res := h.DB.Model(&model.Menu{}).Where("id", id).Update("sort_num", data.Sorts[index]) + if res.Error != nil { + resp.ERROR(c, "更新数据库失败!") + return + } + } + + resp.SUCCESS(c) +} + +func (h *MenuHandler) Remove(c *gin.Context) { + id := h.GetInt(c, "id", 0) + + if id > 0 { + res := h.DB.Where("id", id).Delete(&model.Menu{}) + if res.Error != nil { + resp.ERROR(c, "更新数据库失败!") + return + } + } + resp.SUCCESS(c) +} diff --git a/api/handler/admin/product_handler.go b/api/handler/admin/product_handler.go index 8e960c24..42eff03f 100644 --- a/api/handler/admin/product_handler.go +++ b/api/handler/admin/product_handler.go @@ -65,21 +65,11 @@ func (h *ProductHandler) Save(c *gin.Context) { resp.SUCCESS(c, itemVo) } -// List 模型列表 +// List 数据列表 func (h *ProductHandler) List(c *gin.Context) { - if err := utils.CheckPermission(c, h.DB); err != nil { - resp.NotPermission(c) - return - } - - session := h.DB.Session(&gorm.Session{}) - enable := h.GetBool(c, "enable") - if enable { - session = session.Where("enabled", enable) - } var items []model.Product var list = make([]vo.Product, 0) - res := session.Order("sort_num ASC").Find(&items) + res := h.DB.Order("sort_num ASC").Find(&items) if res.Error == nil { for _, item := range items { var product vo.Product @@ -128,7 +118,7 @@ func (h *ProductHandler) Sort(c *gin.Context) { } for index, id := range data.Ids { - res := h.DB.Model(&model.Product{}).Where("id = ?", id).Update("sort_num", data.Sorts[index]) + res := h.DB.Model(&model.Product{}).Where("id", id).Update("sort_num", data.Sorts[index]) if res.Error != nil { resp.ERROR(c, "更新数据库失败!") return @@ -142,7 +132,7 @@ func (h *ProductHandler) Remove(c *gin.Context) { id := h.GetInt(c, "id", 0) if id > 0 { - res := h.DB.Where("id = ?", id).Delete(&model.Product{}) + res := h.DB.Where("id", id).Delete(&model.Product{}) if res.Error != nil { resp.ERROR(c, "更新数据库失败!") return diff --git a/api/handler/chat_role_handler.go b/api/handler/chat_role_handler.go index 4e48a60b..a555256d 100644 --- a/api/handler/chat_role_handler.go +++ b/api/handler/chat_role_handler.go @@ -25,9 +25,10 @@ func (h *ChatRoleHandler) List(c *gin.Context) { all := h.GetBool(c, "all") userId := h.GetLoginUserId(c) var roles []model.ChatRole + var roleVos = make([]vo.ChatRole, 0) res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles) if res.Error != nil { - resp.ERROR(c, "No roles found,"+res.Error.Error()) + resp.SUCCESS(c, roleVos) return } @@ -55,8 +56,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) { resp.ERROR(c, "角色解析失败!") return } - // 转成 vo - var roleVos = make([]vo.ChatRole, 0) + for _, r := range roles { if !utils.ContainsStr(roleKeys, r.Key) { continue diff --git a/api/handler/menu_handler.go b/api/handler/menu_handler.go new file mode 100644 index 00000000..b922f54c --- /dev/null +++ b/api/handler/menu_handler.go @@ -0,0 +1,36 @@ +package handler + +import ( + "chatplus/core" + "chatplus/store/model" + "chatplus/store/vo" + "chatplus/utils" + "chatplus/utils/resp" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +type MenuHandler struct { + BaseHandler +} + +func NewMenuHandler(app *core.AppServer, db *gorm.DB) *MenuHandler { + return &MenuHandler{BaseHandler: BaseHandler{App: app, DB: db}} +} + +// List 数据列表 +func (h *MenuHandler) List(c *gin.Context) { + var items []model.Menu + var list = make([]vo.Menu, 0) + res := h.DB.Where("enabled", true).Order("sort_num ASC").Find(&items) + if res.Error == nil { + for _, item := range items { + var product vo.Menu + err := utils.CopyObject(item, &product) + if err == nil { + list = append(list, product) + } + } + } + resp.SUCCESS(c, list) +} diff --git a/api/handler/sd_handler.go b/api/handler/sd_handler.go index 9881a35d..09b35d49 100644 --- a/api/handler/sd_handler.go +++ b/api/handler/sd_handler.go @@ -3,6 +3,7 @@ package handler import ( "chatplus/core" "chatplus/core/types" + "chatplus/service" "chatplus/service/oss" "chatplus/service/sd" "chatplus/store/model" @@ -23,15 +24,17 @@ import ( type SdJobHandler struct { BaseHandler - redis *redis.Client - pool *sd.ServicePool - uploader *oss.UploaderManager + redis *redis.Client + pool *sd.ServicePool + uploader *oss.UploaderManager + snowflake *service.Snowflake } -func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager) *SdJobHandler { +func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager, snowflake *service.Snowflake) *SdJobHandler { return &SdJobHandler{ - pool: pool, - uploader: manager, + pool: pool, + uploader: manager, + snowflake: snowflake, BaseHandler: BaseHandler{ App: app, DB: db, @@ -116,8 +119,13 @@ func (h *SdJobHandler) Image(c *gin.Context) { } idValue, _ := c.Get(types.LoginUserID) userId := utils.IntValue(utils.InterfaceToString(idValue), 0) + taskId, err := h.snowflake.Next(true) + if err != nil { + resp.ERROR(c, "error with generate task id: "+err.Error()) + return + } params := types.SdTaskParams{ - TaskId: fmt.Sprintf("task(%s)", utils.RandString(15)), + TaskId: taskId, Prompt: data.Prompt, NegativePrompt: data.NegativePrompt, Steps: data.Steps, diff --git a/api/main.go b/api/main.go index c5ce4baa..6d82fccb 100644 --- a/api/main.go +++ b/api/main.go @@ -417,6 +417,20 @@ func main() { group := s.Engine.Group("/api/admin/powerLog/") group.POST("list", h.List) }), + fx.Provide(admin.NewMenuHandler), + fx.Invoke(func(s *core.AppServer, h *admin.MenuHandler) { + group := s.Engine.Group("/api/admin/menu/") + group.POST("save", h.Save) + group.GET("list", h.List) + group.POST("enable", h.Enable) + group.POST("sort", h.Sort) + group.GET("remove", h.Remove) + }), + fx.Provide(handler.NewMenuHandler), + fx.Invoke(func(s *core.AppServer, h *handler.MenuHandler) { + group := s.Engine.Group("/api/menu/") + group.GET("list", h.List) + }), fx.Invoke(func(s *core.AppServer, db *gorm.DB) { err := s.Run(db) if err != nil { diff --git a/api/service/mj/pool.go b/api/service/mj/pool.go index 14ea1da4..0143467f 100644 --- a/api/service/mj/pool.go +++ b/api/service/mj/pool.go @@ -181,6 +181,7 @@ func (p *ServicePool) SyncTaskProgress() { CreatedAt: time.Now(), }) } + continue } if servicePlus := p.getService(job.ChannelId); servicePlus != nil { diff --git a/api/service/mj/service.go b/api/service/mj/service.go index 861095d4..f73a9c63 100644 --- a/api/service/mj/service.go +++ b/api/service/mj/service.go @@ -77,6 +77,13 @@ func (s *Service) Run() { } } + var job model.MidJourneyJob + tx := s.db.Where("id = ?", task.Id).First(&job) + if tx.Error != nil { + logger.Error("任务不存在,任务ID:", task.TaskId) + continue + } + logger.Infof("%s handle a new MidJourney task: %+v", s.Name, task) var res ImageRes switch task.Type { @@ -97,8 +104,6 @@ func (s *Service) Run() { break } - var job model.MidJourneyJob - s.db.Where("id = ?", task.Id).First(&job) if err != nil || (res.Code != 1 && res.Code != 22) { errMsg := fmt.Sprintf("%v,%s", err, res.Description) logger.Error("绘画任务执行失败:", errMsg) @@ -127,14 +132,17 @@ func (s *Service) canHandleTask() bool { return handledNum < s.maxHandleTaskNum } -// remove the expired tasks +// remove the timeout tasks func (s *Service) checkTasks() { for k, t := range s.taskStartTimes { if time.Now().Unix()-t.Unix() > s.taskTimeout { delete(s.taskStartTimes, k) atomic.AddInt32(&s.HandledTaskNum, -1) - // delete task from database - s.db.Delete(&model.MidJourneyJob{Id: uint(k)}, "progress < 100") + + s.db.Model(&model.MidJourneyJob{Id: uint(k)}).UpdateColumns(map[string]interface{}{ + "progress": -1, + "err_msg": "任务超时", + }) } } } diff --git a/api/service/sd/service.go b/api/service/sd/service.go index ccb56974..f87067a3 100644 --- a/api/service/sd/service.go +++ b/api/service/sd/service.go @@ -114,6 +114,7 @@ func (s *Service) Txt2Img(task types.SdTask) error { Width: task.Params.Width, Height: task.Params.Height, SamplerName: task.Params.Sampler, + ForceTaskId: task.Params.TaskId, } if task.Params.Seed > 0 { body.Seed = task.Params.Seed diff --git a/api/store/model/menu.go b/api/store/model/menu.go new file mode 100644 index 00000000..e215e204 --- /dev/null +++ b/api/store/model/menu.go @@ -0,0 +1,11 @@ +package model + +// Menu 系统菜单 +type Menu struct { + Id uint `gorm:"primarykey;column:id"` + Name string // 菜单名称 + Icon string // 菜单图标 + URL string // 菜单跳转地址 + SortNum int // 排序 + Enabled bool // 启用状态 +} diff --git a/api/store/vo/menu.go b/api/store/vo/menu.go new file mode 100644 index 00000000..c9975a45 --- /dev/null +++ b/api/store/vo/menu.go @@ -0,0 +1,11 @@ +package vo + +// Menu 系统菜单 +type Menu struct { + Id uint `json:"id"` + Name string `json:"name"` + Icon string `json:"icon"` + URL string `json:"url"` + SortNum int `json:"sort_num"` + Enabled bool `json:"enabled"` +} diff --git a/database/update-v4.0.2.sql b/database/update-v4.0.2.sql new file mode 100644 index 00000000..00d7708f --- /dev/null +++ b/database/update-v4.0.2.sql @@ -0,0 +1,19 @@ +-- 菜单表 +CREATE TABLE `chatgpt_plus`.`chatgpt_menus` ( + `id` INT(11) NOT NULL AUTO_INCREMENT , + `name` VARCHAR(30) NOT NULL COMMENT '菜单名称' , + `icon` VARCHAR(150) NOT NULL COMMENT '菜单图标' , + `url` VARCHAR(100) NOT NULL COMMENT '地址' , + `sort_num` SMALLINT(3) NOT NULL COMMENT '排序' , + `enabled` TINYINT(1) NOT NULL COMMENT '是否启用' , + PRIMARY KEY (`id`)) ENGINE = InnoDB COMMENT = '前端菜单表'; + +INSERT INTO `chatgpt_menus` (`id`, `name`, `icon`, `url`, `sort_num`, `enabled`) VALUES + (1, '对话聊天', '/images/menu/chat.png', '/chat', 0, 1), + (5, 'MJ 绘画', '/images/menu/mj.png', '/mj', 1, 1), + (6, 'SD 绘画', '/images/menu/sd.png', '/sd', 2, 1), + (7, '算力日志', '/images/menu/log.png', '/powerLog', 5, 1), + (8, '应用中心', '/images/menu/app.png', '/apps', 3, 1), + (9, '作品展示', '/images/menu/img-wall.png', '/images-wall', 4, 1), + (10, '会员计划', '/images/menu/member.png', '/member', 6, 1), + (11, '分享计划', '/images/menu/share.png', '/invite', 7, 1); \ No newline at end of file diff --git a/web/public/images/menu/app.png b/web/public/images/menu/app.png new file mode 100644 index 00000000..2785231c Binary files /dev/null and b/web/public/images/menu/app.png differ diff --git a/web/public/images/chat.png b/web/public/images/menu/chat.png similarity index 100% rename from web/public/images/chat.png rename to web/public/images/menu/chat.png diff --git a/web/public/images/menu/img-wall.png b/web/public/images/menu/img-wall.png new file mode 100644 index 00000000..61f9fdb8 Binary files /dev/null and b/web/public/images/menu/img-wall.png differ diff --git a/web/public/images/menu/log.png b/web/public/images/menu/log.png new file mode 100644 index 00000000..03870d66 Binary files /dev/null and b/web/public/images/menu/log.png differ diff --git a/web/public/images/menu/member.png b/web/public/images/menu/member.png new file mode 100644 index 00000000..cceac4ca Binary files /dev/null and b/web/public/images/menu/member.png differ diff --git a/web/public/images/mj.png b/web/public/images/menu/mj.png similarity index 100% rename from web/public/images/mj.png rename to web/public/images/menu/mj.png diff --git a/web/public/images/sd.png b/web/public/images/menu/sd.png similarity index 100% rename from web/public/images/sd.png rename to web/public/images/menu/sd.png diff --git a/web/public/images/menu/share.png b/web/public/images/menu/share.png new file mode 100644 index 00000000..8d84b074 Binary files /dev/null and b/web/public/images/menu/share.png differ diff --git a/web/src/router.js b/web/src/router.js index f4c81e61..968b0690 100644 --- a/web/src/router.js +++ b/web/src/router.js @@ -173,7 +173,7 @@ const routes = [ name: 'admin-manger', meta: {title: '管理员'}, component: () => import('@/views/admin/Manager.vue'), - }, + } ] }, diff --git a/web/src/views/ChatPlus.vue b/web/src/views/ChatPlus.vue index 19a729a6..0914358f 100644 --- a/web/src/views/ChatPlus.vue +++ b/web/src/views/ChatPlus.vue @@ -313,12 +313,16 @@ httpGet("/api/config/get?key=system").then(res => { // 获取系统公告 httpGet("/api/config/get?key=notice").then(res => { - notice.value = md.render(res.data['content']) - const oldNotice = localStorage.getItem(noticeKey.value); - // 如果公告有更新,则显示公告 - if (oldNotice !== notice.value && notice.value.length > 10) { - showNotice.value = true + try { + notice.value = md.render(res.data['content']) + const oldNotice = localStorage.getItem(noticeKey.value); + // 如果公告有更新,则显示公告 + if (oldNotice !== notice.value && notice.value.length > 10) { + showNotice.value = true + } + } catch (e) { } + }).catch(e => { ElMessage.error("获取系统配置失败:" + e.message) }) diff --git a/web/src/views/Home.vue b/web/src/views/Home.vue index db214038..43687e56 100644 --- a/web/src/views/Home.vue +++ b/web/src/views/Home.vue @@ -11,10 +11,9 @@ - - + -
{{ item.title }}
+
{{ item.name }}
@@ -37,21 +36,12 @@ import {ElMessage} from "element-plus"; const router = useRouter(); const logo = ref('/images/logo.png'); -const navs = ref([ - {path: "/chat", icon_path: "/images/chat.png", title: "对话聊天"}, - {path: "/mj", icon_path: "/images/mj.png", title: "MJ 绘画"}, - {path: "/sd", icon_path: "/images/sd.png", title: "SD 绘画"}, - {path: "/apps", icon: "menu", title: "应用中心"}, - {path: "/images-wall", icon: "image-list", title: "作品展示"}, - {path: "/powerLog", icon: "log", title: "消费日志"}, - {path: "/member", icon: "vip-user", title: "会员计划"}, - {path: "/invite", icon: "share", title: "推广计划"}, -]) +const navs = ref([]) const curPath = ref(router.currentRoute.value.path) const changeNav = (item) => { - curPath.value = item.path - router.push(item.path) + curPath.value = item.url + router.push(item.url) } onMounted(() => { @@ -60,6 +50,12 @@ onMounted(() => { }).catch(e => { ElMessage.error("获取系统配置失败:" + e.message) }) + // 获取菜单 + httpGet("/api/menu/list").then(res => { + navs.value = res.data + }).catch(e => { + ElMessage.error("获取系统菜单失败:" + e.message) + }) }) diff --git a/web/src/views/admin/Menu.vue b/web/src/views/admin/Menu.vue new file mode 100644 index 00000000..66123e1d --- /dev/null +++ b/web/src/views/admin/Menu.vue @@ -0,0 +1,247 @@ + + + + + \ No newline at end of file diff --git a/web/src/views/admin/Product.vue b/web/src/views/admin/Product.vue index 3f4bd8a5..eae3e812 100644 --- a/web/src/views/admin/Product.vue +++ b/web/src/views/admin/Product.vue @@ -1,5 +1,5 @@