mirror of
https://github.com/linux-do/new-api.git
synced 2025-09-21 17:56:38 +08:00
commit
d5aca0ae78
@ -34,6 +34,8 @@
|
||||
<img src="https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266" width="500">
|
||||
|
||||
## 界面截图
|
||||

|
||||
|
||||

|
||||

|
||||

|
||||
|
64
common/image.go
Normal file
64
common/image.go
Normal file
@ -0,0 +1,64 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/chai2010/webp"
|
||||
"image"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DecodeBase64ImageData(base64String string) (image.Config, error) {
|
||||
// 去除base64数据的URL前缀(如果有)
|
||||
if idx := strings.Index(base64String, ","); idx != -1 {
|
||||
base64String = base64String[idx+1:]
|
||||
}
|
||||
|
||||
// 将base64字符串解码为字节切片
|
||||
decodedData, err := base64.StdEncoding.DecodeString(base64String)
|
||||
if err != nil {
|
||||
fmt.Println("Error: Failed to decode base64 string")
|
||||
return image.Config{}, err
|
||||
}
|
||||
|
||||
// 创建一个bytes.Buffer用于存储解码后的数据
|
||||
reader := bytes.NewReader(decodedData)
|
||||
config, err := getImageConfig(reader)
|
||||
return config, err
|
||||
}
|
||||
|
||||
func DecodeUrlImageData(imageUrl string) (image.Config, error) {
|
||||
response, err := http.Get(imageUrl)
|
||||
if err != nil {
|
||||
SysLog(fmt.Sprintf("fail to get image from url: %s", err.Error()))
|
||||
return image.Config{}, err
|
||||
}
|
||||
|
||||
// 限制读取的字节数,防止下载整个图片
|
||||
limitReader := io.LimitReader(response.Body, 8192)
|
||||
config, err := getImageConfig(limitReader)
|
||||
response.Body.Close()
|
||||
return config, err
|
||||
}
|
||||
|
||||
func getImageConfig(reader io.Reader) (image.Config, error) {
|
||||
// 读取图片的头部信息来获取图片尺寸
|
||||
config, _, err := image.DecodeConfig(reader)
|
||||
if err != nil {
|
||||
err = errors.New(fmt.Sprintf("fail to decode image config(gif, jpg, png): %s", err.Error()))
|
||||
SysLog(err.Error())
|
||||
config, err = webp.DecodeConfig(reader)
|
||||
if err != nil {
|
||||
err = errors.New(fmt.Sprintf("fail to decode image config(webp): %s", err.Error()))
|
||||
SysLog(err.Error())
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return image.Config{}, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
44
common/pprof.go
Normal file
44
common/pprof.go
Normal file
@ -0,0 +1,44 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"os"
|
||||
"runtime/pprof"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Monitor 定时监控cpu使用率,超过阈值输出pprof文件
|
||||
func Monitor() {
|
||||
for {
|
||||
percent, err := cpu.Percent(time.Second, false)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if percent[0] > 80 {
|
||||
fmt.Println("cpu usage too high")
|
||||
// write pprof file
|
||||
if _, err := os.Stat("./pprof"); os.IsNotExist(err) {
|
||||
err := os.Mkdir("./pprof", os.ModePerm)
|
||||
if err != nil {
|
||||
SysLog("创建pprof文件夹失败 " + err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
f, err := os.Create("./pprof/" + fmt.Sprintf("cpu-%s.pprof", time.Now().Format("20060102150405")))
|
||||
if err != nil {
|
||||
SysLog("创建pprof文件失败 " + err.Error())
|
||||
continue
|
||||
}
|
||||
err = pprof.StartCPUProfile(f)
|
||||
if err != nil {
|
||||
SysLog("启动pprof失败 " + err.Error())
|
||||
continue
|
||||
}
|
||||
time.Sleep(10 * time.Second) // profile for 30 seconds
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
}
|
||||
time.Sleep(30 * time.Second)
|
||||
}
|
||||
}
|
@ -19,12 +19,12 @@ import (
|
||||
func UpdateMidjourneyTask() {
|
||||
//revocer
|
||||
imageModel := "midjourney"
|
||||
for {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Printf("UpdateMidjourneyTask panic: %v", err)
|
||||
}
|
||||
}()
|
||||
for {
|
||||
time.Sleep(time.Duration(15) * time.Second)
|
||||
tasks := model.GetAllUnFinishTasks()
|
||||
if len(tasks) != 0 {
|
||||
@ -55,7 +55,6 @@ func UpdateMidjourneyTask() {
|
||||
// 设置超时时间
|
||||
timeout := time.Second * 5
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
// 使用带有超时的 context 创建新的请求
|
||||
req = req.WithContext(ctx)
|
||||
@ -68,8 +67,8 @@ func UpdateMidjourneyTask() {
|
||||
log.Printf("UpdateMidjourneyTask error: %v", err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
responseBody, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
log.Printf("responseBody: %s", string(responseBody))
|
||||
var responseItem Midjourney
|
||||
// err = json.NewDecoder(resp.Body).Decode(&responseItem)
|
||||
@ -83,12 +82,12 @@ func UpdateMidjourneyTask() {
|
||||
if err1 == nil && err2 == nil {
|
||||
jsonData, err3 := json.Marshal(responseWithoutStatus)
|
||||
if err3 != nil {
|
||||
log.Fatalf("UpdateMidjourneyTask error1: %v", err3)
|
||||
log.Printf("UpdateMidjourneyTask error1: %v", err3)
|
||||
continue
|
||||
}
|
||||
err4 := json.Unmarshal(jsonData, &responseStatus)
|
||||
if err4 != nil {
|
||||
log.Fatalf("UpdateMidjourneyTask error2: %v", err4)
|
||||
log.Printf("UpdateMidjourneyTask error2: %v", err4)
|
||||
continue
|
||||
}
|
||||
responseItem.Status = strconv.Itoa(responseStatus.Status)
|
||||
@ -138,6 +137,7 @@ func UpdateMidjourneyTask() {
|
||||
log.Printf("UpdateMidjourneyTask error5: %v", err)
|
||||
}
|
||||
log.Printf("UpdateMidjourneyTask success: %v", task)
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,7 +147,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
|
||||
var textResponse ImageResponse
|
||||
defer func(ctx context.Context) {
|
||||
if consumeQuota {
|
||||
err := model.PostConsumeTokenQuota(tokenId, userId, quota, 0, true)
|
||||
err := model.PostConsumeTokenQuota(tokenId, userQuota, quota, 0, true)
|
||||
if err != nil {
|
||||
common.SysError("error consuming token remain quota: " + err.Error())
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/chai2010/webp"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/pkoukk/tiktoken-go"
|
||||
"image"
|
||||
@ -75,29 +74,21 @@ func getImageToken(imageUrl MessageImageUrl) (int, error) {
|
||||
if imageUrl.Detail == "low" {
|
||||
return 85, nil
|
||||
}
|
||||
|
||||
response, err := http.Get(imageUrl.Url)
|
||||
var config image.Config
|
||||
var err error
|
||||
if strings.HasPrefix(imageUrl.Url, "http") {
|
||||
common.SysLog(fmt.Sprintf("downloading image: %s", imageUrl.Url))
|
||||
config, err = common.DecodeUrlImageData(imageUrl.Url)
|
||||
} else {
|
||||
common.SysLog(fmt.Sprintf("decoding image"))
|
||||
config, err = common.DecodeBase64ImageData(imageUrl.Url)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println("Error: Failed to get the URL")
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// 限制读取的字节数,防止下载整个图片
|
||||
limitReader := io.LimitReader(response.Body, 8192)
|
||||
|
||||
response.Body.Close()
|
||||
|
||||
// 读取图片的头部信息来获取图片尺寸
|
||||
config, _, err := image.DecodeConfig(limitReader)
|
||||
if err != nil {
|
||||
common.SysLog(fmt.Sprintf("fail to decode image config(gif, jpg, png): %s", err.Error()))
|
||||
config, err = webp.DecodeConfig(limitReader)
|
||||
if err != nil {
|
||||
common.SysLog(fmt.Sprintf("fail to decode image config(webp): %s", err.Error()))
|
||||
}
|
||||
}
|
||||
if config.Width == 0 || config.Height == 0 {
|
||||
return 0, errors.New(fmt.Sprintf("fail to decode image config: %s", err.Error()))
|
||||
return 0, errors.New(fmt.Sprintf("fail to decode image config: %s", imageUrl.Url))
|
||||
}
|
||||
if config.Width < 512 && config.Height < 512 {
|
||||
if imageUrl.Detail == "auto" || imageUrl.Detail == "" {
|
||||
|
@ -79,6 +79,7 @@ func setupLogin(user *model.User, c *gin.Context) {
|
||||
DisplayName: user.DisplayName,
|
||||
Role: user.Role,
|
||||
Status: user.Status,
|
||||
Group: user.Group,
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "",
|
||||
@ -284,6 +285,42 @@ func GenerateAccessToken(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
type TransferAffQuotaRequest struct {
|
||||
Quota int `json:"quota" binding:"required"`
|
||||
}
|
||||
|
||||
func TransferAffQuota(c *gin.Context) {
|
||||
id := c.GetInt("id")
|
||||
user, err := model.GetUserById(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
tran := TransferAffQuotaRequest{}
|
||||
if err := c.ShouldBindJSON(&tran); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
err = user.TransferAffQuotaToQuota(tran.Quota)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "划转失败 " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "划转成功",
|
||||
})
|
||||
}
|
||||
|
||||
func GetAffCode(c *gin.Context) {
|
||||
id := c.GetInt("id")
|
||||
user, err := model.GetUserById(id, true)
|
||||
@ -330,6 +367,28 @@ func GetSelf(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
func GetUserModels(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
id = c.GetInt("id")
|
||||
}
|
||||
user, err := model.GetUserById(id, true)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
models := model.GetGroupModels(user.Group)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"message": "",
|
||||
"data": models,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func UpdateUser(c *gin.Context) {
|
||||
var updatedUser model.User
|
||||
err := json.NewDecoder(c.Request.Body).Decode(&updatedUser)
|
||||
|
5
go.mod
5
go.mod
@ -17,6 +17,7 @@ require (
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/pkoukk/tiktoken-go v0.1.1
|
||||
github.com/samber/lo v1.38.1
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible
|
||||
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2
|
||||
golang.org/x/crypto v0.14.0
|
||||
gorm.io/driver/mysql v1.4.3
|
||||
@ -33,6 +34,7 @@ require (
|
||||
github.com/dlclark/regexp2 v1.8.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.6.0 // indirect
|
||||
@ -54,8 +56,11 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
|
||||
golang.org/x/net v0.17.0 // indirect
|
||||
|
13
go.sum
13
go.sum
@ -33,6 +33,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
@ -131,6 +133,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/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
|
||||
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2 h1:avbt5a8F/zbYwFzTugrqWOBJe/K1cJj6+xpr+x1oVAI=
|
||||
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2/go.mod h1:SiffGCWGGMVwujne2dUQbJ5zUVD1V1Yj0hDuTfqFNEo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
@ -146,6 +150,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
@ -154,6 +162,8 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
@ -165,6 +175,7 @@ golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@ -172,6 +183,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
|
6
main.go
6
main.go
@ -83,6 +83,12 @@ func main() {
|
||||
common.SysLog("batch update enabled with interval " + strconv.Itoa(common.BatchUpdateInterval) + "s")
|
||||
model.InitBatchUpdater()
|
||||
}
|
||||
|
||||
if os.Getenv("ENABLE_PPROF") == "true" {
|
||||
go common.Monitor()
|
||||
common.SysLog("pprof enabled")
|
||||
}
|
||||
|
||||
controller.InitTokenEncoders()
|
||||
|
||||
// Initialize HTTP server
|
||||
|
@ -13,6 +13,17 @@ type Ability struct {
|
||||
Priority *int64 `json:"priority" gorm:"bigint;default:0;index"`
|
||||
}
|
||||
|
||||
func GetGroupModels(group string) []string {
|
||||
var abilities []Ability
|
||||
//去重
|
||||
DB.Where("`group` = ?", group).Distinct("model").Find(&abilities)
|
||||
models := make([]string, 0, len(abilities))
|
||||
for _, ability := range abilities {
|
||||
models = append(models, ability.Model)
|
||||
}
|
||||
return models
|
||||
}
|
||||
|
||||
func GetRandomSatisfiedChannel(group string, model string) (*Channel, error) {
|
||||
ability := Ability{}
|
||||
groupCol := "`group`"
|
||||
|
@ -220,6 +220,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
|
||||
}
|
||||
|
||||
if sendEmail {
|
||||
if (quota + preConsumedQuota) != 0 {
|
||||
quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-(quota+preConsumedQuota) < common.QuotaRemindThreshold
|
||||
noMoreQuota := userQuota-(quota+preConsumedQuota) <= 0
|
||||
if quotaTooLow || noMoreQuota {
|
||||
@ -244,6 +245,7 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !token.UnlimitedQuota {
|
||||
if quota > 0 {
|
||||
|
@ -27,6 +27,9 @@ type User struct {
|
||||
RequestCount int `json:"request_count" gorm:"type:int;default:0;"` // request number
|
||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||
AffCode string `json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
|
||||
AffCount int `json:"aff_count" gorm:"type:int;default:0;column:aff_count"`
|
||||
AffQuota int `json:"aff_quota" gorm:"type:int;default:0;column:aff_quota"` // 邀请剩余额度
|
||||
AffHistoryQuota int `json:"aff_history_quota" gorm:"type:int;default:0;column:aff_history"` // 邀请历史额度
|
||||
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
|
||||
}
|
||||
|
||||
@ -77,6 +80,54 @@ func DeleteUserById(id int) (err error) {
|
||||
return user.Delete()
|
||||
}
|
||||
|
||||
func inviteUser(inviterId int) (err error) {
|
||||
user, err := GetUserById(inviterId, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.AffCount++
|
||||
user.AffQuota += common.QuotaForInviter
|
||||
user.AffHistoryQuota += common.QuotaForInviter
|
||||
return DB.Save(user).Error
|
||||
}
|
||||
|
||||
func (user *User) TransferAffQuotaToQuota(quota int) error {
|
||||
// 检查quota是否小于最小额度
|
||||
if float64(quota) < common.QuotaPerUnit {
|
||||
return fmt.Errorf("转移额度最小为%s!", common.LogQuota(int(common.QuotaPerUnit)))
|
||||
}
|
||||
|
||||
// 开始数据库事务
|
||||
tx := DB.Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback() // 确保在函数退出时事务能回滚
|
||||
|
||||
// 加锁查询用户以确保数据一致性
|
||||
err := tx.Set("gorm:query_option", "FOR UPDATE").First(&user, user.Id).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 再次检查用户的AffQuota是否足够
|
||||
if user.AffQuota < quota {
|
||||
return errors.New("邀请额度不足!")
|
||||
}
|
||||
|
||||
// 更新用户额度
|
||||
user.AffQuota -= quota
|
||||
user.Quota += quota
|
||||
|
||||
// 保存用户状态
|
||||
if err := tx.Save(user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
func (user *User) Insert(inviterId int) error {
|
||||
var err error
|
||||
if user.Password != "" {
|
||||
@ -101,8 +152,9 @@ func (user *User) Insert(inviterId int) error {
|
||||
RecordLog(user.Id, LogTypeSystem, fmt.Sprintf("使用邀请码赠送 %s", common.LogQuota(common.QuotaForInvitee)))
|
||||
}
|
||||
if common.QuotaForInviter > 0 {
|
||||
_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
|
||||
//_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
|
||||
RecordLog(inviterId, LogTypeSystem, fmt.Sprintf("邀请用户赠送 %s", common.LogQuota(common.QuotaForInviter)))
|
||||
_ = inviteUser(inviterId)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
selfRoute.Use(middleware.UserAuth())
|
||||
{
|
||||
selfRoute.GET("/self", controller.GetSelf)
|
||||
selfRoute.GET("/models", controller.GetUserModels)
|
||||
selfRoute.PUT("/self", controller.UpdateSelf)
|
||||
selfRoute.DELETE("/self", controller.DeleteSelf)
|
||||
selfRoute.GET("/token", controller.GenerateAccessToken)
|
||||
@ -46,6 +47,7 @@ func SetApiRouter(router *gin.Engine) {
|
||||
selfRoute.POST("/topup", controller.TopUp)
|
||||
selfRoute.POST("/pay", controller.RequestEpay)
|
||||
selfRoute.POST("/amount", controller.RequestAmount)
|
||||
selfRoute.POST("/aff_transfer", controller.TransferAffQuota)
|
||||
}
|
||||
|
||||
adminRoute := userRoute.Group("/")
|
||||
|
@ -106,7 +106,7 @@ const LogsTable = () => {
|
||||
return (
|
||||
record.type === 0 || record.type === 2 ?
|
||||
<div>
|
||||
<Tag color='grey' size='large' onClick={()=>{
|
||||
<Tag color='grey' size='large' onClick={() => {
|
||||
copyText(text)
|
||||
}}> {text} </Tag>
|
||||
</div>
|
||||
@ -133,7 +133,7 @@ const LogsTable = () => {
|
||||
return (
|
||||
record.type === 0 || record.type === 2 ?
|
||||
<div>
|
||||
<Tag color={stringToColor(text)} size='large' onClick={()=>{
|
||||
<Tag color={stringToColor(text)} size='large' onClick={() => {
|
||||
copyText(text)
|
||||
}}> {text} </Tag>
|
||||
</div>
|
||||
@ -202,11 +202,12 @@ const LogsTable = () => {
|
||||
const [logType, setLogType] = useState(0);
|
||||
const isAdminUser = isAdmin();
|
||||
let now = new Date();
|
||||
// 初始化start_timestamp为前一天
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
model_name: '',
|
||||
start_timestamp: timestamp2string(0),
|
||||
start_timestamp: timestamp2string(now.getTime() / 1000 - 86400),
|
||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
|
||||
channel: ''
|
||||
});
|
||||
@ -338,7 +339,7 @@ const LogsTable = () => {
|
||||
showSuccess('已复制:' + text);
|
||||
} else {
|
||||
// setSearchKeyword(text);
|
||||
Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
|
||||
Modal.error({title: '无法复制到剪贴板,请手动复制', content: text});
|
||||
}
|
||||
}
|
||||
|
||||
@ -412,10 +413,12 @@ const LogsTable = () => {
|
||||
name='model_name'
|
||||
onChange={value => handleInputChange(value, 'model_name')}/>
|
||||
<Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
|
||||
initValue={start_timestamp}
|
||||
value={start_timestamp} type='dateTime'
|
||||
name='start_timestamp'
|
||||
onChange={value => handleInputChange(value, 'start_timestamp')}/>
|
||||
<Form.DatePicker field="end_timestamp" fluid label='结束时间' style={{width: 272}}
|
||||
initValue={end_timestamp}
|
||||
value={end_timestamp} type='dateTime'
|
||||
name='end_timestamp'
|
||||
onChange={value => handleInputChange(value, 'end_timestamp')}/>
|
||||
|
@ -1,10 +1,26 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import {Form, Image, Message} from 'semantic-ui-react';
|
||||
import {Link, useNavigate} from 'react-router-dom';
|
||||
import {API, copy, isRoot, showError, showInfo, showNotice, showSuccess} from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../context/User';
|
||||
import { onGitHubOAuthClicked } from './utils';
|
||||
import {UserContext} from '../context/User';
|
||||
import {onGitHubOAuthClicked} from './utils';
|
||||
import {
|
||||
Avatar, Banner,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Divider,
|
||||
Input, InputNumber,
|
||||
Layout,
|
||||
Modal,
|
||||
Space,
|
||||
Tag,
|
||||
Typography
|
||||
} from "@douyinfe/semi-ui";
|
||||
import {getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor} from "../helpers/render";
|
||||
import EditToken from "../pages/Token/EditToken";
|
||||
import EditUser from "../pages/User/EditUser";
|
||||
|
||||
const PersonalSetting = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
@ -28,8 +44,17 @@ const PersonalSetting = () => {
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const [affLink, setAffLink] = useState("");
|
||||
const [systemToken, setSystemToken] = useState("");
|
||||
const [models, setModels] = useState([]);
|
||||
const [openTransfer, setOpenTransfer] = useState(false);
|
||||
const [transferAmount, setTransferAmount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
// let user = localStorage.getItem('user');
|
||||
// if (user) {
|
||||
// userDispatch({ type: 'login', payload: user });
|
||||
// }
|
||||
// console.log(localStorage.getItem('user'))
|
||||
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
@ -39,6 +64,14 @@ const PersonalSetting = () => {
|
||||
setTurnstileSiteKey(status.turnstile_site_key);
|
||||
}
|
||||
}
|
||||
getUserData().then(
|
||||
(res) => {
|
||||
console.log(userState)
|
||||
}
|
||||
);
|
||||
loadModels().then();
|
||||
getAffLink().then();
|
||||
setTransferAmount(getQuotaPerUnit())
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@ -54,16 +87,15 @@ const PersonalSetting = () => {
|
||||
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||||
}, [disableButton, countdown]);
|
||||
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
const handleInputChange = (name, value) => {
|
||||
setInputs((inputs) => ({...inputs, [name]: value}));
|
||||
};
|
||||
|
||||
const generateAccessToken = async () => {
|
||||
const res = await API.get('/api/user/token');
|
||||
const { success, message, data } = res.data;
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
setSystemToken(data);
|
||||
setAffLink("");
|
||||
await copy(data);
|
||||
showSuccess(`令牌已重置并已复制到剪贴板`);
|
||||
} else {
|
||||
@ -73,18 +105,36 @@ const PersonalSetting = () => {
|
||||
|
||||
const getAffLink = async () => {
|
||||
const res = await API.get('/api/user/aff');
|
||||
const { success, message, data } = res.data;
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
let link = `${window.location.origin}/register?aff=${data}`;
|
||||
setAffLink(link);
|
||||
setSystemToken("");
|
||||
await copy(link);
|
||||
showSuccess(`邀请链接已复制到剪切板`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const getUserData = async () => {
|
||||
let res = await API.get(`/api/user/self`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
userDispatch({type: 'login', payload: data});
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
const loadModels = async () => {
|
||||
let res = await API.get(`/api/user/models`);
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
setModels(data);
|
||||
console.log(data)
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
}
|
||||
|
||||
const handleAffLinkClick = async (e) => {
|
||||
e.target.select();
|
||||
await copy(e.target.value);
|
||||
@ -104,12 +154,12 @@ const PersonalSetting = () => {
|
||||
}
|
||||
|
||||
const res = await API.delete('/api/user/self');
|
||||
const { success, message } = res.data;
|
||||
const {success, message} = res.data;
|
||||
|
||||
if (success) {
|
||||
showSuccess('账户已删除!');
|
||||
await API.get('/api/user/logout');
|
||||
userDispatch({ type: 'logout' });
|
||||
userDispatch({type: 'logout'});
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
} else {
|
||||
@ -122,7 +172,7 @@ const PersonalSetting = () => {
|
||||
const res = await API.get(
|
||||
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('微信账户绑定成功!');
|
||||
setShowWeChatBindModal(false);
|
||||
@ -131,9 +181,33 @@ const PersonalSetting = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const transfer = async () => {
|
||||
if (transferAmount < getQuotaPerUnit()) {
|
||||
showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
|
||||
return;
|
||||
}
|
||||
const res = await API.post(
|
||||
`/api/user/aff_transfer`,
|
||||
{
|
||||
quota: transferAmount
|
||||
}
|
||||
);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess(message);
|
||||
setOpenTransfer(false);
|
||||
getUserData().then();
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
if (inputs.email === '') {
|
||||
showError('请输入邮箱!');
|
||||
return;
|
||||
}
|
||||
setDisableButton(true);
|
||||
if (inputs.email === '') return;
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
return;
|
||||
@ -142,7 +216,7 @@ const PersonalSetting = () => {
|
||||
const res = await API.get(
|
||||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('验证码发送成功,请检查邮箱!');
|
||||
} else {
|
||||
@ -152,35 +226,193 @@ const PersonalSetting = () => {
|
||||
};
|
||||
|
||||
const bindEmail = async () => {
|
||||
if (inputs.email_verification_code === '') return;
|
||||
if (inputs.email_verification_code === '') {
|
||||
showError('请输入邮箱验证码!');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('邮箱账户绑定成功!');
|
||||
setShowEmailBindModal(false);
|
||||
userState.user.email = inputs.email;
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const getUsername = () => {
|
||||
if (userState.user) {
|
||||
return userState.user.username;
|
||||
} else {
|
||||
return 'null';
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
setOpenTransfer(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: '40px' }}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Message>
|
||||
注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
|
||||
</Message>
|
||||
<Button as={Link} to={`/user/edit/`}>
|
||||
更新个人信息
|
||||
<div style={{lineHeight: '40px'}}>
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Modal
|
||||
title="请输入要划转的数量"
|
||||
visible={openTransfer}
|
||||
onOk={transfer}
|
||||
onCancel={handleCancel}
|
||||
maskClosable={false}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text>
|
||||
<Input style={{marginTop: 5}} value={userState?.user?.aff_quota} disabled={true}></Input>
|
||||
</div>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text>{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` + renderQuota(getQuotaPerUnit())}</Typography.Text>
|
||||
<div>
|
||||
<InputNumber min={0} style={{marginTop: 5}} value={transferAmount} onChange={(value)=>setTransferAmount(value)} disabled={false}></InputNumber>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Card
|
||||
title={
|
||||
<Card.Meta
|
||||
avatar={<Avatar size="default" color={stringToColor(getUsername())}
|
||||
style={{marginRight: 4}}>
|
||||
{typeof getUsername() === 'string' && getUsername().slice(0, 1)}
|
||||
</Avatar>}
|
||||
title={<Typography.Text>{getUsername()}</Typography.Text>}
|
||||
description={isRoot()?<Tag color="red">管理员</Tag>:<Tag color="blue">普通用户</Tag>}
|
||||
></Card.Meta>
|
||||
}
|
||||
headerExtraContent={
|
||||
<>
|
||||
<Space vertical align="start">
|
||||
<Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
|
||||
<Tag color="blue">{userState?.user?.group}</Tag>
|
||||
</Space>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<Descriptions row>
|
||||
<Descriptions.Item itemKey="当前余额">{renderQuota(userState?.user?.quota)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="历史消耗">{renderQuota(userState?.user?.used_quota)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="请求次数">{userState.user?.request_count}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
}
|
||||
>
|
||||
<Typography.Title heading={6}>可用模型</Typography.Title>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Space wrap>
|
||||
{models.map((model) => (
|
||||
<Tag key={model} color="cyan">
|
||||
{model}
|
||||
</Tag>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
</Card>
|
||||
<Card
|
||||
footer={
|
||||
<div>
|
||||
<Typography.Text>邀请链接</Typography.Text>
|
||||
<Input
|
||||
style={{marginTop: 10}}
|
||||
value={affLink}
|
||||
onClick={handleAffLinkClick}
|
||||
readOnly
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Typography.Title heading={6}>邀请信息</Typography.Title>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Descriptions row>
|
||||
<Descriptions.Item itemKey="待使用收益">
|
||||
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
|
||||
{
|
||||
renderQuota(userState?.user?.aff_quota)
|
||||
}
|
||||
</span>
|
||||
<Button type={'secondary'} onClick={()=>setOpenTransfer(true)} size={'small'} style={{marginLeft: 10}}>划转</Button>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item>
|
||||
<Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</div>
|
||||
</Card>
|
||||
<Card>
|
||||
<Typography.Title heading={6}>个人信息</Typography.Title>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Typography.Text strong>邮箱</Typography.Text>
|
||||
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||
<div>
|
||||
<Input
|
||||
value={userState.user && userState.user.email !== ''?userState.user.email:'未绑定'}
|
||||
readonly={true}
|
||||
></Input>
|
||||
</div>
|
||||
<div>
|
||||
<Button onClick={()=>{setShowEmailBindModal(true)}} disabled={userState.user && userState.user.email !== ''}>绑定邮箱</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>微信</Typography.Text>
|
||||
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||
<div>
|
||||
<Input
|
||||
value={userState.user && userState.user.wechat_id !== ''?'已绑定':'未绑定'}
|
||||
readonly={true}
|
||||
></Input>
|
||||
</div>
|
||||
<div>
|
||||
<Button disabled={(userState.user && userState.user.wechat_id !== '') || !status.wechat_login}>
|
||||
{
|
||||
status.wechat_login?'绑定':'未启用'
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Typography.Text strong>GitHub</Typography.Text>
|
||||
<div style={{display: 'flex', justifyContent: 'space-between'}}>
|
||||
<div>
|
||||
<Input
|
||||
value={userState.user && userState.user.github_id !== ''?userState.user.github_id:'未绑定'}
|
||||
readonly={true}
|
||||
></Input>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {onGitHubOAuthClicked(status.github_client_id)}}
|
||||
disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
|
||||
>
|
||||
{
|
||||
status.github_oauth?'绑定':'未启用'
|
||||
}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{marginTop: 10}}>
|
||||
<Space>
|
||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
||||
<Button onClick={() => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}>删除个人账户</Button>
|
||||
</Space>
|
||||
|
||||
{systemToken && (
|
||||
<Form.Input
|
||||
@ -188,20 +420,9 @@ const PersonalSetting = () => {
|
||||
readOnly
|
||||
value={systemToken}
|
||||
onClick={handleSystemTokenClick}
|
||||
style={{ marginTop: '10px' }}
|
||||
style={{marginTop: '10px'}}
|
||||
/>
|
||||
)}
|
||||
{affLink && (
|
||||
<Form.Input
|
||||
fluid
|
||||
readOnly
|
||||
value={affLink}
|
||||
onClick={handleAffLinkClick}
|
||||
style={{ marginTop: '10px' }}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<Header as='h3'>账号绑定</Header>
|
||||
{
|
||||
status.wechat_login && (
|
||||
<Button
|
||||
@ -214,15 +435,13 @@ const PersonalSetting = () => {
|
||||
)
|
||||
}
|
||||
<Modal
|
||||
onClose={() => setShowWeChatBindModal(false)}
|
||||
onOpen={() => setShowWeChatBindModal(true)}
|
||||
open={showWeChatBindModal}
|
||||
onCancel={() => setShowWeChatBindModal(false)}
|
||||
// onOpen={() => setShowWeChatBindModal(true)}
|
||||
visible={showWeChatBindModal}
|
||||
size={'mini'}
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Image src={status.wechat_qrcode} fluid />
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<Image src={status.wechat_qrcode} fluid/>
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<p>
|
||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||
</p>
|
||||
@ -239,51 +458,41 @@ const PersonalSetting = () => {
|
||||
绑定
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
{
|
||||
status.github_oauth && (
|
||||
<Button onClick={()=>{onGitHubOAuthClicked(status.github_client_id)}}>绑定 GitHub 账号</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEmailBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定邮箱地址
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
<Modal
|
||||
onClose={() => setShowEmailBindModal(false)}
|
||||
onOpen={() => setShowEmailBindModal(true)}
|
||||
open={showEmailBindModal}
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
onCancel={() => setShowEmailBindModal(false)}
|
||||
// onOpen={() => setShowEmailBindModal(true)}
|
||||
onOk={bindEmail}
|
||||
visible={showEmailBindModal}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Modal.Header>绑定邮箱地址</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
|
||||
<div style={{marginTop: 20, display: 'flex', justifyContent: 'space-between'}}>
|
||||
<Input
|
||||
fluid
|
||||
placeholder='输入邮箱地址'
|
||||
onChange={handleInputChange}
|
||||
onChange={(value)=>handleInputChange('email', value)}
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
||||
/>
|
||||
<Button onClick={sendVerificationCode}
|
||||
disabled={disableButton || loading}>
|
||||
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
<Input
|
||||
fluid
|
||||
placeholder='验证码'
|
||||
name='email_verification_code'
|
||||
value={inputs.email_verification_code}
|
||||
onChange={handleInputChange}
|
||||
onChange={(value)=>handleInputChange('email_verification_code', value)}
|
||||
/>
|
||||
</div>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
@ -294,47 +503,27 @@ const PersonalSetting = () => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||
<Button
|
||||
color=''
|
||||
fluid
|
||||
size='large'
|
||||
onClick={bindEmail}
|
||||
loading={loading}
|
||||
>
|
||||
确认绑定
|
||||
</Button>
|
||||
<div style={{ width: '1rem' }}></div>
|
||||
<Button
|
||||
fluid
|
||||
size='large'
|
||||
onClick={() => setShowEmailBindModal(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
<Modal
|
||||
onClose={() => setShowAccountDeleteModal(false)}
|
||||
onOpen={() => setShowAccountDeleteModal(true)}
|
||||
open={showAccountDeleteModal}
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
onCancel={() => setShowAccountDeleteModal(false)}
|
||||
visible={showAccountDeleteModal}
|
||||
size={'small'}
|
||||
centered={true}
|
||||
onOk={deleteAccount}
|
||||
>
|
||||
<Modal.Header>危险操作</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Message>您正在删除自己的帐户,将清空所有数据且不可恢复</Message>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
<div style={{marginTop: 20}}>
|
||||
<Banner
|
||||
type="danger"
|
||||
description="您正在删除自己的帐户,将清空所有数据且不可恢复"
|
||||
closeIcon={null}
|
||||
/>
|
||||
</div>
|
||||
<div style={{marginTop: 20}}>
|
||||
<Input
|
||||
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
||||
name='self_account_deletion_confirmation'
|
||||
value={inputs.self_account_deletion_confirmation}
|
||||
onChange={handleInputChange}
|
||||
onChange={(value)=>handleInputChange('self_account_deletion_confirmation', value)}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
@ -346,30 +535,13 @@ const PersonalSetting = () => {
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
|
||||
<Button
|
||||
color='red'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={deleteAccount}
|
||||
loading={loading}
|
||||
>
|
||||
确认删除
|
||||
</Button>
|
||||
<div style={{ width: '1rem' }}></div>
|
||||
<Button
|
||||
fluid
|
||||
size='large'
|
||||
onClick={() => setShowAccountDeleteModal(false)}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -37,6 +37,12 @@ export function renderNumber(num) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getQuotaPerUnit() {
|
||||
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
||||
quotaPerUnit = parseFloat(quotaPerUnit);
|
||||
return quotaPerUnit;
|
||||
}
|
||||
|
||||
export function renderQuota(quota, digits = 2) {
|
||||
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
|
@ -1,54 +1,52 @@
|
||||
import React from 'react';
|
||||
import { Segment, Tab } from 'semantic-ui-react';
|
||||
import SystemSetting from '../../components/SystemSetting';
|
||||
import { isRoot } from '../../helpers';
|
||||
import {isRoot} from '../../helpers';
|
||||
import OtherSetting from '../../components/OtherSetting';
|
||||
import PersonalSetting from '../../components/PersonalSetting';
|
||||
import OperationSetting from '../../components/OperationSetting';
|
||||
import {Layout, TabPane, Tabs} from "@douyinfe/semi-ui";
|
||||
|
||||
const Setting = () => {
|
||||
let panes = [
|
||||
{
|
||||
menuItem: '个人设置',
|
||||
render: () => (
|
||||
<Tab.Pane attached={false}>
|
||||
<PersonalSetting />
|
||||
</Tab.Pane>
|
||||
)
|
||||
tab: '个人设置',
|
||||
content: <PersonalSetting/>,
|
||||
itemKey: '1'
|
||||
}
|
||||
];
|
||||
|
||||
if (isRoot()) {
|
||||
panes.push({
|
||||
menuItem: '运营设置',
|
||||
render: () => (
|
||||
<Tab.Pane attached={false}>
|
||||
<OperationSetting />
|
||||
</Tab.Pane>
|
||||
)
|
||||
tab: '运营设置',
|
||||
content: <OperationSetting/>,
|
||||
itemKey: '2'
|
||||
});
|
||||
panes.push({
|
||||
menuItem: '系统设置',
|
||||
render: () => (
|
||||
<Tab.Pane attached={false}>
|
||||
<SystemSetting />
|
||||
</Tab.Pane>
|
||||
)
|
||||
tab: '系统设置',
|
||||
content: <SystemSetting/>,
|
||||
itemKey: '3'
|
||||
});
|
||||
panes.push({
|
||||
menuItem: '其他设置',
|
||||
render: () => (
|
||||
<Tab.Pane attached={false}>
|
||||
<OtherSetting />
|
||||
</Tab.Pane>
|
||||
)
|
||||
tab: '其他设置',
|
||||
content: <OtherSetting/>,
|
||||
itemKey: '4'
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Segment>
|
||||
<Tab menu={{ secondary: true, pointing: true }} panes={panes} />
|
||||
</Segment>
|
||||
<div>
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
<Tabs type="line" defaultActiveKey="1">
|
||||
{panes.map(pane => (
|
||||
<TabPane itemKey={pane.itemKey} tab={pane.tab}>
|
||||
{pane.content}
|
||||
</TabPane>
|
||||
))}
|
||||
</Tabs>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user