Merge branch 'main' into dev
@@ -73,7 +73,7 @@ ChatGLM,讯飞星火,文心一言等多个平台的大语言模型。集成了
 | 
			
		||||
**演示站不提供任何充值点卡售卖或者VIP充值服务。** 如果您体验过后觉得还不错的话,可以花两分钟用下面的一键部署脚本自己部署一套。
 | 
			
		||||
 | 
			
		||||
```shell
 | 
			
		||||
bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v3.2.7-6c232bdaf8.sh)"
 | 
			
		||||
bash -c "$(curl -fsSL https://img.r9it.com/tmp/install-v4.0.1-5f1a7c3fc9.sh)"
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
最新版本的一键部署脚本请参考 [**ChatGPT-Plus 文档**](https://ai.r9it.com/docs/install/)。
 | 
			
		||||
 
 | 
			
		||||
@@ -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/") ||
 | 
			
		||||
 
 | 
			
		||||
@@ -144,11 +144,11 @@ 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 操作(放大,变换)消耗算力
 | 
			
		||||
	MjActionPower int `json:"mj_action_power,omitempty"` // MJ 操作(放大,变换)消耗算力
 | 
			
		||||
	SdPower       int `json:"sd_power,omitempty"`        // SD 绘画消耗算力
 | 
			
		||||
	DallPower     int `json:"dall_power,omitempty"`      // DALLE3 绘图消耗算力
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								api/handler/admin/menu_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										36
									
								
								api/handler/menu_handler.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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)
 | 
			
		||||
}
 | 
			
		||||
@@ -3,6 +3,7 @@ package handler
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/core"
 | 
			
		||||
	"chatplus/core/types"
 | 
			
		||||
	"chatplus/service"
 | 
			
		||||
	"chatplus/service/oss"
 | 
			
		||||
	"chatplus/service/sd"
 | 
			
		||||
	"chatplus/store/model"
 | 
			
		||||
@@ -26,12 +27,14 @@ type SdJobHandler struct {
 | 
			
		||||
	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,
 | 
			
		||||
		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,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								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 {
 | 
			
		||||
 
 | 
			
		||||
@@ -181,6 +181,7 @@ func (p *ServicePool) SyncTaskProgress() {
 | 
			
		||||
							CreatedAt: time.Now(),
 | 
			
		||||
						})
 | 
			
		||||
					}
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				if servicePlus := p.getService(job.ChannelId); servicePlus != nil {
 | 
			
		||||
 
 | 
			
		||||
@@ -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":  "任务超时",
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								api/store/model/menu.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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   // 启用状态
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								api/store/vo/menu.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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"`
 | 
			
		||||
}
 | 
			
		||||
@@ -1,11 +1,5 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"chatplus/utils"
 | 
			
		||||
	"fmt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	text := "一只 蜗牛在树干上爬,阳光透过树叶照在蜗牛的背上 --ar 1:1 --iw 0.250000 --v 6"
 | 
			
		||||
	fmt.Println(utils.HasChinese(text))
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								database/update-v4.0.2.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -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);
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
-- https://www.phpmyadmin.net/
 | 
			
		||||
--
 | 
			
		||||
-- 主机: localhost:3307
 | 
			
		||||
-- 生成日期: 2024-03-28 17:27:20
 | 
			
		||||
-- 生成日期: 2024-03-29 17:26:02
 | 
			
		||||
-- 服务器版本: 8.0.33
 | 
			
		||||
-- PHP 版本: 8.1.18
 | 
			
		||||
 | 
			
		||||
@@ -47,7 +47,7 @@ CREATE TABLE `chatgpt_admin_users` (
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_admin_users` (`id`, `username`, `password`, `salt`, `status`, `last_login_at`, `last_login_ip`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, 1711594316, '172.22.11.200', '2024-03-11 16:30:20', '2024-03-28 10:51:57'),
 | 
			
		||||
(1, 'admin', '6d17e80c87d209efb84ca4b2e0824f549d09fac8b2e1cc698de5bb5e1d75dfd0', 'mmrql75o', 1, 1711704342, '::1', '2024-03-11 16:30:20', '2024-03-29 17:25:42'),
 | 
			
		||||
(108, 'test', '9ed720ce03e0a69885455271b4b3e1710bff79434f2a95d0de6406dd88cc9f79', '4b9orqjh', 0, 1710396975, '::1', '2024-03-13 16:06:43', '2024-03-21 15:15:04');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
@@ -71,21 +71,6 @@ CREATE TABLE `chatgpt_api_keys` (
 | 
			
		||||
  `updated_at` datetime NOT NULL
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='OpenAI API ';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_api_keys` (`id`, `platform`, `name`, `value`, `type`, `last_used_at`, `api_url`, `enabled`, `proxy_url`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(11, 'ChatGLM', '智普', 'c3694f748a33d4d163bfa4b77ec1a153.p6s2NUZf5kAb0M6w', 'chat', 1710498745, 'https://open.bigmodel.cn/api/paas/v3/model-api/{model}/sse-invoke', 1, '', '2023-09-04 16:25:54', '2024-03-15 13:54:00'),
 | 
			
		||||
(14, 'Baidu', '千帆', 'Wdpi8o3T8UWrxgVqjtq7HODS|SeseBNp9Y5rQdwlVG60isEofEMT8y9zz', 'chat', 1699519822, 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/{model}', 0, '', '2023-10-10 18:11:50', '2024-03-15 13:53:54'),
 | 
			
		||||
(15, 'XunFei', '星火', 'b54dd11b|dbdcf59c74f8ea05e1792cad54409065|MTAxNjQ1MTVlODliNWM0NTVkOWM2ZTVm', 'chat', 1710497804, 'wss://spark-api.xf-yun.com/{version}/chat', 1, '', '2023-10-11 15:10:09', '2024-03-15 13:54:13'),
 | 
			
		||||
(22, 'OpenAI', '官方', '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', 'img', 0, 'https://api.openai.com/v1/images/generations', 0, 'http://127.0.0.1:7777', '2023-12-07 11:48:36', '2024-03-18 15:32:58'),
 | 
			
		||||
(24, 'OpenAI', '官方', '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', 'chat', 1710747118, 'https://api.openai.com/v1/chat/completions', 0, 'http://127.0.0.1:7777', '2024-01-04 10:10:58', '2024-03-18 15:32:59'),
 | 
			
		||||
(25, 'OpenAI', '极客学长', 'sk-qR5kdnAhTPuiXf2r7d9bEbBdD6F64d9e81Dd9c75D716BdD4', 'chat', 1711586301, 'https://api.chat-plus.net/v1/chat/completions', 1, '', '2024-01-04 15:58:30', '2024-03-18 15:33:02'),
 | 
			
		||||
(26, 'OpenAI', '极客学长', 'sk-n3JghlwzY6JaTIbv7064F94155F942D386F3F685F6Bd3b97', 'img', 1711007339, 'https://gpt.bemore.lol/v1/images/generations', 1, '', '2024-01-05 14:31:45', '2024-03-18 15:33:03'),
 | 
			
		||||
(27, 'QWen', '通义千问', 'sk-6241c622828c44b1a2c70f495b16efac', 'chat', 1711334875, 'https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation', 1, '', '2024-01-19 10:39:22', '2024-03-15 11:55:02'),
 | 
			
		||||
(28, 'OpenAI', '丈母娘', 'sk-tAKgJwKbi9cHzFMdC9C34f0207B74225977256F65972Fb94', 'img', 1708593118, 'https://www.jiujiuai.net/v1/images/generations', 0, '', '2024-02-22 17:06:02', '2024-02-23 09:29:26');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
@@ -172,7 +157,8 @@ INSERT INTO `chatgpt_chat_models` (`id`, `platform`, `name`, `value`, `sort_num`
 | 
			
		||||
(18, 'QWen', '通义千问-Plus', 'qwen-plus', 8, 1, 1, 1.0, 1024, 32768, 1, '2024-01-19 10:42:49', '2024-03-18 14:27:19'),
 | 
			
		||||
(19, 'QWen', '通义千问-Max', 'qwen-max-1201', 9, 1, 1, 1.0, 1024, 32768, 1, '2024-01-19 10:51:03', '2024-03-18 14:27:19'),
 | 
			
		||||
(21, 'OpenAI', '董宇辉小作文助手', 'gpt-4-gizmo-g-dse9iXvor', 6, 1, 30, 1.0, 8192, 32768, 0, '2024-03-18 14:24:20', '2024-03-18 14:27:19'),
 | 
			
		||||
(22, 'OpenAI', 'LOGO生成神器', 'gpt-4-gizmo-g-YL87j8C7S', 0, 1, 30, 1.0, 1024, 4096, 1, '2024-03-20 14:02:11', '2024-03-20 14:02:18');
 | 
			
		||||
(22, 'OpenAI', 'LOGO生成神器', 'gpt-4-gizmo-g-YL87j8C7S', 0, 1, 30, 1.0, 1024, 4096, 1, '2024-03-20 14:02:11', '2024-03-20 14:02:18'),
 | 
			
		||||
(23, 'OpenAI', '音乐生成器', 'suno-v3', 0, 1, 50, 0.8, 1024, 4096, 1, '2024-03-29 15:43:40', '2024-03-29 15:45:15');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
@@ -236,7 +222,7 @@ CREATE TABLE `chatgpt_configs` (
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_configs` (`id`, `marker`, `config_json`) VALUES
 | 
			
		||||
(1, 'system', '{\"title\":\"ChatPlus AI 智能助手\",\"admin_title\":\"ChatPlus 控制台\",\"logo\":\"http://localhost:5678/static/upload/2024/3/1711334798556619.png\",\"init_power\":100,\"daily_power\":99,\"invite_power\":10,\"vip_month_power\":1000,\"register_ways\":[\"mobile\",\"username\",\"email\"],\"enabled_register\":true,\"reward_img\":\"http://localhost:5678/static/upload/2024/3/1710753716309668.jpg\",\"enabled_reward\":true,\"power_price\":0.1,\"order_pay_timeout\":1800,\"vip_info_text\":\"月度会员,年度会员每月赠送 1000 点算力,赠送算力当月有效当月没有消费完的算力不结余到下个月。 点卡充值的算力长期有效。\",\"default_models\":[11,7,1,10,12,19,18,17,3],\"mj_power\":20,\"mj_action_power\":10,\"sd_power\":5,\"dall_power\":15,\"wechat_card_url\":\"/images/wx.png\",\"enable_context\":true,\"context_deep\":4}'),
 | 
			
		||||
(3, 'notice', '{\"content\":\"注意:当前站点仅为开源项目 \\u003ca style=\\\"color: #F56C6C\\\" href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003eChatPlus\\u003c/a\\u003e 的演示项目,本项目单纯就是给大家体验项目功能使用。\\n体验额度用完之后请不要在当前站点进行任何充值操作!!!\\n体验额度用完之后请不要在当前站点进行任何充值操作!!!\\n体验额度用完之后请不要在当前站点进行任何充值操作!!!\\n 如果觉得好用你就花几分钟自己部署一套,没有API KEY 的同学可以去 \\u003ca href=\\\"https://gpt.bemore.lol\\\" target=\\\"_blank\\\"\\n             style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://gpt.bemore.lol\\u003c/a\\u003e 购买,现在有超级优惠,价格远低于 OpenAI 官方。\\nGPT-3.5,GPT-4,DALL-E3 绘图......你都可以随意使用,无需魔法。\\nMidJourney API 购买地址:\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n   style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e\\n接入教程: \\u003ca href=\\\"https://ai.r9it.com/docs/install/\\\" target=\\\"_blank\\\"\\n             style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://ai.r9it.com/docs/install/\\u003c/a\\u003e\\n本项目源码地址:\\u003ca href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/chatgpt-plus\\u003c/a\\u003e\",\"updated\":true}');
 | 
			
		||||
(3, 'notice', '{\"content\":\"系统每日会给免费会员赠送10算力值,用完请第二天再来领取。\\n## v4.0.1 更新日志\\n* 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口,SDAPI 兼容各种 stable-diffusion 发行版,稳定性更强一些\\n* 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API,兼容 MJ-Plus 中转\\n* 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力\\n* Bug修复:修复 iphone 手机无法通过图形验证码的Bug,使用滑动验证码替换\\n* Bug修复:修复手机端 MidJourney 绘画页面滚动条无法滚动的Bug\\n\\n 如果觉得好用你就花几分钟自己部署一套,没有API KEY 的同学可以去\\u003ca href=\\\"https://api.chat-plus.net\\\" target=\\\"_blank\\\"\\n   style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://api.chat-plus.net\\u003c/a\\u003e (支持MidJourney,GPT,Claude,Google Gemmi 各种表格模型) 或者 \\u003ca href=\\\"https://gpt.bemore.lol\\\" target=\\\"_blank\\\"\\n             style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://gpt.bemore.lol\\u003c/a\\u003e(不支持 Midjourney) 购买,现在有超级优惠,价格远低于 OpenAI 官方。关于中转 API 的优势和劣势请参考 [中转API技术原理](https://ai.r9it.com/docs/install/errors-handle.html#%E8%B0%83%E7%94%A8%E4%B8%AD%E8%BD%AC-api-%E6%8A%A5%E9%94%99%E6%97%A0%E5%8F%AF%E7%94%A8%E6%B8%A0%E9%81%93)。\\nGPT-3.5,GPT-4,DALL-E3 绘图......你都可以随意使用,无需魔法。\\n接入教程: \\u003ca href=\\\"https://ai.r9it.com/docs/install/\\\" target=\\\"_blank\\\"\\n             style=\\\"font-size: 20px;color:#F56C6C\\\"\\u003ehttps://ai.r9it.com/docs/install/\\u003c/a\\u003e\\n\\n本项目源码地址:\\u003ca href=\\\"https://github.com/yangjian102621/chatgpt-plus\\\" target=\\\"_blank\\\"\\u003ehttps://github.com/yangjian102621/chatgpt-plus\\u003c/a\\u003e\",\"updated\":true}');
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
@@ -411,18 +397,6 @@ CREATE TABLE `chatgpt_products` (
 | 
			
		||||
  `url` varchar(255) DEFAULT NULL COMMENT '跳转地址'
 | 
			
		||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='会员套餐表';
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 转存表中的数据 `chatgpt_products`
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_products` (`id`, `name`, `price`, `discount`, `days`, `power`, `enabled`, `sales`, `sort_num`, `created_at`, `updated_at`, `app_url`, `url`) VALUES
 | 
			
		||||
(1, '会员1个月', '19.90', '10.00', 30, 0, 1, 2, 0, '2023-08-28 10:48:57', '2024-03-15 15:19:52', 'snssdk1128://ec_goods_detail?promotion_id=3668220199204683930', 'snssdk1128://ec_goods_detail?promotion_id=3668220199204683930'),
 | 
			
		||||
(2, '会员3个月', '140.00', '30.00', 90, 0, 1, 1, 0, '2023-08-28 10:52:22', '2024-02-27 11:34:26', NULL, NULL),
 | 
			
		||||
(3, '会员6个月', '290.00', '100.00', 180, 0, 1, 1, 0, '2023-08-28 10:53:39', '2023-08-31 16:24:36', NULL, NULL),
 | 
			
		||||
(4, '会员12个月', '580.00', '200.00', 365, 0, 1, 1, 0, '2023-08-28 10:54:15', '2023-08-31 16:24:42', NULL, NULL),
 | 
			
		||||
(5, '100算力', '10.00', '9.90', 0, 100, 1, 10, 0, '2023-08-28 10:55:08', '2024-03-15 15:20:29', NULL, 'snssdk1128://ec_goods_detail?promotion_id=3668220199204683930'),
 | 
			
		||||
(6, '200算力', '29.90', '20.00', 0, 200, 1, 1, 0, '2023-12-15 16:55:12', '2024-03-15 15:20:37', NULL, NULL);
 | 
			
		||||
 | 
			
		||||
-- --------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
@@ -496,7 +470,7 @@ CREATE TABLE `chatgpt_users` (
 | 
			
		||||
--
 | 
			
		||||
 | 
			
		||||
INSERT INTO `chatgpt_users` (`id`, `username`, `nickname`, `password`, `avatar`, `salt`, `power`, `expired_time`, `status`, `chat_config_json`, `chat_roles_json`, `chat_models_json`, `last_login_at`, `vip`, `last_login_ip`, `created_at`, `updated_at`) VALUES
 | 
			
		||||
(4, '18575670125', '极客学长@830270', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2024/2/1708682650912429.png', 'ueedue5l', 9434, 1717292086, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"red_book\",\"gpt\",\"programmer\",\"seller\"]', '[1,11]', 1711608964, 1, '::1', '2023-06-12 16:47:17', '2024-03-28 14:56:05'),
 | 
			
		||||
(4, '18575670125', '极客学长@830270', 'ccc3fb7ab61b8b5d096a4a166ae21d121fc38c71bbd1be6173d9ab973214a63b', 'http://localhost:5678/static/upload/2024/2/1708682650912429.png', 'ueedue5l', 9384, 1717292086, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"red_book\",\"gpt\",\"programmer\",\"seller\"]', '[1,11]', 1711698298, 1, '::1', '2023-06-12 16:47:17', '2024-03-29 15:44:58'),
 | 
			
		||||
(91, '18575670126', '极客学长@204872', '5e4050b8dd403f593260395d9edeb9f273dbe92d15dfdd929c4a182e95da10c4', '/images/avatar/user.png', '6fj0otl8', 33, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"gpt\"]', '[1,11]', 1697184324, 1, '::1', '2023-10-13 16:01:56', '2024-03-25 11:07:45'),
 | 
			
		||||
(100, '13777777777', '极客学长@292245', 'dcaf31b154432310bd700349e7de7e9dde2a3d6955a035a01fe527c7917a4f99', '/images/avatar/user.png', 'i8a53f8f', 99, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"gpt\"]', '[1,11]', 0, 0, '', '2023-11-23 16:55:45', '2024-03-18 15:08:12'),
 | 
			
		||||
(102, 'yangjian102621@gmail.com', '极客学长@207163', 'd51cec21942737083943e5c3a8f063dea034e40622ac8bd47d771f13707e4676', '/images/avatar/user.png', 'eqezapgk', 99, 0, 1, '{\"api_keys\":{\"Azure\":\"\",\"ChatGLM\":\"\",\"OpenAI\":\"\"}}', '[\"gpt\"]', '[1,11]', 1704448377, 0, '::1', '2024-01-05 17:48:00', '2024-03-18 15:08:41'),
 | 
			
		||||
@@ -667,7 +641,7 @@ ALTER TABLE `chatgpt_admin_users`
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_api_keys`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_api_keys`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=32;
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_history`
 | 
			
		||||
@@ -685,7 +659,7 @@ ALTER TABLE `chatgpt_chat_items`
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_models`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_chat_models`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=23;
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=24;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_chat_roles`
 | 
			
		||||
@@ -745,7 +719,7 @@ ALTER TABLE `chatgpt_power_logs`
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_products`
 | 
			
		||||
--
 | 
			
		||||
ALTER TABLE `chatgpt_products`
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
 | 
			
		||||
  MODIFY `id` int NOT NULL AUTO_INCREMENT;
 | 
			
		||||
 | 
			
		||||
--
 | 
			
		||||
-- 使用表AUTO_INCREMENT `chatgpt_rewards`
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								web/public/images/menu/app.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 741 B  | 
| 
		 Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/menu/img-wall.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.2 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/menu/log.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 939 B  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/menu/member.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.7 KiB  | 
| 
		 Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB  | 
| 
		 Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								web/public/images/menu/share.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 1.2 KiB  | 
@@ -173,7 +173,7 @@ const routes = [
 | 
			
		||||
                name: 'admin-manger',
 | 
			
		||||
                meta: {title: '管理员'},
 | 
			
		||||
                component: () => import('@/views/admin/Manager.vue'),
 | 
			
		||||
            },
 | 
			
		||||
            }
 | 
			
		||||
        ]
 | 
			
		||||
    },
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -313,12 +313,16 @@ httpGet("/api/config/get?key=system").then(res => {
 | 
			
		||||
 | 
			
		||||
// 获取系统公告
 | 
			
		||||
httpGet("/api/config/get?key=notice").then(res => {
 | 
			
		||||
  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)
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,10 @@
 | 
			
		||||
      </div>
 | 
			
		||||
      <ul class="nav-items">
 | 
			
		||||
        <li v-for="item in navs" :key="item.path">
 | 
			
		||||
          <!--          <el-tooltip effect="light" :content="item.title" placement="right">-->
 | 
			
		||||
          <!--            -->
 | 
			
		||||
          <!--          </el-tooltip>-->
 | 
			
		||||
          <a @click="changeNav(item)" :class="item.path === curPath ? 'active' : ''">
 | 
			
		||||
            <el-image :src="item.icon_path" :width="20" v-if="item.icon_path"/>
 | 
			
		||||
            <i :class="'iconfont icon-' + item.icon" v-else></i>
 | 
			
		||||
            <el-image :src="item.icon" :width="20"/>
 | 
			
		||||
          </a>
 | 
			
		||||
          <div :class="item.path === curPath ? 'title active' : 'title'">{{ item.title }}</div>
 | 
			
		||||
          <div :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</div>
 | 
			
		||||
        </li>
 | 
			
		||||
      </ul>
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -37,21 +33,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 +47,12 @@ onMounted(() => {
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("获取系统配置失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
  // 获取菜单
 | 
			
		||||
  httpGet("/api/menu/list").then(res => {
 | 
			
		||||
    navs.value = res.data
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("获取系统菜单失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										247
									
								
								web/src/views/admin/Menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,247 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container menu" v-loading="loading">
 | 
			
		||||
 | 
			
		||||
    <div class="handle-box">
 | 
			
		||||
      <el-button type="primary" :icon="Plus" @click="add">新增</el-button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <el-row>
 | 
			
		||||
      <el-table :data="items" :row-key="row => row.id" table-layout="auto">
 | 
			
		||||
        <el-table-column prop="name" label="菜单名称">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <span class="sort" :data-id="scope.row.id">{{ scope.row.name }}</span>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column prop="icon" label="菜单图标">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <el-image class="menu-icon" :src="scope.row.icon"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column prop="url" label="菜单URL"/>
 | 
			
		||||
        <el-table-column prop="enabled" label="启用状态">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <el-switch v-model="scope.row['enabled']" @change="enable(scope.row)"/>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
        <el-table-column label="操作" width="180">
 | 
			
		||||
          <template #default="scope">
 | 
			
		||||
            <el-button size="small" type="primary" @click="edit(scope.row)">编辑</el-button>
 | 
			
		||||
            <el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row)" :width="200">
 | 
			
		||||
              <template #reference>
 | 
			
		||||
                <el-button size="small" type="danger">删除</el-button>
 | 
			
		||||
              </template>
 | 
			
		||||
            </el-popconfirm>
 | 
			
		||||
          </template>
 | 
			
		||||
        </el-table-column>
 | 
			
		||||
      </el-table>
 | 
			
		||||
    </el-row>
 | 
			
		||||
 | 
			
		||||
    <el-dialog
 | 
			
		||||
        v-model="showDialog"
 | 
			
		||||
        :title="title"
 | 
			
		||||
        :close-on-click-modal="false"
 | 
			
		||||
    >
 | 
			
		||||
      <el-form :model="item" label-width="120px" ref="formRef" :rules="rules">
 | 
			
		||||
        <el-form-item label="菜单名称:" prop="name">
 | 
			
		||||
          <el-input v-model="item.name" autocomplete="off"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="菜单图标:" prop="icon">
 | 
			
		||||
          <el-input v-model="item.icon" placeholder="菜单图标地址">
 | 
			
		||||
            <template #append>
 | 
			
		||||
              <el-upload
 | 
			
		||||
                  :auto-upload="true"
 | 
			
		||||
                  :show-file-list="false"
 | 
			
		||||
                  :http-request="uploadImg"
 | 
			
		||||
              >
 | 
			
		||||
                <el-icon class="uploader-icon">
 | 
			
		||||
                  <UploadFilled/>
 | 
			
		||||
                </el-icon>
 | 
			
		||||
              </el-upload>
 | 
			
		||||
            </template>
 | 
			
		||||
          </el-input>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="菜单URL:" prop="url">
 | 
			
		||||
          <el-input v-model="item.url" autocomplete="off"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
 | 
			
		||||
        <el-form-item label="启用状态:" prop="enable">
 | 
			
		||||
          <el-switch v-model="item.enabled"/>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
      </el-form>
 | 
			
		||||
 | 
			
		||||
      <template #footer>
 | 
			
		||||
            <span class="dialog-footer">
 | 
			
		||||
              <el-button @click="showDialog = false">取消</el-button>
 | 
			
		||||
              <el-button type="primary" @click="save">提交</el-button>
 | 
			
		||||
            </span>
 | 
			
		||||
      </template>
 | 
			
		||||
    </el-dialog>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import {onMounted, reactive, ref} from "vue";
 | 
			
		||||
import {httpGet, httpPost} from "@/utils/http";
 | 
			
		||||
import {ElMessage} from "element-plus";
 | 
			
		||||
import {dateFormat, removeArrayItem} from "@/utils/libs";
 | 
			
		||||
import {Plus, UploadFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import {Sortable} from "sortablejs";
 | 
			
		||||
import Compressor from "compressorjs";
 | 
			
		||||
 | 
			
		||||
// 变量定义
 | 
			
		||||
const items = ref([])
 | 
			
		||||
const item = ref({})
 | 
			
		||||
const showDialog = ref(false)
 | 
			
		||||
const title = ref("")
 | 
			
		||||
const rules = reactive({
 | 
			
		||||
  name: [{required: true, message: '请输入菜单名称', trigger: 'change',}],
 | 
			
		||||
  icon: [{required: true, message: '请上传菜单图标', trigger: 'change',}],
 | 
			
		||||
  url: [{required: true, message: '请输入菜单地址', trigger: 'change',}],
 | 
			
		||||
})
 | 
			
		||||
const loading = ref(true)
 | 
			
		||||
const formRef = ref(null)
 | 
			
		||||
 | 
			
		||||
const fetchData = () => {
 | 
			
		||||
  // 获取数据
 | 
			
		||||
  httpGet('/api/admin/menu/list').then((res) => {
 | 
			
		||||
    if (res.data) {
 | 
			
		||||
      // 初始化数据
 | 
			
		||||
      const arr = res.data;
 | 
			
		||||
      for (let i = 0; i < arr.length; i++) {
 | 
			
		||||
        arr[i].last_used_at = dateFormat(arr[i].last_used_at)
 | 
			
		||||
      }
 | 
			
		||||
      items.value = arr
 | 
			
		||||
    }
 | 
			
		||||
    loading.value = false
 | 
			
		||||
  }).catch(() => {
 | 
			
		||||
    ElMessage.error("获取数据失败");
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  const drawBodyWrapper = document.querySelector('.el-table__body tbody')
 | 
			
		||||
  fetchData()
 | 
			
		||||
 | 
			
		||||
  // 初始化拖动排序插件
 | 
			
		||||
  Sortable.create(drawBodyWrapper, {
 | 
			
		||||
    sort: true,
 | 
			
		||||
    animation: 500,
 | 
			
		||||
    onEnd({newIndex, oldIndex, from}) {
 | 
			
		||||
      if (oldIndex === newIndex) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const sortedData = Array.from(from.children).map(row => row.querySelector('.sort').getAttribute('data-id'));
 | 
			
		||||
      const ids = []
 | 
			
		||||
      const sorts = []
 | 
			
		||||
      sortedData.forEach((id, index) => {
 | 
			
		||||
        ids.push(parseInt(id))
 | 
			
		||||
        sorts.push(index)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      httpPost("/api/admin/menu/sort", {ids: ids, sorts: sorts}).catch(e => {
 | 
			
		||||
        ElMessage.error("排序失败:" + e.message)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const add = function () {
 | 
			
		||||
  title.value = "新增菜单"
 | 
			
		||||
  showDialog.value = true
 | 
			
		||||
  item.value = {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const edit = function (row) {
 | 
			
		||||
  title.value = "修改菜单"
 | 
			
		||||
  showDialog.value = true
 | 
			
		||||
  item.value = row
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const save = function () {
 | 
			
		||||
  formRef.value.validate((valid) => {
 | 
			
		||||
    if (valid) {
 | 
			
		||||
      showDialog.value = false
 | 
			
		||||
      if (!item.value.id) {
 | 
			
		||||
        item.value.sort_num = items.value.length + 1
 | 
			
		||||
      }
 | 
			
		||||
      httpPost('/api/admin/menu/save', item.value).then(() => {
 | 
			
		||||
        ElMessage.success('操作成功!')
 | 
			
		||||
        fetchData()
 | 
			
		||||
      }).catch((e) => {
 | 
			
		||||
        ElMessage.error('操作失败,' + e.message)
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const enable = (row) => {
 | 
			
		||||
  httpPost('/api/admin/menu/enable', {id: row.id, enabled: row.enabled}).then(() => {
 | 
			
		||||
    ElMessage.success("操作成功!")
 | 
			
		||||
  }).catch(e => {
 | 
			
		||||
    ElMessage.error("操作失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const remove = function (row) {
 | 
			
		||||
  httpGet('/api/admin/menu/remove?id=' + row.id).then(() => {
 | 
			
		||||
    ElMessage.success("删除成功!")
 | 
			
		||||
    items.value = removeArrayItem(items.value, row, (v1, v2) => {
 | 
			
		||||
      return v1.id === v2.id
 | 
			
		||||
    })
 | 
			
		||||
  }).catch((e) => {
 | 
			
		||||
    ElMessage.error("删除失败:" + e.message)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 图片上传
 | 
			
		||||
const uploadImg = (file) => {
 | 
			
		||||
  // 压缩图片并上传
 | 
			
		||||
  new Compressor(file.file, {
 | 
			
		||||
    quality: 0.6,
 | 
			
		||||
    success(result) {
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append('file', result, result.name);
 | 
			
		||||
      // 执行上传操作
 | 
			
		||||
      httpPost('/api/admin/upload', formData).then((res) => {
 | 
			
		||||
        item.value.icon = res.data.url
 | 
			
		||||
        ElMessage.success('上传成功')
 | 
			
		||||
      }).catch((e) => {
 | 
			
		||||
        ElMessage.error('上传失败:' + e.message)
 | 
			
		||||
      })
 | 
			
		||||
    },
 | 
			
		||||
    error(e) {
 | 
			
		||||
      ElMessage.error('上传失败:' + e.message)
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.menu {
 | 
			
		||||
 | 
			
		||||
  .opt-box {
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
    display flex;
 | 
			
		||||
    justify-content flex-end
 | 
			
		||||
 | 
			
		||||
    .el-icon {
 | 
			
		||||
      margin-right: 5px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .menu-icon {
 | 
			
		||||
    width 36px
 | 
			
		||||
    height 36px
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .el-select {
 | 
			
		||||
    width: 100%
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="container list" v-loading="loading">
 | 
			
		||||
  <div class="container product" v-loading="loading">
 | 
			
		||||
 | 
			
		||||
    <div class="handle-box">
 | 
			
		||||
      <el-button type="primary" :icon="Plus" @click="add">新增</el-button>
 | 
			
		||||
@@ -153,13 +153,13 @@ onMounted(() => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const add = function () {
 | 
			
		||||
  title.value = "新增模型"
 | 
			
		||||
  title.value = "新增产品"
 | 
			
		||||
  showDialog.value = true
 | 
			
		||||
  item.value = {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const edit = function (row) {
 | 
			
		||||
  title.value = "修改模型"
 | 
			
		||||
  title.value = "修改产品"
 | 
			
		||||
  showDialog.value = true
 | 
			
		||||
  item.value = row
 | 
			
		||||
}
 | 
			
		||||
@@ -206,7 +206,7 @@ const remove = function (row) {
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="stylus" scoped>
 | 
			
		||||
.list {
 | 
			
		||||
.product {
 | 
			
		||||
 | 
			
		||||
  .opt-box {
 | 
			
		||||
    padding-bottom: 10px;
 | 
			
		||||
 
 | 
			
		||||
@@ -253,6 +253,10 @@
 | 
			
		||||
          <el-button type="primary" @click="save('notice')">保存</el-button>
 | 
			
		||||
        </el-form-item>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
 | 
			
		||||
      <el-tab-pane label="菜单配置" name="menu">
 | 
			
		||||
        <Menu/>
 | 
			
		||||
      </el-tab-pane>
 | 
			
		||||
    </el-tabs>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -265,6 +269,7 @@ import {ElMessage} from "element-plus";
 | 
			
		||||
import {InfoFilled, UploadFilled} from "@element-plus/icons-vue";
 | 
			
		||||
import MdEditor from "md-editor-v3";
 | 
			
		||||
import 'md-editor-v3/lib/style.css';
 | 
			
		||||
import Menu from "@/views/admin/Menu.vue";
 | 
			
		||||
 | 
			
		||||
const activeName = ref('basic')
 | 
			
		||||
const system = ref({models: []})
 | 
			
		||||
 
 | 
			
		||||