Merge branch 'ui' of 172.28.1.6:yangjian/chatgpt-plus into ui

This commit is contained in:
廖彦棋 2024-03-11 13:52:24 +08:00
commit be379b6d63
23 changed files with 8155 additions and 53 deletions

View File

@ -146,6 +146,7 @@ func authorizeMiddleware(s *AppServer, client *redis.Client) gin.HandlerFunc {
if c.Request.URL.Path == "/api/user/login" || if c.Request.URL.Path == "/api/user/login" ||
c.Request.URL.Path == "/api/user/resetPass" || c.Request.URL.Path == "/api/user/resetPass" ||
c.Request.URL.Path == "/api/admin/login" || c.Request.URL.Path == "/api/admin/login" ||
c.Request.URL.Path == "/api/admin/login/captcha" ||
c.Request.URL.Path == "/api/user/register" || c.Request.URL.Path == "/api/user/register" ||
c.Request.URL.Path == "/api/chat/history" || c.Request.URL.Path == "/api/chat/history" ||
c.Request.URL.Path == "/api/chat/detail" || c.Request.URL.Path == "/api/chat/detail" ||

View File

@ -124,8 +124,10 @@ func (c RedisConfig) Url() string {
// Manager 管理员 // Manager 管理员
type Manager struct { type Manager struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
Captcha string `json:"captcha"` // 验证码
CaptchaId string `json:"captcha_id"` // 验证码id
} }
// ChatConfig 系统默认的聊天配置 // ChatConfig 系统默认的聊天配置

View File

@ -27,6 +27,16 @@ require github.com/xxl-job/xxl-job-executor-go v1.2.0
require github.com/bg5t/mydiscordgo v0.28.1 require github.com/bg5t/mydiscordgo v0.28.1
require (
github.com/mojocn/base64Captcha v1.3.1
github.com/shopspring/decimal v1.3.1
)
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect
)
require ( require (
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect

View File

@ -65,6 +65,8 @@ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A= github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptGaCkuDUx6wNykzzlUixGxvkme+H/lnzb+A=
github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
@ -129,6 +131,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mojocn/base64Captcha v1.3.1 h1:2Wbkt8Oc8qjmNJ5GyOfSo4tgVQPsbKMftqASnq8GlT0=
github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ8bRfPWUuz/iY=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
@ -166,6 +170,8 @@ github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUA
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
@ -227,6 +233,8 @@ golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc=
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 h1:TbGuee8sSq15Iguxu4deQ7+Bqq/d2rsQejGcEtADAMQ=
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=

View File

@ -9,6 +9,7 @@ import (
"context" "context"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/mojocn/base64Captcha"
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -36,6 +37,13 @@ func (h *ManagerHandler) Login(c *gin.Context) {
resp.ERROR(c, types.InvalidArgs) resp.ERROR(c, types.InvalidArgs)
return return
} }
// add captcha
if !base64Captcha.DefaultMemStore.Verify(data.CaptchaId, data.Captcha, true) {
resp.ERROR(c, "验证码错误,请重新输入!")
return
}
manager := h.App.Config.Manager manager := h.App.Config.Manager
if data.Username == manager.Username && data.Password == manager.Password { if data.Username == manager.Username && data.Password == manager.Password {
// 创建 token // 创建 token

View File

@ -0,0 +1,147 @@
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 SysUserHandler struct {
handler.BaseHandler
db *gorm.DB
}
func NewSysUserHandler(app *core.AppServer, db *gorm.DB) *SysUserHandler {
h := SysUserHandler{db: db}
h.App = app
return &h
}
// List 用户列表
func (h *SysUserHandler) List(c *gin.Context) {
page := h.GetInt(c, "page", 1)
pageSize := h.GetInt(c, "page_size", 20)
username := h.GetTrim(c, "username")
offset := (page - 1) * pageSize
var items []model.AdminUser
var users = make([]vo.AdminUser, 0)
var total int64
session := h.db.Session(&gorm.Session{})
if username != "" {
session = session.Where("username LIKE ?", "%"+username+"%")
}
// 查询total
session.Model(&model.AdminUser{}).Count(&total)
res := session.Offset(offset).Limit(pageSize).Find(&items)
if res.Error == nil {
for _, item := range items {
var userVo vo.AdminUser
err := utils.CopyObject(item, &userVo)
if err == nil {
userVo.Id = item.Id
userVo.CreatedAt = item.CreatedAt.Unix()
userVo.UpdatedAt = item.UpdatedAt.Unix()
users = append(users, userVo)
} else {
logger.Error(err)
}
}
}
pageVo := vo.NewPage(total, page, pageSize, users)
resp.SUCCESS(c, pageVo)
}
// Save 更新或者新增
func (h *SysUserHandler) Save(c *gin.Context) {
var data struct {
Id uint `json:"id"`
Password string `json:"password"`
Username string `json:"username"`
Status bool `json:"status"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var user = model.AdminUser{}
var res *gorm.DB
var userVo vo.AdminUser
if data.Id > 0 { // 更新
user.Id = data.Id
// 此处需要用 map 更新,用结构体无法更新 0 值
res = h.db.Model(&user).Updates(map[string]interface{}{
"username": data.Username,
"status": data.Status,
})
} else {
salt := utils.RandString(8)
u := model.AdminUser{
Username: data.Username,
Password: utils.GenPassword(data.Password, salt),
Salt: salt,
Status: true,
}
res = h.db.Create(&u)
_ = utils.CopyObject(u, &userVo)
userVo.Id = u.Id
userVo.CreatedAt = u.CreatedAt.Unix()
userVo.UpdatedAt = u.UpdatedAt.Unix()
}
if res.Error != nil {
resp.ERROR(c, "更新数据库失败")
return
}
resp.SUCCESS(c, userVo)
}
// ResetPass 重置密码
func (h *SysUserHandler) ResetPass(c *gin.Context) {
var data struct {
Id uint
Password string
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
var user model.AdminUser
res := h.db.First(&user, data.Id)
if res.Error != nil {
resp.ERROR(c, "No user found")
return
}
password := utils.GenPassword(data.Password, user.Salt)
user.Password = password
res = h.db.Updates(&user)
if res.Error != nil {
resp.ERROR(c)
} else {
resp.SUCCESS(c)
}
}
// Remove 删除
func (h *SysUserHandler) Remove(c *gin.Context) {
id := h.GetInt(c, "id", 0)
if id > 0 {
res := h.db.Where("id = ?", id).Delete(&model.AdminUser{})
if res.Error != nil {
resp.ERROR(c, "删除失败")
return
}
}
resp.SUCCESS(c)
}

View File

@ -0,0 +1,41 @@
package admin
import (
"chatplus/core"
"chatplus/handler"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"github.com/mojocn/base64Captcha"
)
type CaptchaHandler struct {
handler.BaseHandler
}
func NewCaptchaHandler(app *core.AppServer) *CaptchaHandler {
h := CaptchaHandler{}
h.App = app
return &h
}
type CaptchaVo struct {
CaptchaId string `json:"captcha_id"`
PicPath string `json:"pic_path"`
}
// GetCaptcha 获取验证码
func (h *CaptchaHandler) GetCaptcha(c *gin.Context) {
var captchaVo CaptchaVo
driver := base64Captcha.NewDriverDigit(48, 130, 4, 0.4, 10)
cp := base64Captcha.NewCaptcha(driver, base64Captcha.DefaultMemStore)
// b64s是图片的base64编码
id, b64s, err := cp.Generate()
if err != nil {
resp.ERROR(c, "生成验证码错误!")
return
}
captchaVo.CaptchaId = id
captchaVo.PicPath = b64s
resp.SUCCESS(c, captchaVo)
}

View File

@ -7,6 +7,7 @@ import (
"chatplus/store/model" "chatplus/store/model"
"chatplus/utils/resp" "chatplus/utils/resp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/shopspring/decimal"
"gorm.io/gorm" "gorm.io/gorm"
"time" "time"
) )
@ -23,10 +24,11 @@ func NewDashboardHandler(app *core.AppServer, db *gorm.DB) *DashboardHandler {
} }
type statsVo struct { type statsVo struct {
Users int64 `json:"users"` Users int64 `json:"users"`
Chats int64 `json:"chats"` Chats int64 `json:"chats"`
Tokens int `json:"tokens"` Tokens int `json:"tokens"`
Income float64 `json:"income"` Income float64 `json:"income"`
Chart map[string]map[string]float64 `json:"chart"`
} }
func (h *DashboardHandler) Stats(c *gin.Context) { func (h *DashboardHandler) Stats(c *gin.Context) {
@ -67,5 +69,52 @@ func (h *DashboardHandler) Stats(c *gin.Context) {
for _, item := range orders { for _, item := range orders {
stats.Income += item.Amount stats.Income += item.Amount
} }
// 统计7天的订单的图表
startDate := now.Add(-7 * 24 * time.Hour).Format("2006-01-02")
var statsChart = make(map[string]map[string]float64)
//// 初始化
var userStatistic, historyMessagesStatistic, incomeStatistic = make(map[string]float64), make(map[string]float64), make(map[string]float64)
for i := 0; i < 7; i++ {
var initTime = time.Date(now.Year(), now.Month(), now.Day()-i, 0, 0, 0, 0, now.Location()).Format("2006-01-02")
userStatistic[initTime] = float64(0)
historyMessagesStatistic[initTime] = float64(0)
incomeStatistic[initTime] = float64(0)
}
// 统计用户7天增加的曲线
var users []model.User
res = h.db.Model(&model.User{}).Where("created_at > ?", startDate).Find(&users)
if res.Error == nil {
for _, item := range users {
userStatistic[item.CreatedAt.Format("2006-01-02")] += 1
}
}
// 统计7天Token 消耗
res = h.db.Where("created_at > ?", startDate).Find(&historyMessages)
for _, item := range historyMessages {
historyMessagesStatistic[item.CreatedAt.Format("2006-01-02")] += float64(item.Tokens)
}
// 浮点数相加?
// 统计最近7天的众筹
res = h.db.Where("created_at > ?", startDate).Find(&rewards)
for _, item := range rewards {
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
}
// 统计最近7天的订单
res = h.db.Where("status = ?", types.OrderPaidSuccess).Where("created_at > ?", startDate).Find(&orders)
for _, item := range orders {
incomeStatistic[item.CreatedAt.Format("2006-01-02")], _ = decimal.NewFromFloat(incomeStatistic[item.CreatedAt.Format("2006-01-02")]).Add(decimal.NewFromFloat(item.Amount)).Float64()
}
statsChart["users"] = userStatistic
statsChart["historyMessage"] = historyMessagesStatistic
statsChart["orders"] = incomeStatistic
stats.Chart = statsChart
resp.SUCCESS(c, stats) resp.SUCCESS(c, stats)
} }

View File

@ -0,0 +1,97 @@
package admin
import (
"chatplus/core"
"chatplus/handler"
"chatplus/service/oss"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"time"
)
type UploadHandler struct {
handler.BaseHandler
db *gorm.DB
uploaderManager *oss.UploaderManager
}
func NewUploadHandler(app *core.AppServer, db *gorm.DB, manager *oss.UploaderManager) *UploadHandler {
adminHandler := &UploadHandler{db: db, uploaderManager: manager}
adminHandler.App = app
return adminHandler
}
func (h *UploadHandler) Upload(c *gin.Context) {
file, err := h.uploaderManager.GetUploadHandler().PutFile(c, "file")
if err != nil {
resp.ERROR(c, err.Error())
return
}
userId := 0
res := h.db.Create(&model.File{
UserId: userId,
Name: file.Name,
ObjKey: file.ObjKey,
URL: file.URL,
Ext: file.Ext,
Size: file.Size,
CreatedAt: time.Time{},
})
if res.Error != nil || res.RowsAffected == 0 {
resp.ERROR(c, "error with update database: "+res.Error.Error())
return
}
resp.SUCCESS(c, file)
}
func (h *UploadHandler) List(c *gin.Context) {
userId := 0
var items []model.File
var files = make([]vo.File, 0)
h.db.Where("user_id = ?", userId).Find(&items)
if len(items) > 0 {
for _, v := range items {
var file vo.File
err := utils.CopyObject(v, &file)
if err != nil {
logger.Error(err)
continue
}
file.CreatedAt = v.CreatedAt.Unix()
files = append(files, file)
}
}
resp.SUCCESS(c, files)
}
// Remove remove files
func (h *UploadHandler) Remove(c *gin.Context) {
userId := 0
id := h.GetInt(c, "id", 0)
var file model.File
tx := h.db.Where("user_id = ? AND id = ?", userId, id).First(&file)
if tx.Error != nil || file.Id == 0 {
resp.ERROR(c, "file not existed")
return
}
// remove database
tx = h.db.Model(&model.File{}).Delete("id = ?", id)
if tx.Error != nil || tx.RowsAffected == 0 {
resp.ERROR(c, "failed to update database")
return
}
// remove files
objectKey := file.ObjKey
if objectKey == "" {
objectKey = file.URL
}
_ = h.uploaderManager.GetUploadHandler().Delete(objectKey)
resp.SUCCESS(c)
}

View File

@ -124,10 +124,9 @@ func (h *ChatHandler) Clear(c *gin.Context) {
// History 获取聊天历史记录 // History 获取聊天历史记录
func (h *ChatHandler) History(c *gin.Context) { func (h *ChatHandler) History(c *gin.Context) {
chatId := c.Query("chat_id") // 会话 ID chatId := c.Query("chat_id") // 会话 ID
userId := h.GetLoginUserId(c)
var items []model.ChatMessage var items []model.ChatMessage
var messages = make([]vo.HistoryMessage, 0) var messages = make([]vo.HistoryMessage, 0)
res := h.db.Where("user_id = ? AND chat_id = ?", userId, chatId).Find(&items) res := h.db.Debug().Where("chat_id = ?", chatId).Find(&items)
if res.Error != nil { if res.Error != nil {
resp.ERROR(c, "No history message") resp.ERROR(c, "No history message")
return return

View File

@ -276,6 +276,7 @@ func (h *ChatHandler) sendOpenAiMessage(
var res types.ApiError var res types.ApiError
err = json.Unmarshal(body, &res) err = json.Unmarshal(body, &res)
if err != nil { if err != nil {
logger.Debug(string(body))
return fmt.Errorf("error with decode response: %v", err) return fmt.Errorf("error with decode response: %v", err)
} }

View File

@ -33,7 +33,7 @@ func (h *UploadHandler) Upload(c *gin.Context) {
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
res := h.db.Create(&model.File{ res := h.db.Create(&model.File{
UserId: userId, UserId: int(userId),
Name: file.Name, Name: file.Name,
ObjKey: file.ObjKey, ObjKey: file.ObjKey,
URL: file.URL, URL: file.URL,

View File

@ -369,6 +369,30 @@ func main() {
group.GET("token", h.GenToken) group.GET("token", h.GenToken)
}), }),
// 验证码
fx.Provide(admin.NewCaptchaHandler),
fx.Invoke(func(s *core.AppServer, h *admin.CaptchaHandler) {
group := s.Engine.Group("/api/admin/login/")
group.GET("captcha", h.GetCaptcha)
}),
fx.Provide(admin.NewUploadHandler),
fx.Invoke(func(s *core.AppServer, h *admin.UploadHandler) {
s.Engine.POST("/api/admin/upload", h.Upload)
s.Engine.GET("/api/admin/upload/list", h.List)
s.Engine.GET("/api/admin/upload/remove", h.Remove)
}),
//// 系统管理员
//fx.Provide(admin.NewSysUserHandler),
//fx.Invoke(func(s *core.AppServer, h *admin.SysUserHandler) {
// group := s.Engine.Group("/api/admin/sysUser/")
// group.POST("save", h.Save)
// group.GET("list", h.List)
// group.POST("remove", h.Remove)
// group.POST("resetPass", h.ResetPass)
//}),
fx.Provide(handler.NewFunctionHandler), fx.Provide(handler.NewFunctionHandler),
fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) { fx.Invoke(func(s *core.AppServer, h *handler.FunctionHandler) {
group := s.Engine.Group("/api/function/") group := s.Engine.Group("/api/function/")

View File

@ -0,0 +1,11 @@
package model
type AdminUser struct {
BaseModel
Username string
Password string
Salt string // 密码盐
Status bool `gorm:"default:true"` // 当前状态
LastLoginAt int64 // 最后登录时间
LastLoginIp string // 最后登录 IP
}

View File

@ -4,7 +4,7 @@ import "time"
type File struct { type File struct {
Id uint `gorm:"primarykey;column:id"` Id uint `gorm:"primarykey;column:id"`
UserId uint UserId int
Name string Name string
ObjKey string ObjKey string
URL string URL string

View File

@ -0,0 +1,10 @@
package vo
type AdminUser struct {
BaseVo
Username string `json:"username"`
Salt string `json:"salt"` // 密码盐
Status bool `json:"status"` // 当前状态
LastLoginAt int64 `json:"last_login_at"` // 最后登录时间
LastLoginIp string `json:"last_login_ip"` // 最后登录 IP
}

View File

@ -1,5 +1,5 @@
VUE_APP_API_HOST=http://localhost:5678 VUE_APP_API_HOST=http://172.22.11.2:5678
VUE_APP_WS_HOST=ws://localhost:5678 VUE_APP_WS_HOST=ws://172.22.11.2:5678
VUE_APP_USER=18575670125 VUE_APP_USER=18575670125
VUE_APP_PASS=12345678 VUE_APP_PASS=12345678
VUE_APP_ADMIN_USER=admin VUE_APP_ADMIN_USER=admin

7627
web/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -51,6 +51,10 @@
position: absolute; position: absolute;
right: 20px; right: 20px;
} }
.mobile-mj .content .text-line .align-right {
display: flex;
justify-content: right;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content { .mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content {
padding: 0; padding: 0;
position: relative; position: relative;

View File

@ -67,6 +67,11 @@
right 20px right 20px
} }
} }
.align-right {
display flex
justify-content right
}
} }
.running-job-list { .running-job-list {

View File

@ -18,9 +18,6 @@
.task-list-box .task-list-inner .title-tabs .el-tabs__active-bar { .task-list-box .task-list-inner .title-tabs .el-tabs__active-bar {
background-color: #47fff1; background-color: #47fff1;
} }
.task-list-box .task-list-inner .title-tabs .el-tabs__content {
padding: 10px 0;
}
.task-list-box .task-list-inner .el-textarea { .task-list-box .task-list-inner .el-textarea {
--el-input-focus-border-color: #47fff1; --el-input-focus-border-color: #47fff1;
} }
@ -51,26 +48,49 @@
.task-list-box .task-list-inner .el-form-item__label { .task-list-box .task-list-inner .el-form-item__label {
color: #fff; color: #fff;
} }
.task-list-box .task-list-inner .img-uploader .el-upload { .task-list-box .task-list-inner .img-inline {
display: flex;
}
.task-list-box .task-list-inner .img-inline .img-uploader .el-upload {
border: 1px dashed var(--el-border-color); border: 1px dashed var(--el-border-color);
border-radius: 6px; border-radius: 6px;
cursor: pointer; cursor: pointer;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
width: 300px; width: 120px;
transition: var(--el-transition-duration-fast); transition: var(--el-transition-duration-fast);
margin-bottom: 20px; margin-bottom: 20px;
} }
.task-list-box .task-list-inner .img-uploader .el-upload:hover { .task-list-box .task-list-inner .img-inline .img-uploader .el-upload:hover {
border-color: var(--el-color-primary); border-color: var(--el-color-primary);
} }
.task-list-box .task-list-inner .img-uploader .el-upload .el-icon.uploader-icon { .task-list-box .task-list-inner .img-inline .img-uploader .el-upload .el-icon.uploader-icon {
font-size: 28px; font-size: 28px;
color: #8c939d; color: #8c939d;
width: 100%; width: 100%;
height: 120px; height: 120px;
text-align: center; text-align: center;
} }
.task-list-box .task-list-inner .img-inline .img-list-box {
display: flex;
}
.task-list-box .task-list-inner .img-inline .img-list-box .img-item {
width: 120px;
position: relative;
margin-right: 10px;
}
.task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-image {
width: 120px;
height: 120px;
border-radius: 5px;
}
.task-list-box .task-list-inner .img-inline .img-list-box .img-item .el-button {
position: absolute;
right: 5px;
top: 5px;
width: 20px;
height: 20px;
}
.task-list-box .task-list-inner .submit-btn { .task-list-box .task-list-inner .submit-btn {
display: flex; display: flex;
margin: 20px 0; margin: 20px 0;
@ -84,17 +104,17 @@
justify-content: right; justify-content: right;
align-items: center; align-items: center;
} }
.task-list-box .running-job-list .job-item { .task-list-box .task-list-inner .job-list-box .running-job-list .job-item {
width: 100%; width: 100%;
padding: 2px; padding: 2px;
background-color: #555; background-color: #555;
} }
.task-list-box .running-job-list .job-item .job-item-inner { .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner {
position: relative; position: relative;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
.task-list-box .running-job-list .job-item .job-item-inner .progress { .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress {
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -104,11 +124,11 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
.task-list-box .running-job-list .job-item .job-item-inner .progress span { .task-list-box .task-list-inner .job-list-box .running-job-list .job-item .job-item-inner .progress span {
font-size: 20px; font-size: 20px;
color: #fff; color: #fff;
} }
.task-list-box .finish-job-list .job-item { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item {
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 1px solid #666; border: 1px solid #666;
@ -116,18 +136,19 @@
overflow: hidden; overflow: hidden;
border-radius: 6px; border-radius: 6px;
transition: all 0.3s ease; /* 添加过渡效果 */ transition: all 0.3s ease; /* 添加过渡效果 */
position: relative;
} }
.task-list-box .finish-job-list .job-item .opt .opt-line { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line {
margin: 6px 0; margin: 6px 0;
} }
.task-list-box .finish-job-list .job-item .opt .opt-line ul { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul {
display: flex; display: flex;
flex-flow: row; flex-flow: row;
} }
.task-list-box .finish-job-list .job-item .opt .opt-line ul li { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li {
margin-right: 6px; margin-right: 6px;
} }
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a {
padding: 3px 0; padding: 3px 0;
width: 40px; width: 40px;
text-align: center; text-align: center;
@ -137,50 +158,58 @@
background-color: #4e5058; background-color: #4e5058;
color: #fff; color: #fff;
} }
.task-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul li a:hover {
background-color: #6d6f78; background-color: #6d6f78;
} }
.task-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .opt .opt-line ul .show-prompt {
font-size: 20px; font-size: 20px;
cursor: pointer; cursor: pointer;
} }
.task-list-box .finish-job-list .animate:hover { .task-list-box .task-list-inner .job-list-box .finish-job-list .job-item .remove {
display: none;
position: absolute;
right: 10px;
top: 10px;
}
.task-list-box .task-list-inner .job-list-box .finish-job-list .job-item:hover .remove {
display: block;
}
.task-list-box .task-list-inner .job-list-box .finish-job-list .animate:hover {
box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */ box-shadow: 0 0 10px rgba(71,255,241,0.6); /* 添加阴影效果 */
transform: translateY(-10px); /* 向上移动10像素 */ transform: translateY(-10px); /* 向上移动10像素 */
} }
.task-list-box .el-image { .task-list-box .task-list-inner .job-list-box .el-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: visible; overflow: visible;
} }
.task-list-box .el-image img { .task-list-box .task-list-inner .job-list-box .el-image img {
height: 240px; height: 240px;
} }
.task-list-box .el-image .el-image-viewer__wrapper img { .task-list-box .task-list-inner .job-list-box .el-image .el-image-viewer__wrapper img {
width: auto; width: auto;
height: auto; height: auto;
} }
.task-list-box .el-image .image-slot { .task-list-box .task-list-inner .job-list-box .el-image .image-slot {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100%;
min-height: 200px; min-height: 200px;
color: #fff; color: #fff;
height: 240px; height: 240px;
} }
.task-list-box .el-image .image-slot .iconfont { .task-list-box .task-list-inner .job-list-box .el-image .image-slot .iconfont {
font-size: 50px; font-size: 50px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.task-list-box .el-image.upscale { .task-list-box .task-list-inner .job-list-box .el-image.upscale {
max-height: 310px; max-height: 310px;
} }
.task-list-box .el-image.upscale img { .task-list-box .task-list-inner .job-list-box .el-image.upscale img {
height: 310px; height: 310px;
} }
.task-list-box .el-image.upscale .el-image-viewer__wrapper img { .task-list-box .task-list-inner .job-list-box .el-image.upscale .el-image-viewer__wrapper img {
width: auto; width: auto;
height: auto; height: auto;
} }

View File

@ -87,8 +87,8 @@ const handleKeyup = (e) => {
}; };
const login = function () { const login = function () {
if (!validateMobile(username.value) && !validateEmail(username.value)) { if (username.value.trim() === '') {
return ElMessage.error("请输入合法的手机号/邮箱地址") return ElMessage.error("请输入用户民")
} }
if (password.value.trim() === '') { if (password.value.trim() === '') {
return ElMessage.error('请输入密码'); return ElMessage.error('请输入密码');

View File

@ -62,14 +62,18 @@
</div> </div>
<div class="text-line"> <div class="text-line">
<van-field <van-field v-model="params.prompt"
v-model="params.prompt" rows="3"
rows="3" label="提示词"
autosize autosize
label="提示词" type="textarea"
type="textarea" placeholder="如:一个美丽的中国女孩站在电影院门口,手上拿着爆米花,微笑,写实风格,电影灯光效果,半身像">
placeholder="如:一个美丽的中国女孩站在电影院门口,手上拿着爆米花,微笑,写实风格,电影灯光效果,半身像" <template #button>
/> <van-button v-if="translating" disabled loading type="primary"/>
<van-button v-else size="small" type="primary" @click="translatePrompt">翻译</van-button>
</template>
</van-field>
</div> </div>
<van-collapse v-model="activeColspan"> <van-collapse v-model="activeColspan">
@ -192,7 +196,15 @@
<script setup> <script setup>
import {onMounted, ref} from "vue"; import {onMounted, ref} from "vue";
import {showConfirmDialog, showFailToast, showNotify, showToast, showDialog, showImagePreview} from "vant"; import {
showConfirmDialog,
showFailToast,
showNotify,
showToast,
showDialog,
showImagePreview,
showSuccessToast
} from "vant";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import Compressor from "compressorjs"; import Compressor from "compressorjs";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
@ -449,10 +461,10 @@ const publishImage = (item, action) => {
text = "取消发布" text = "取消发布"
} }
httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => { httpPost("/api/mj/publish", {id: item.id, action: action}).then(() => {
ElMessage.success(text + "成功") showSuccessToast(text + "成功")
item.publish = action item.publish = action
}).catch(e => { }).catch(e => {
ElMessage.error(text + "失败:" + e.message) showFailToast(text + "失败:" + e.message)
}) })
} }
@ -469,6 +481,23 @@ const imageView = (item) => {
showImagePreview([item['img_url']]); showImagePreview([item['img_url']]);
} }
const translating = ref(false)
const translatePrompt = () => {
if (params.value.prompt === '') {
return showToast("请输入中文提示词!")
}
translating.value = true
const prompt = params.value.prompt
httpPost("/api/prompt/translate", {"prompt": prompt}).then(res => {
params.value.prompt = res.data
translating.value = false
}).catch(e => {
translating.value = false
showFailToast("翻译失败:" + e.message)
})
}
</script> </script>
<style lang="stylus"> <style lang="stylus">