Merge branch 'main' into dev

This commit is contained in:
RockYang 2024-03-30 11:57:31 +08:00
commit b35d95f0c7
31 changed files with 543 additions and 105 deletions

View File

@ -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/)。

View File

@ -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/") ||

View File

@ -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"` // 微信客服地址

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

View File

@ -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

View File

@ -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

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

View File

@ -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,

View File

@ -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 {

View File

@ -181,6 +181,7 @@ func (p *ServicePool) SyncTaskProgress() {
CreatedAt: time.Now(),
})
}
continue
}
if servicePlus := p.getService(job.ChannelId); servicePlus != nil {

View File

@ -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": "任务超时",
})
}
}
}

View File

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

View File

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

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

View File

@ -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.5GPT-4DALL-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 支持MidJourneyGPTClaudeGoogle 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.5GPT-4DALL-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`

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

View File

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 939 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -173,7 +173,7 @@ const routes = [
name: 'admin-manger',
meta: {title: '管理员'},
component: () => import('@/views/admin/Manager.vue'),
},
}
]
},

View File

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

View File

@ -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>

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

View File

@ -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;

View File

@ -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: []})