Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
0b745373
authored
Nov 22, 2023
by
luxl
Committed by
GitHub
Nov 22, 2023
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8 from Calcium-Ion/main
11
parents
4577235c
580dfd94
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
976 additions
and
546 deletions
+976
-546
README.md
+2
-0
common/image.go
+64
-0
common/pprof.go
+44
-0
controller/midjourney.go
+9
-9
controller/relay-image.go
+1
-1
controller/relay-utils.go
+10
-19
controller/user.go
+59
-0
go.mod
+5
-0
go.sum
+13
-0
main.go
+6
-0
model/ability.go
+11
-0
model/token.go
+22
-20
model/user.go
+53
-1
router/api-router.go
+2
-0
web/src/components/LogsTable.js
+7
-4
web/src/components/PersonalSetting.js
+523
-351
web/src/helpers/render.js
+6
-0
web/src/pages/Setting/index.js
+41
-43
web/src/pages/User/EditUser.js
+98
-98
No files found.
README.md
View file @
0b745373
...
@@ -34,6 +34,8 @@
...
@@ -34,6 +34,8 @@
<img
src=
"https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266"
width=
"500"
>
<img
src=
"https://github.com/Calcium-Ion/new-api/assets/61247483/de536a8a-0161-47a7-a0a2-66ef6de81266"
width=
"500"
>
## 界面截图
## 界面截图







...
...
common/image.go
0 → 100644
View file @
0b745373
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
}
common/pprof.go
0 → 100644
View file @
0b745373
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
)
}
}
controller/midjourney.go
View file @
0b745373
...
@@ -19,12 +19,12 @@ import (
...
@@ -19,12 +19,12 @@ import (
func
UpdateMidjourneyTask
()
{
func
UpdateMidjourneyTask
()
{
//revocer
//revocer
imageModel
:=
"midjourney"
imageModel
:=
"midjourney"
defer
func
()
{
if
err
:=
recover
();
err
!=
nil
{
log
.
Printf
(
"UpdateMidjourneyTask panic: %v"
,
err
)
}
}()
for
{
for
{
defer
func
()
{
if
err
:=
recover
();
err
!=
nil
{
log
.
Printf
(
"UpdateMidjourneyTask panic: %v"
,
err
)
}
}()
time
.
Sleep
(
time
.
Duration
(
15
)
*
time
.
Second
)
time
.
Sleep
(
time
.
Duration
(
15
)
*
time
.
Second
)
tasks
:=
model
.
GetAllUnFinishTasks
()
tasks
:=
model
.
GetAllUnFinishTasks
()
if
len
(
tasks
)
!=
0
{
if
len
(
tasks
)
!=
0
{
...
@@ -55,7 +55,6 @@ func UpdateMidjourneyTask() {
...
@@ -55,7 +55,6 @@ func UpdateMidjourneyTask() {
// 设置超时时间
// 设置超时时间
timeout
:=
time
.
Second
*
5
timeout
:=
time
.
Second
*
5
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
timeout
)
ctx
,
cancel
:=
context
.
WithTimeout
(
context
.
Background
(),
timeout
)
defer
cancel
()
// 使用带有超时的 context 创建新的请求
// 使用带有超时的 context 创建新的请求
req
=
req
.
WithContext
(
ctx
)
req
=
req
.
WithContext
(
ctx
)
...
@@ -68,8 +67,8 @@ func UpdateMidjourneyTask() {
...
@@ -68,8 +67,8 @@ func UpdateMidjourneyTask() {
log
.
Printf
(
"UpdateMidjourneyTask error: %v"
,
err
)
log
.
Printf
(
"UpdateMidjourneyTask error: %v"
,
err
)
continue
continue
}
}
defer
resp
.
Body
.
Close
()
responseBody
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
responseBody
,
err
:=
io
.
ReadAll
(
resp
.
Body
)
resp
.
Body
.
Close
()
log
.
Printf
(
"responseBody: %s"
,
string
(
responseBody
))
log
.
Printf
(
"responseBody: %s"
,
string
(
responseBody
))
var
responseItem
Midjourney
var
responseItem
Midjourney
// err = json.NewDecoder(resp.Body).Decode(&responseItem)
// err = json.NewDecoder(resp.Body).Decode(&responseItem)
...
@@ -83,12 +82,12 @@ func UpdateMidjourneyTask() {
...
@@ -83,12 +82,12 @@ func UpdateMidjourneyTask() {
if
err1
==
nil
&&
err2
==
nil
{
if
err1
==
nil
&&
err2
==
nil
{
jsonData
,
err3
:=
json
.
Marshal
(
responseWithoutStatus
)
jsonData
,
err3
:=
json
.
Marshal
(
responseWithoutStatus
)
if
err3
!=
nil
{
if
err3
!=
nil
{
log
.
Fatal
f
(
"UpdateMidjourneyTask error1: %v"
,
err3
)
log
.
Print
f
(
"UpdateMidjourneyTask error1: %v"
,
err3
)
continue
continue
}
}
err4
:=
json
.
Unmarshal
(
jsonData
,
&
responseStatus
)
err4
:=
json
.
Unmarshal
(
jsonData
,
&
responseStatus
)
if
err4
!=
nil
{
if
err4
!=
nil
{
log
.
Fatal
f
(
"UpdateMidjourneyTask error2: %v"
,
err4
)
log
.
Print
f
(
"UpdateMidjourneyTask error2: %v"
,
err4
)
continue
continue
}
}
responseItem
.
Status
=
strconv
.
Itoa
(
responseStatus
.
Status
)
responseItem
.
Status
=
strconv
.
Itoa
(
responseStatus
.
Status
)
...
@@ -138,6 +137,7 @@ func UpdateMidjourneyTask() {
...
@@ -138,6 +137,7 @@ func UpdateMidjourneyTask() {
log
.
Printf
(
"UpdateMidjourneyTask error5: %v"
,
err
)
log
.
Printf
(
"UpdateMidjourneyTask error5: %v"
,
err
)
}
}
log
.
Printf
(
"UpdateMidjourneyTask success: %v"
,
task
)
log
.
Printf
(
"UpdateMidjourneyTask success: %v"
,
task
)
cancel
()
}
}
}
}
}
}
...
...
controller/relay-image.go
View file @
0b745373
...
@@ -147,7 +147,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
...
@@ -147,7 +147,7 @@ func relayImageHelper(c *gin.Context, relayMode int) *OpenAIErrorWithStatusCode
var
textResponse
ImageResponse
var
textResponse
ImageResponse
defer
func
(
ctx
context
.
Context
)
{
defer
func
(
ctx
context
.
Context
)
{
if
consumeQuota
{
if
consumeQuota
{
err
:=
model
.
PostConsumeTokenQuota
(
tokenId
,
user
Id
,
quota
,
0
,
true
)
err
:=
model
.
PostConsumeTokenQuota
(
tokenId
,
user
Quota
,
quota
,
0
,
true
)
if
err
!=
nil
{
if
err
!=
nil
{
common
.
SysError
(
"error consuming token remain quota: "
+
err
.
Error
())
common
.
SysError
(
"error consuming token remain quota: "
+
err
.
Error
())
}
}
...
...
controller/relay-utils.go
View file @
0b745373
...
@@ -4,7 +4,6 @@ import (
...
@@ -4,7 +4,6 @@ import (
"encoding/json"
"encoding/json"
"errors"
"errors"
"fmt"
"fmt"
"github.com/chai2010/webp"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin"
"github.com/pkoukk/tiktoken-go"
"github.com/pkoukk/tiktoken-go"
"image"
"image"
...
@@ -75,29 +74,21 @@ func getImageToken(imageUrl MessageImageUrl) (int, error) {
...
@@ -75,29 +74,21 @@ func getImageToken(imageUrl MessageImageUrl) (int, error) {
if
imageUrl
.
Detail
==
"low"
{
if
imageUrl
.
Detail
==
"low"
{
return
85
,
nil
return
85
,
nil
}
}
var
config
image
.
Config
response
,
err
:=
http
.
Get
(
imageUrl
.
Url
)
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
{
if
err
!=
nil
{
fmt
.
Println
(
"Error: Failed to get the URL"
)
return
0
,
err
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
{
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
config
.
Width
<
512
&&
config
.
Height
<
512
{
if
imageUrl
.
Detail
==
"auto"
||
imageUrl
.
Detail
==
""
{
if
imageUrl
.
Detail
==
"auto"
||
imageUrl
.
Detail
==
""
{
...
...
controller/user.go
View file @
0b745373
...
@@ -79,6 +79,7 @@ func setupLogin(user *model.User, c *gin.Context) {
...
@@ -79,6 +79,7 @@ func setupLogin(user *model.User, c *gin.Context) {
DisplayName
:
user
.
DisplayName
,
DisplayName
:
user
.
DisplayName
,
Role
:
user
.
Role
,
Role
:
user
.
Role
,
Status
:
user
.
Status
,
Status
:
user
.
Status
,
Group
:
user
.
Group
,
}
}
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
c
.
JSON
(
http
.
StatusOK
,
gin
.
H
{
"message"
:
""
,
"message"
:
""
,
...
@@ -284,6 +285,42 @@ func GenerateAccessToken(c *gin.Context) {
...
@@ -284,6 +285,42 @@ func GenerateAccessToken(c *gin.Context) {
return
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
)
{
func
GetAffCode
(
c
*
gin
.
Context
)
{
id
:=
c
.
GetInt
(
"id"
)
id
:=
c
.
GetInt
(
"id"
)
user
,
err
:=
model
.
GetUserById
(
id
,
true
)
user
,
err
:=
model
.
GetUserById
(
id
,
true
)
...
@@ -330,6 +367,28 @@ func GetSelf(c *gin.Context) {
...
@@ -330,6 +367,28 @@ func GetSelf(c *gin.Context) {
return
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
)
{
func
UpdateUser
(
c
*
gin
.
Context
)
{
var
updatedUser
model
.
User
var
updatedUser
model
.
User
err
:=
json
.
NewDecoder
(
c
.
Request
.
Body
)
.
Decode
(
&
updatedUser
)
err
:=
json
.
NewDecoder
(
c
.
Request
.
Body
)
.
Decode
(
&
updatedUser
)
...
...
go.mod
View file @
0b745373
...
@@ -17,6 +17,7 @@ require (
...
@@ -17,6 +17,7 @@ require (
github.com/gorilla/websocket v1.5.0
github.com/gorilla/websocket v1.5.0
github.com/pkoukk/tiktoken-go v0.1.1
github.com/pkoukk/tiktoken-go v0.1.1
github.com/samber/lo v1.38.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
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2
golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.14.0
gorm.io/driver/mysql v1.4.3
gorm.io/driver/mysql v1.4.3
...
@@ -33,6 +34,7 @@ require (
...
@@ -33,6 +34,7 @@ require (
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/dlclark/regexp2 v1.8.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // 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/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
...
@@ -54,8 +56,11 @@ require (
...
@@ -54,8 +56,11 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.11 // 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/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/net v0.17.0 // indirect
...
...
go.sum
View file @
0b745373
...
@@ -33,6 +33,8 @@ github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwv
...
@@ -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.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 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
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.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
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=
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
...
@@ -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/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 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
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 h1:avbt5a8F/zbYwFzTugrqWOBJe/K1cJj6+xpr+x1oVAI=
github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2/go.mod h1:SiffGCWGGMVwujne2dUQbJ5zUVD1V1Yj0hDuTfqFNEo=
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=
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
...
@@ -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.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
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/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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
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=
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
...
@@ -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.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
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/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.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 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
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
...
@@ -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.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 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
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-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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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
...
@@ -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-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.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.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 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
...
...
main.go
View file @
0b745373
...
@@ -83,6 +83,12 @@ func main() {
...
@@ -83,6 +83,12 @@ func main() {
common
.
SysLog
(
"batch update enabled with interval "
+
strconv
.
Itoa
(
common
.
BatchUpdateInterval
)
+
"s"
)
common
.
SysLog
(
"batch update enabled with interval "
+
strconv
.
Itoa
(
common
.
BatchUpdateInterval
)
+
"s"
)
model
.
InitBatchUpdater
()
model
.
InitBatchUpdater
()
}
}
if
os
.
Getenv
(
"ENABLE_PPROF"
)
==
"true"
{
go
common
.
Monitor
()
common
.
SysLog
(
"pprof enabled"
)
}
controller
.
InitTokenEncoders
()
controller
.
InitTokenEncoders
()
// Initialize HTTP server
// Initialize HTTP server
...
...
model/ability.go
View file @
0b745373
...
@@ -13,6 +13,17 @@ type Ability struct {
...
@@ -13,6 +13,17 @@ type Ability struct {
Priority
*
int64
`json:"priority" gorm:"bigint;default:0;index"`
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
)
{
func
GetRandomSatisfiedChannel
(
group
string
,
model
string
)
(
*
Channel
,
error
)
{
ability
:=
Ability
{}
ability
:=
Ability
{}
groupCol
:=
"`group`"
groupCol
:=
"`group`"
...
...
model/token.go
View file @
0b745373
...
@@ -220,28 +220,30 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
...
@@ -220,28 +220,30 @@ func PostConsumeTokenQuota(tokenId int, userQuota int, quota int, preConsumedQuo
}
}
if
sendEmail
{
if
sendEmail
{
quotaTooLow
:=
userQuota
>=
common
.
QuotaRemindThreshold
&&
userQuota
-
(
quota
+
preConsumedQuota
)
<
common
.
QuotaRemindThreshold
if
(
quota
+
preConsumedQuota
)
!=
0
{
noMoreQuota
:=
userQuota
-
(
quota
+
preConsumedQuota
)
<=
0
quotaTooLow
:=
userQuota
>=
common
.
QuotaRemindThreshold
&&
userQuota
-
(
quota
+
preConsumedQuota
)
<
common
.
QuotaRemindThreshold
if
quotaTooLow
||
noMoreQuota
{
noMoreQuota
:=
userQuota
-
(
quota
+
preConsumedQuota
)
<=
0
go
func
()
{
if
quotaTooLow
||
noMoreQuota
{
email
,
err
:=
GetUserEmail
(
token
.
UserId
)
go
func
()
{
if
err
!=
nil
{
email
,
err
:=
GetUserEmail
(
token
.
UserId
)
common
.
SysError
(
"failed to fetch user email: "
+
err
.
Error
())
}
prompt
:=
"您的额度即将用尽"
if
noMoreQuota
{
prompt
=
"您的额度已用尽"
}
if
email
!=
""
{
topUpLink
:=
fmt
.
Sprintf
(
"%s/topup"
,
common
.
ServerAddress
)
err
=
common
.
SendEmail
(
prompt
,
email
,
fmt
.
Sprintf
(
"%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>"
,
prompt
,
userQuota
,
topUpLink
,
topUpLink
))
if
err
!=
nil
{
if
err
!=
nil
{
common
.
SysError
(
"failed to
send email
"
+
err
.
Error
())
common
.
SysError
(
"failed to
fetch user email:
"
+
err
.
Error
())
}
}
common
.
SysLog
(
"user quota is low, consumed quota: "
+
strconv
.
Itoa
(
quota
)
+
", user quota: "
+
strconv
.
Itoa
(
userQuota
))
prompt
:=
"您的额度即将用尽"
}
if
noMoreQuota
{
}()
prompt
=
"您的额度已用尽"
}
if
email
!=
""
{
topUpLink
:=
fmt
.
Sprintf
(
"%s/topup"
,
common
.
ServerAddress
)
err
=
common
.
SendEmail
(
prompt
,
email
,
fmt
.
Sprintf
(
"%s,当前剩余额度为 %d,为了不影响您的使用,请及时充值。<br/>充值链接:<a href='%s'>%s</a>"
,
prompt
,
userQuota
,
topUpLink
,
topUpLink
))
if
err
!=
nil
{
common
.
SysError
(
"failed to send email"
+
err
.
Error
())
}
common
.
SysLog
(
"user quota is low, consumed quota: "
+
strconv
.
Itoa
(
quota
)
+
", user quota: "
+
strconv
.
Itoa
(
userQuota
))
}
}()
}
}
}
}
}
...
...
model/user.go
View file @
0b745373
...
@@ -27,6 +27,9 @@ type User struct {
...
@@ -27,6 +27,9 @@ type User struct {
RequestCount
int
`json:"request_count" gorm:"type:int;default:0;"`
// request number
RequestCount
int
`json:"request_count" gorm:"type:int;default:0;"`
// request number
Group
string
`json:"group" gorm:"type:varchar(32);default:'default'"`
Group
string
`json:"group" gorm:"type:varchar(32);default:'default'"`
AffCode
string
`json:"aff_code" gorm:"type:varchar(32);column:aff_code;uniqueIndex"`
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"`
InviterId
int
`json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
}
}
...
@@ -77,6 +80,54 @@ func DeleteUserById(id int) (err error) {
...
@@ -77,6 +80,54 @@ func DeleteUserById(id int) (err error) {
return
user
.
Delete
()
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
{
func
(
user
*
User
)
Insert
(
inviterId
int
)
error
{
var
err
error
var
err
error
if
user
.
Password
!=
""
{
if
user
.
Password
!=
""
{
...
@@ -101,8 +152,9 @@ func (user *User) Insert(inviterId int) error {
...
@@ -101,8 +152,9 @@ func (user *User) Insert(inviterId int) error {
RecordLog
(
user
.
Id
,
LogTypeSystem
,
fmt
.
Sprintf
(
"使用邀请码赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInvitee
)))
RecordLog
(
user
.
Id
,
LogTypeSystem
,
fmt
.
Sprintf
(
"使用邀请码赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInvitee
)))
}
}
if
common
.
QuotaForInviter
>
0
{
if
common
.
QuotaForInviter
>
0
{
_
=
IncreaseUserQuota
(
inviterId
,
common
.
QuotaForInviter
)
//
_ = IncreaseUserQuota(inviterId, common.QuotaForInviter)
RecordLog
(
inviterId
,
LogTypeSystem
,
fmt
.
Sprintf
(
"邀请用户赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInviter
)))
RecordLog
(
inviterId
,
LogTypeSystem
,
fmt
.
Sprintf
(
"邀请用户赠送 %s"
,
common
.
LogQuota
(
common
.
QuotaForInviter
)))
_
=
inviteUser
(
inviterId
)
}
}
}
}
return
nil
return
nil
...
...
router/api-router.go
View file @
0b745373
...
@@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
...
@@ -39,6 +39,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute
.
Use
(
middleware
.
UserAuth
())
selfRoute
.
Use
(
middleware
.
UserAuth
())
{
{
selfRoute
.
GET
(
"/self"
,
controller
.
GetSelf
)
selfRoute
.
GET
(
"/self"
,
controller
.
GetSelf
)
selfRoute
.
GET
(
"/models"
,
controller
.
GetUserModels
)
selfRoute
.
PUT
(
"/self"
,
controller
.
UpdateSelf
)
selfRoute
.
PUT
(
"/self"
,
controller
.
UpdateSelf
)
selfRoute
.
DELETE
(
"/self"
,
controller
.
DeleteSelf
)
selfRoute
.
DELETE
(
"/self"
,
controller
.
DeleteSelf
)
selfRoute
.
GET
(
"/token"
,
controller
.
GenerateAccessToken
)
selfRoute
.
GET
(
"/token"
,
controller
.
GenerateAccessToken
)
...
@@ -46,6 +47,7 @@ func SetApiRouter(router *gin.Engine) {
...
@@ -46,6 +47,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute
.
POST
(
"/topup"
,
controller
.
TopUp
)
selfRoute
.
POST
(
"/topup"
,
controller
.
TopUp
)
selfRoute
.
POST
(
"/pay"
,
controller
.
RequestEpay
)
selfRoute
.
POST
(
"/pay"
,
controller
.
RequestEpay
)
selfRoute
.
POST
(
"/amount"
,
controller
.
RequestAmount
)
selfRoute
.
POST
(
"/amount"
,
controller
.
RequestAmount
)
selfRoute
.
POST
(
"/aff_transfer"
,
controller
.
TransferAffQuota
)
}
}
adminRoute
:=
userRoute
.
Group
(
"/"
)
adminRoute
:=
userRoute
.
Group
(
"/"
)
...
...
web/src/components/LogsTable.js
View file @
0b745373
...
@@ -106,7 +106,7 @@ const LogsTable = () => {
...
@@ -106,7 +106,7 @@ const LogsTable = () => {
return
(
return
(
record
.
type
===
0
||
record
.
type
===
2
?
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
<
div
>
<
Tag
color
=
'grey'
size
=
'large'
onClick
=
{()
=>
{
<
Tag
color
=
'grey'
size
=
'large'
onClick
=
{()
=>
{
copyText
(
text
)
copyText
(
text
)
}}
>
{
text
}
<
/Tag
>
}}
>
{
text
}
<
/Tag
>
<
/div
>
<
/div
>
...
@@ -133,7 +133,7 @@ const LogsTable = () => {
...
@@ -133,7 +133,7 @@ const LogsTable = () => {
return
(
return
(
record
.
type
===
0
||
record
.
type
===
2
?
record
.
type
===
0
||
record
.
type
===
2
?
<
div
>
<
div
>
<
Tag
color
=
{
stringToColor
(
text
)}
size
=
'large'
onClick
=
{()
=>
{
<
Tag
color
=
{
stringToColor
(
text
)}
size
=
'large'
onClick
=
{()
=>
{
copyText
(
text
)
copyText
(
text
)
}}
>
{
text
}
<
/Tag
>
}}
>
{
text
}
<
/Tag
>
<
/div
>
<
/div
>
...
@@ -202,11 +202,12 @@ const LogsTable = () => {
...
@@ -202,11 +202,12 @@ const LogsTable = () => {
const
[
logType
,
setLogType
]
=
useState
(
0
);
const
[
logType
,
setLogType
]
=
useState
(
0
);
const
isAdminUser
=
isAdmin
();
const
isAdminUser
=
isAdmin
();
let
now
=
new
Date
();
let
now
=
new
Date
();
// 初始化start_timestamp为前一天
const
[
inputs
,
setInputs
]
=
useState
({
const
[
inputs
,
setInputs
]
=
useState
({
username
:
''
,
username
:
''
,
token_name
:
''
,
token_name
:
''
,
model_name
:
''
,
model_name
:
''
,
start_timestamp
:
timestamp2string
(
0
),
start_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
-
8640
0
),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
end_timestamp
:
timestamp2string
(
now
.
getTime
()
/
1000
+
3600
),
channel
:
''
channel
:
''
});
});
...
@@ -338,7 +339,7 @@ const LogsTable = () => {
...
@@ -338,7 +339,7 @@ const LogsTable = () => {
showSuccess
(
'已复制:'
+
text
);
showSuccess
(
'已复制:'
+
text
);
}
else
{
}
else
{
// setSearchKeyword(text);
// setSearchKeyword(text);
Modal
.
error
({
title
:
'无法复制到剪贴板,请手动复制'
,
content
:
text
});
Modal
.
error
({
title
:
'无法复制到剪贴板,请手动复制'
,
content
:
text
});
}
}
}
}
...
@@ -412,10 +413,12 @@ const LogsTable = () => {
...
@@ -412,10 +413,12 @@ const LogsTable = () => {
name
=
'model_name'
name
=
'model_name'
onChange
=
{
value
=>
handleInputChange
(
value
,
'model_name'
)}
/
>
onChange
=
{
value
=>
handleInputChange
(
value
,
'model_name'
)}
/
>
<
Form
.
DatePicker
field
=
"start_timestamp"
label
=
'起始时间'
style
=
{{
width
:
272
}}
<
Form
.
DatePicker
field
=
"start_timestamp"
label
=
'起始时间'
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
'dateTime'
value
=
{
start_timestamp
}
type
=
'dateTime'
name
=
'start_timestamp'
name
=
'start_timestamp'
onChange
=
{
value
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
onChange
=
{
value
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
<
Form
.
DatePicker
field
=
"end_timestamp"
fluid
label
=
'结束时间'
style
=
{{
width
:
272
}}
<
Form
.
DatePicker
field
=
"end_timestamp"
fluid
label
=
'结束时间'
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
'dateTime'
value
=
{
end_timestamp
}
type
=
'dateTime'
name
=
'end_timestamp'
name
=
'end_timestamp'
onChange
=
{
value
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
onChange
=
{
value
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
...
...
web/src/components/PersonalSetting.js
View file @
0b745373
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
React
,
{
useContext
,
useEffect
,
useState
}
from
'react'
;
import
{
Button
,
Divider
,
Form
,
Header
,
Image
,
Message
,
Modal
}
from
'semantic-ui-react'
;
import
{
Form
,
Image
,
Message
}
from
'semantic-ui-react'
;
import
{
Link
,
useNavigate
}
from
'react-router-dom'
;
import
{
Link
,
useNavigate
}
from
'react-router-dom'
;
import
{
API
,
copy
,
showError
,
showInfo
,
showNotice
,
showSuccess
}
from
'../helpers'
;
import
{
API
,
copy
,
isRoot
,
showError
,
showInfo
,
showNotice
,
showSuccess
}
from
'../helpers'
;
import
Turnstile
from
'react-turnstile'
;
import
Turnstile
from
'react-turnstile'
;
import
{
UserContext
}
from
'../context/User'
;
import
{
UserContext
}
from
'../context/User'
;
import
{
onGitHubOAuthClicked
}
from
'./utils'
;
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
PersonalSetting
=
()
=>
{
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
let
navigate
=
useNavigate
();
let
navigate
=
useNavigate
();
const
[
inputs
,
setInputs
]
=
useState
({
const
[
inputs
,
setInputs
]
=
useState
({
wechat_verification_code
:
''
,
wechat_verification_code
:
''
,
email_verification_code
:
''
,
email_verification_code
:
''
,
email
:
''
,
email
:
''
,
self_account_deletion_confirmation
:
''
self_account_deletion_confirmation
:
''
});
});
const
[
status
,
setStatus
]
=
useState
({});
const
[
status
,
setStatus
]
=
useState
({});
const
[
showWeChatBindModal
,
setShowWeChatBindModal
]
=
useState
(
false
);
const
[
showWeChatBindModal
,
setShowWeChatBindModal
]
=
useState
(
false
);
const
[
showEmailBindModal
,
setShowEmailBindModal
]
=
useState
(
false
);
const
[
showEmailBindModal
,
setShowEmailBindModal
]
=
useState
(
false
);
const
[
showAccountDeleteModal
,
setShowAccountDeleteModal
]
=
useState
(
false
);
const
[
showAccountDeleteModal
,
setShowAccountDeleteModal
]
=
useState
(
false
);
const
[
turnstileEnabled
,
setTurnstileEnabled
]
=
useState
(
false
);
const
[
turnstileEnabled
,
setTurnstileEnabled
]
=
useState
(
false
);
const
[
turnstileSiteKey
,
setTurnstileSiteKey
]
=
useState
(
''
);
const
[
turnstileSiteKey
,
setTurnstileSiteKey
]
=
useState
(
''
);
const
[
turnstileToken
,
setTurnstileToken
]
=
useState
(
''
);
const
[
turnstileToken
,
setTurnstileToken
]
=
useState
(
''
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
loading
,
setLoading
]
=
useState
(
false
);
const
[
disableButton
,
setDisableButton
]
=
useState
(
false
);
const
[
disableButton
,
setDisableButton
]
=
useState
(
false
);
const
[
countdown
,
setCountdown
]
=
useState
(
30
);
const
[
countdown
,
setCountdown
]
=
useState
(
30
);
const
[
affLink
,
setAffLink
]
=
useState
(
""
);
const
[
affLink
,
setAffLink
]
=
useState
(
""
);
const
[
systemToken
,
setSystemToken
]
=
useState
(
""
);
const
[
systemToken
,
setSystemToken
]
=
useState
(
""
);
const
[
models
,
setModels
]
=
useState
([]);
const
[
openTransfer
,
setOpenTransfer
]
=
useState
(
false
);
const
[
transferAmount
,
setTransferAmount
]
=
useState
(
0
);
useEffect
(()
=>
{
useEffect
(()
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
// let user = localStorage.getItem('user');
if
(
status
)
{
// if (user) {
status
=
JSON
.
parse
(
status
);
// userDispatch({ type: 'login', payload: user });
setStatus
(
status
);
// }
if
(
status
.
turnstile_check
)
{
// console.log(localStorage.getItem('user'))
setTurnstileEnabled
(
true
);
setTurnstileSiteKey
(
status
.
turnstile_site_key
);
}
}
},
[]);
useEffect
(()
=>
{
let
status
=
localStorage
.
getItem
(
'status'
);
let
countdownInterval
=
null
;
if
(
status
)
{
if
(
disableButton
&&
countdown
>
0
)
{
status
=
JSON
.
parse
(
status
);
countdownInterval
=
setInterval
(()
=>
{
setStatus
(
status
);
setCountdown
(
countdown
-
1
);
if
(
status
.
turnstile_check
)
{
},
1000
);
setTurnstileEnabled
(
true
);
}
else
if
(
countdown
===
0
)
{
setTurnstileSiteKey
(
status
.
turnstile_site_key
);
setDisableButton
(
false
);
}
setCountdown
(
30
);
}
}
getUserData
().
then
(
return
()
=>
clearInterval
(
countdownInterval
);
// Clean up on unmount
(
res
)
=>
{
},
[
disableButton
,
countdown
]);
console
.
log
(
userState
)
}
);
loadModels
().
then
();
getAffLink
().
then
();
setTransferAmount
(
getQuotaPerUnit
())
},
[]);
useEffect
(()
=>
{
let
countdownInterval
=
null
;
if
(
disableButton
&&
countdown
>
0
)
{
countdownInterval
=
setInterval
(()
=>
{
setCountdown
(
countdown
-
1
);
},
1000
);
}
else
if
(
countdown
===
0
)
{
setDisableButton
(
false
);
setCountdown
(
30
);
}
return
()
=>
clearInterval
(
countdownInterval
);
// Clean up on unmount
},
[
disableButton
,
countdown
]);
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
;
if
(
success
)
{
setSystemToken
(
data
);
await
copy
(
data
);
showSuccess
(
`令牌已重置并已复制到剪贴板`
);
}
else
{
showError
(
message
);
}
};
const
handleInputChange
=
(
e
,
{
name
,
value
})
=>
{
const
getAffLink
=
async
()
=>
{
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
const
res
=
await
API
.
get
(
'/api/user/aff'
);
};
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
let
link
=
`
${
window
.
location
.
origin
}
/register?aff=
${
data
}
`
;
setAffLink
(
link
);
}
else
{
showError
(
message
);
}
};
const
generateAccessToken
=
async
()
=>
{
const
getUserData
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/user/token'
);
let
res
=
await
API
.
get
(
`/api/user/self`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
setSystemToken
(
data
);
userDispatch
({
type
:
'login'
,
payload
:
data
});
setAffLink
(
""
);
}
else
{
await
copy
(
data
);
showError
(
message
);
showSuccess
(
`令牌已重置并已复制到剪贴板`
);
}
}
else
{
showError
(
message
);
}
}
};
const
getAffLink
=
async
()
=>
{
const
loadModels
=
async
()
=>
{
const
res
=
await
API
.
get
(
'/api/user/aff'
);
let
res
=
await
API
.
get
(
`/api/user/models`
);
const
{
success
,
message
,
data
}
=
res
.
data
;
const
{
success
,
message
,
data
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
let
link
=
`
${
window
.
location
.
origin
}
/register?aff=
${
data
}
`
;
setModels
(
data
);
setAffLink
(
link
);
console
.
log
(
data
)
setSystemToken
(
""
);
}
else
{
await
copy
(
link
);
showError
(
message
);
showSuccess
(
`邀请链接已复制到剪切板`
);
}
}
else
{
showError
(
message
);
}
}
};
const
handleAffLinkClick
=
async
(
e
)
=>
{
const
handleAffLinkClick
=
async
(
e
)
=>
{
e
.
target
.
select
();
e
.
target
.
select
();
await
copy
(
e
.
target
.
value
);
await
copy
(
e
.
target
.
value
);
showSuccess
(
`邀请链接已复制到剪切板`
);
showSuccess
(
`邀请链接已复制到剪切板`
);
};
};
const
handleSystemTokenClick
=
async
(
e
)
=>
{
const
handleSystemTokenClick
=
async
(
e
)
=>
{
e
.
target
.
select
();
e
.
target
.
select
();
await
copy
(
e
.
target
.
value
);
await
copy
(
e
.
target
.
value
);
showSuccess
(
`系统令牌已复制到剪切板`
);
showSuccess
(
`系统令牌已复制到剪切板`
);
};
};
const
deleteAccount
=
async
()
=>
{
const
deleteAccount
=
async
()
=>
{
if
(
inputs
.
self_account_deletion_confirmation
!==
userState
.
user
.
username
)
{
if
(
inputs
.
self_account_deletion_confirmation
!==
userState
.
user
.
username
)
{
showError
(
'请输入你的账户名以确认删除!'
);
showError
(
'请输入你的账户名以确认删除!'
);
return
;
return
;
}
}
const
res
=
await
API
.
delete
(
'/api/user/self'
);
const
res
=
await
API
.
delete
(
'/api/user/self'
);
const
{
success
,
message
}
=
res
.
data
;
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
'账户已删除!'
);
showSuccess
(
'账户已删除!'
);
await
API
.
get
(
'/api/user/logout'
);
await
API
.
get
(
'/api/user/logout'
);
userDispatch
({
type
:
'logout'
});
userDispatch
({
type
:
'logout'
});
localStorage
.
removeItem
(
'user'
);
localStorage
.
removeItem
(
'user'
);
navigate
(
'/login'
);
navigate
(
'/login'
);
}
else
{
}
else
{
showError
(
message
);
showError
(
message
);
}
}
};
};
const
bindWeChat
=
async
()
=>
{
const
bindWeChat
=
async
()
=>
{
if
(
inputs
.
wechat_verification_code
===
''
)
return
;
if
(
inputs
.
wechat_verification_code
===
''
)
return
;
const
res
=
await
API
.
get
(
const
res
=
await
API
.
get
(
`/api/oauth/wechat/bind?code=
${
inputs
.
wechat_verification_code
}
`
`/api/oauth/wechat/bind?code=
${
inputs
.
wechat_verification_code
}
`
);
);
const
{
success
,
message
}
=
res
.
data
;
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
'微信账户绑定成功!'
);
showSuccess
(
'微信账户绑定成功!'
);
setShowWeChatBindModal
(
false
);
setShowWeChatBindModal
(
false
);
}
else
{
}
else
{
showError
(
message
);
showError
(
message
);
}
}
};
};
const
sendVerificationCode
=
async
()
=>
{
const
transfer
=
async
()
=>
{
setDisableButton
(
true
);
if
(
transferAmount
<
getQuotaPerUnit
())
{
if
(
inputs
.
email
===
''
)
return
;
showError
(
'划转金额最低为'
+
renderQuota
(
getQuotaPerUnit
()));
if
(
turnstileEnabled
&&
turnstileToken
===
''
)
{
return
;
showInfo
(
'请稍后几秒重试,Turnstile 正在检查用户环境!'
);
}
return
;
const
res
=
await
API
.
post
(
}
`/api/user/aff_transfer`
,
setLoading
(
true
);
{
const
res
=
await
API
.
get
(
quota
:
transferAmount
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
}
);
);
const
{
success
,
message
}
=
res
.
data
;
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
if
(
success
)
{
showSuccess
(
'验证码发送成功,请检查邮箱!'
);
showSuccess
(
message
);
}
else
{
setOpenTransfer
(
false
);
showError
(
message
);
getUserData
().
then
();
}
else
{
showError
(
message
);
}
};
const
sendVerificationCode
=
async
()
=>
{
if
(
inputs
.
email
===
''
)
{
showError
(
'请输入邮箱!'
);
return
;
}
setDisableButton
(
true
);
if
(
turnstileEnabled
&&
turnstileToken
===
''
)
{
showInfo
(
'请稍后几秒重试,Turnstile 正在检查用户环境!'
);
return
;
}
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/verification?email=
${
inputs
.
email
}
&turnstile=
${
turnstileToken
}
`
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'验证码发送成功,请检查邮箱!'
);
}
else
{
showError
(
message
);
}
setLoading
(
false
);
};
const
bindEmail
=
async
()
=>
{
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
;
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'
;
}
}
}
setLoading
(
false
);
};
const
bindEmail
=
async
()
=>
{
const
handleCancel
=
()
=>
{
if
(
inputs
.
email_verification_code
===
''
)
return
;
setOpenTransfer
(
false
);
setLoading
(
true
);
const
res
=
await
API
.
get
(
`/api/oauth/email/bind?email=
${
inputs
.
email
}
&code=
${
inputs
.
email_verification_code
}
`
);
const
{
success
,
message
}
=
res
.
data
;
if
(
success
)
{
showSuccess
(
'邮箱账户绑定成功!'
);
setShowEmailBindModal
(
false
);
}
else
{
showError
(
message
);
}
}
setLoading
(
false
);
};
return
(
return
(
<
div
style
=
{{
lineHeight
:
'40px'
}}
>
<
div
style
=
{{
lineHeight
:
'40px'
}}
>
<
Header
as
=
'h3'
>
通用设置
<
/Header
>
<
Layout
>
<
Message
>
<
Layout
.
Content
>
注意,此处生成的令牌用于系统管理,而非用于请求
OpenAI
相关的服务,请知悉。
<
Modal
<
/Message
>
title
=
"请输入要划转的数量"
<
Button
as
=
{
Link
}
to
=
{
`/user/edit/`
}
>
visible
=
{
openTransfer
}
更新个人信息
onOk
=
{
transfer
}
<
/Button
>
onCancel
=
{
handleCancel
}
<
Button
onClick
=
{
generateAccessToken
}
>
生成系统访问令牌
<
/Button
>
maskClosable
=
{
false
}
<
Button
onClick
=
{
getAffLink
}
>
复制邀请链接
<
/Button
>
size
=
{
'small'
}
<
Button
onClick
=
{()
=>
{
centered
=
{
true
}
setShowAccountDeleteModal
(
true
);
>
}}
>
删除个人账户
<
/Button
>
<
div
style
=
{{
marginTop
:
20
}}
>
<
Typography
.
Text
>
{
`可用额度
${
renderQuotaWithPrompt
(
userState
?.
user
?.
aff_quota
)}
`}</Typography.Text>
{
systemToken
&&
(
<Input style={{marginTop: 5}} value={userState?.user?.aff_quota} disabled={true}></Input>
<
Form
.
Input
</div>
fluid
<div style={{marginTop: 20}}>
readOnly
<Typography.Text>{`
划转额度
$
{
renderQuotaWithPrompt
(
transferAmount
)}
最低
` + renderQuota(getQuotaPerUnit())}</Typography.Text>
value
=
{
systemToken
}
<div>
onClick
=
{
handleSystemTokenClick
}
<InputNumber min={0} style={{marginTop: 5}} value={transferAmount} onChange={(value)=>setTransferAmount(value)} disabled={false}></InputNumber>
style
=
{{
marginTop
:
'10px'
}}
</div>
/
>
</div>
)}
</Modal>
{
affLink
&&
(
<div style={{marginTop: 20}}>
<
Form
.
Input
<Card
fluid
title={
readOnly
<Card.Meta
value
=
{
affLink
}
avatar={<Avatar size="default" color={stringToColor(getUsername())}
onClick
=
{
handleAffLinkClick
}
style={{marginRight: 4}}>
style
=
{{
marginTop
:
'10px'
}}
{typeof getUsername() === 'string' && getUsername().slice(0, 1)}
/
>
</Avatar>}
)}
title={<Typography.Text>{getUsername()}</Typography.Text>}
<
Divider
/>
description={isRoot()?<Tag color="red">管理员</Tag>:<Tag color="blue">普通用户</Tag>}
<
Header
as
=
'h3'
>
账号绑定
<
/Header
>
></Card.Meta>
{
}
status
.
wechat_login
&&
(
headerExtraContent={
<
Button
<>
onClick
=
{()
=>
{
<Space vertical align="start">
setShowWeChatBindModal
(
true
);
<Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
}}
<Tag color="blue">{userState?.user?.group}</Tag>
>
</Space>
绑定微信账号
</>
<
/Button
>
}
)
footer={
}
<Descriptions row>
<
Modal
<Descriptions.Item itemKey="当前余额">{renderQuota(userState?.user?.quota)}</Descriptions.Item>
onClose
=
{()
=>
setShowWeChatBindModal
(
false
)}
<Descriptions.Item itemKey="历史消耗">{renderQuota(userState?.user?.used_quota)}</Descriptions.Item>
onOpen
=
{()
=>
setShowWeChatBindModal
(
true
)}
<Descriptions.Item itemKey="请求次数">{userState.user?.request_count}</Descriptions.Item>
open
=
{
showWeChatBindModal
}
</Descriptions>
size
=
{
'mini'
}
}
>
>
<
Modal
.
Content
>
<Typography.Title heading={6}>可用模型</Typography.Title>
<
Modal
.
Description
>
<div style={{marginTop: 10}}>
<
Image
src
=
{
status
.
wechat_qrcode
}
fluid
/>
<Space wrap>
<
div
style
=
{{
textAlign
:
'center'
}}
>
{models.map((model) => (
<
p
>
<Tag key={model} color="cyan">
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
{model}
<
/p
>
</Tag>
<
/div
>
))}
<
Form
size
=
'large'
>
</Space>
<
Form
.
Input
</div>
fluid
placeholder
=
'验证码'
</Card>
name
=
'wechat_verification_code'
<Card
value
=
{
inputs
.
wechat_verification_code
}
footer={
onChange
=
{
handleInputChange
}
<div>
/
>
<Typography.Text>邀请链接</Typography.Text>
<
Button
color
=
''
fluid
size
=
'large'
onClick
=
{
bindWeChat
}
>
<Input
绑定
style={{marginTop: 10}}
<
/Button
>
value={affLink}
<
/Form
>
onClick={handleAffLinkClick}
<
/Modal.Description
>
readOnly
<
/Modal.Content
>
/>
<
/Modal
>
</div>
{
}
status
.
github_oauth
&&
(
>
<
Button
onClick
=
{()
=>
{
onGitHubOAuthClicked
(
status
.
github_client_id
)}}
>
绑定
GitHub
账号
<
/Button
>
<Typography.Title heading={6}>邀请信息</Typography.Title>
)
<div style={{marginTop: 10}}>
}
<Descriptions row>
<
Button
<Descriptions.Item itemKey="待使用收益">
onClick
=
{()
=>
{
<span style={{color: 'rgba(var(--semi-red-5), 1)'}}>
setShowEmailBindModal
(
true
);
{
}}
renderQuota(userState?.user?.aff_quota)
>
}
绑定邮箱地址
</span>
<
/Button
>
<Button type={'secondary'} onClick={()=>setOpenTransfer(true)} size={'small'} style={{marginLeft: 10}}>划转</Button>
<
Modal
</Descriptions.Item>
onClose
=
{()
=>
setShowEmailBindModal
(
false
)}
<Descriptions.Item itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item>
onOpen
=
{()
=>
setShowEmailBindModal
(
true
)}
<Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item>
open
=
{
showEmailBindModal
}
</Descriptions>
size
=
{
'tiny'
}
</div>
style
=
{{
maxWidth
:
'450px'
}}
</Card>
>
<Card>
<
Modal
.
Header
>
绑定邮箱地址
<
/Modal.Header
>
<Typography.Title heading={6}>个人信息</Typography.Title>
<
Modal
.
Content
>
<div style={{marginTop: 20}}>
<
Modal
.
Description
>
<Typography.Text strong>邮箱</Typography.Text>
<
Form
size
=
'large'
>
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<
Form
.
Input
<div>
fluid
<Input
placeholder
=
'输入邮箱地址'
value={userState.user && userState.user.email !== ''?userState.user.email:'未绑定'}
onChange
=
{
handleInputChange
}
readonly={true}
name
=
'email'
></Input>
type
=
'email'
</div>
action
=
{
<div>
<
Button
onClick
=
{
sendVerificationCode
}
disabled
=
{
disableButton
||
loading
}
>
<Button onClick={()=>{setShowEmailBindModal(true)}} disabled={userState.user && userState.user.email !== ''}>绑定邮箱</Button>
{
disableButton
?
`重新发送(
${
countdown
}
)`
:
'获取验证码'
}
</div>
<
/Button
>
</div>
}
</div>
/
>
<div style={{marginTop: 10}}>
<
Form
.
Input
<Typography.Text strong>微信</Typography.Text>
fluid
<div style={{display: 'flex', justifyContent: 'space-between'}}>
placeholder
=
'验证码'
<div>
name
=
'email_verification_code'
<Input
value
=
{
inputs
.
email_verification_code
}
value={userState.user && userState.user.wechat_id !== ''?'已绑定':'未绑定'}
onChange
=
{
handleInputChange
}
readonly={true}
/
>
></Input>
{
turnstileEnabled
?
(
</div>
<
Turnstile
<div>
sitekey
=
{
turnstileSiteKey
}
<Button disabled={(userState.user && userState.user.wechat_id !== '') || !status.wechat_login}>
onVerify
=
{(
token
)
=>
{
{
setTurnstileToken
(
token
);
status.wechat_login?'绑定':'未启用'
}}
}
/
>
</Button>
)
:
(
</div>
<><
/
>
</div>
)}
</div>
<
div
style
=
{{
display
:
'flex'
,
justifyContent
:
'space-between'
,
marginTop
:
'1rem'
}}
>
<div style={{marginTop: 10}}>
<
Button
<Typography.Text strong>GitHub</Typography.Text>
color
=
''
<div style={{display: 'flex', justifyContent: 'space-between'}}>
fluid
<div>
size
=
'large'
<Input
onClick
=
{
bindEmail
}
value={userState.user && userState.user.github_id !== ''?userState.user.github_id:'未绑定'}
loading
=
{
loading
}
readonly={true}
>
></Input>
确认绑定
</div>
<
/Button
>
<div>
<
div
style
=
{{
width
:
'1rem'
}}
><
/div>
<Button
<
Button
onClick={() => {onGitHubOAuthClicked(status.github_client_id)}}
fluid
disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
size
=
'large'
>
onClick
=
{()
=>
setShowEmailBindModal
(
false
)}
{
>
status.github_oauth?'绑定':'未启用'
取消
}
<
/Button
>
</Button>
<
/div
>
</div>
<
/Form
>
</div>
<
/Modal.Description
>
</div>
<
/Modal.Content
>
<
/Modal
>
<div style={{marginTop: 10}}>
<
Modal
<Space>
onClose
=
{()
=>
setShowAccountDeleteModal
(
false
)}
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
onOpen
=
{()
=>
setShowAccountDeleteModal
(
true
)}
<Button onClick={() => {
open
=
{
showAccountDeleteModal
}
setShowAccountDeleteModal(true);
size
=
{
'tiny'
}
}}>删除个人账户</Button>
style
=
{{
maxWidth
:
'450px'
}}
</Space>
>
<
Modal
.
Header
>
危险操作
<
/Modal.Header
>
{systemToken && (
<
Modal
.
Content
>
<Form.Input
<
Message
>
您正在删除自己的帐户,将清空所有数据且不可恢复
<
/Message
>
fluid
<
Modal
.
Description
>
readOnly
<
Form
size
=
'large'
>
value={systemToken}
<
Form
.
Input
onClick={handleSystemTokenClick}
fluid
style={{marginTop: '10px'}}
placeholder
=
{
`输入你的账户名
${
userState
?.
user
?.
username
}
以确认删除
`}
/>
name='self_account_deletion_confirmation'
)}
value={inputs.self_account_deletion_confirmation}
{
onChange={handleInputChange}
status.wechat_login && (
/>
<Button
{turnstileEnabled ? (
onClick={() => {
<Turnstile
setShowWeChatBindModal(true);
sitekey={turnstileSiteKey}
}}
onVerify={(token) => {
>
setTurnstileToken(token);
绑定微信账号
}}
</Button>
/>
)
) : (
}
<></>
<Modal
)}
onCancel={() => setShowWeChatBindModal(false)}
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: '1rem' }}>
// onOpen={() => setShowWeChatBindModal(true)}
<Button
visible={showWeChatBindModal}
color='red'
size={'mini'}
fluid
>
size='large'
<Image src={status.wechat_qrcode} fluid/>
onClick={deleteAccount}
<div style={{textAlign: 'center'}}>
loading={loading}
<p>
>
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
确认删除
</p>
</Button>
</div>
<div style={{ width: '1rem' }}></div>
<Form size='large'>
<Button
<Form.Input
fluid
fluid
size='large'
placeholder='验证码'
onClick={() => setShowAccountDeleteModal(false)}
name='wechat_verification_code'
>
value={inputs.wechat_verification_code}
取消
onChange={handleInputChange}
</Button>
/>
</div>
<Button color='' fluid size='large' onClick={bindWeChat}>
</Form>
绑定
</Modal.Description>
</Button>
</Modal.Content>
</Form>
</Modal>
</Modal>
</div>
</div>
);
</Card>
<Modal
onCancel={() => setShowEmailBindModal(false)}
// onOpen={() => setShowEmailBindModal(true)}
onOk={bindEmail}
visible={showEmailBindModal}
size={'small'}
centered={true}
maskClosable={false}
>
<Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
<div style={{marginTop: 20, display: 'flex', justifyContent: 'space-between'}}>
<Input
fluid
placeholder='输入邮箱地址'
onChange={(value)=>handleInputChange('email', value)}
name='email'
type='email'
/>
<Button onClick={sendVerificationCode}
disabled={disableButton || loading}>
{disableButton ? `
重新发送
(
$
{
countdown
})
` : '获取验证码'}
</Button>
</div>
<div style={{marginTop: 10}}>
<Input
fluid
placeholder='验证码'
name='email_verification_code'
value={inputs.email_verification_code}
onChange={(value)=>handleInputChange('email_verification_code', value)}
/>
</div>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
</Modal>
<Modal
onCancel={() => setShowAccountDeleteModal(false)}
visible={showAccountDeleteModal}
size={'small'}
centered={true}
onOk={deleteAccount}
>
<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={(value)=>handleInputChange('self_account_deletion_confirmation', value)}
/>
{turnstileEnabled ? (
<Turnstile
sitekey={turnstileSiteKey}
onVerify={(token) => {
setTurnstileToken(token);
}}
/>
) : (
<></>
)}
</div>
</Modal>
</div>
</Layout.Content>
</Layout>
</div>
);
};
};
export default PersonalSetting;
export default PersonalSetting;
web/src/helpers/render.js
View file @
0b745373
...
@@ -37,6 +37,12 @@ export function renderNumber(num) {
...
@@ -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
)
{
export
function
renderQuota
(
quota
,
digits
=
2
)
{
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
let
quotaPerUnit
=
localStorage
.
getItem
(
'quota_per_unit'
);
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
let
displayInCurrency
=
localStorage
.
getItem
(
'display_in_currency'
);
...
...
web/src/pages/Setting/index.js
View file @
0b745373
import
React
from
'react'
;
import
React
from
'react'
;
import
{
Segment
,
Tab
}
from
'semantic-ui-react'
;
import
SystemSetting
from
'../../components/SystemSetting'
;
import
SystemSetting
from
'../../components/SystemSetting'
;
import
{
isRoot
}
from
'../../helpers'
;
import
{
isRoot
}
from
'../../helpers'
;
import
OtherSetting
from
'../../components/OtherSetting'
;
import
OtherSetting
from
'../../components/OtherSetting'
;
import
PersonalSetting
from
'../../components/PersonalSetting'
;
import
PersonalSetting
from
'../../components/PersonalSetting'
;
import
OperationSetting
from
'../../components/OperationSetting'
;
import
OperationSetting
from
'../../components/OperationSetting'
;
import
{
Layout
,
TabPane
,
Tabs
}
from
"@douyinfe/semi-ui"
;
const
Setting
=
()
=>
{
const
Setting
=
()
=>
{
let
panes
=
[
let
panes
=
[
{
{
menuItem
:
'个人设置'
,
tab
:
'个人设置'
,
render
:
()
=>
(
content
:
<
PersonalSetting
/>
,
<
Tab
.
Pane
attached
=
{
false
}
>
itemKey
:
'1'
<
PersonalSetting
/>
}
<
/Tab.Pane
>
];
)
}
];
if
(
isRoot
())
{
if
(
isRoot
())
{
panes
.
push
({
panes
.
push
({
menuItem
:
'运营设置'
,
tab
:
'运营设置'
,
render
:
()
=>
(
content
:
<
OperationSetting
/>
,
<
Tab
.
Pane
attached
=
{
false
}
>
itemKey
:
'2'
<
OperationSetting
/>
});
<
/Tab.Pane
>
panes
.
push
({
)
tab
:
'系统设置'
,
});
content
:
<
SystemSetting
/>
,
panes
.
push
({
itemKey
:
'3'
menuItem
:
'系统设置'
,
});
render
:
()
=>
(
panes
.
push
({
<
Tab
.
Pane
attached
=
{
false
}
>
tab
:
'其他设置'
,
<
SystemSetting
/>
content
:
<
OtherSetting
/>
,
<
/Tab.Pane
>
itemKey
:
'4'
)
});
});
}
panes
.
push
({
menuItem
:
'其他设置'
,
render
:
()
=>
(
<
Tab
.
Pane
attached
=
{
false
}
>
<
OtherSetting
/>
<
/Tab.Pane
>
)
});
}
return
(
return
(
<
Segment
>
<
div
>
<
Tab
menu
=
{{
secondary
:
true
,
pointing
:
true
}}
panes
=
{
panes
}
/
>
<
Layout
>
<
/Segment
>
<
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
>
);
};
};
export
default
Setting
;
export
default
Setting
;
web/src/pages/User/EditUser.js
View file @
0b745373
...
@@ -20,7 +20,7 @@ const EditUser = () => {
...
@@ -20,7 +20,7 @@ const EditUser = () => {
});
});
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
[
groupOptions
,
setGroupOptions
]
=
useState
([]);
const
{
username
,
display_name
,
password
,
github_id
,
wechat_id
,
email
,
quota
,
group
}
=
const
{
username
,
display_name
,
password
,
github_id
,
wechat_id
,
email
,
quota
,
group
}
=
inputs
;
inputs
;
const
handleInputChange
=
(
e
,
{
name
,
value
})
=>
{
const
handleInputChange
=
(
e
,
{
name
,
value
})
=>
{
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
setInputs
((
inputs
)
=>
({
...
inputs
,
[
name
]:
value
}));
};
};
...
@@ -83,107 +83,107 @@ const EditUser = () => {
...
@@ -83,107 +83,107 @@ const EditUser = () => {
};
};
return
(
return
(
<>
<>
<
Segment
loading
=
{
loading
}
>
<
Segment
loading
=
{
loading
}
>
<
Header
as
=
'h3'
>
更新用户信息
<
/Header
>
<
Header
as
=
'h3'
>
更新用户信息
<
/Header
>
<
Form
autoComplete
=
'new-password'
>
<
Form
autoComplete
=
'new-password'
>
<
Form
.
Field
>
<
Form
.
Field
>
<
Form
.
Input
<
Form
.
Input
label
=
'用户名'
label
=
'用户名'
name
=
'username'
name
=
'username'
placeholder
=
{
'请输入新的用户名'
}
placeholder
=
{
'请输入新的用户名'
}
onChange
=
{
handleInputChange
}
value
=
{
username
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'密码'
name
=
'password'
type
=
{
'password'
}
placeholder
=
{
'请输入新的密码,最短 8 位'
}
onChange
=
{
handleInputChange
}
value
=
{
password
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'显示名称'
name
=
'display_name'
placeholder
=
{
'请输入新的显示名称'
}
onChange
=
{
handleInputChange
}
value
=
{
display_name
}
autoComplete
=
'new-password'
/>
<
/Form.Field
>
{
userId
&&
<>
<
Form
.
Field
>
<
Form
.
Dropdown
label
=
'分组'
placeholder
=
{
'请选择分组'
}
name
=
'group'
fluid
search
selection
allowAdditions
additionLabel
=
{
'请在系统设置页面编辑分组倍率以添加新的分组:'
}
onChange
=
{
handleInputChange
}
onChange
=
{
handleInputChange
}
value
=
{
inputs
.
group
}
value
=
{
username
}
autoComplete
=
'new-password'
autoComplete
=
'new-password'
options
=
{
groupOptions
}
/>
/
>
<
/Form.Field
>
<
/
Form.Field
>
<
Form
.
Field
>
<
Form
.
Field
>
<
Form
.
Input
<
Form
.
Input
label
=
'密码'
label
=
{
`剩余额度
${
renderQuotaWithPrompt
(
quota
)}
`
}
name
=
'password'
name
=
'quota'
type
=
{
'password'
}
placeholder
=
{
'请输入新的
剩余额度
'
}
placeholder
=
{
'请输入新的
密码,最短 8 位
'
}
onChange
=
{
handleInputChange
}
onChange
=
{
handleInputChange
}
value
=
{
quota
}
value
=
{
password
}
type
=
{
'number'
}
autoComplete
=
'new-password'
autoComplete
=
'new-password'
/>
/>
<
/Form.Field
>
<
/Form.Field
>
<
/
>
<
Form
.
Field
>
}
<
Form
.
Input
<
Form
.
Field
>
label
=
'显示名称'
<
Form
.
Input
name
=
'display_name'
label
=
'已绑定的 GitHub 账户'
placeholder
=
{
'请输入新的显示名称'
}
name
=
'github_id'
onChange
=
{
handleInputChange
}
value
=
{
github_id
}
value
=
{
display_name
}
autoComplete
=
'new-password'
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
/>
readOnly
<
/Form.Field
>
/>
{
<
/Form.Field
>
userId
&&
<>
<
Form
.
Field
>
<
Form
.
Field
>
<
Form
.
Input
<
Form
.
Dropdown
label
=
'已绑定的微信账户'
label
=
'分组'
name
=
'wechat_id'
placeholder
=
{
'请选择分组'
}
value
=
{
wechat_id
}
name
=
'group'
autoComplete
=
'new-password'
fluid
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
search
readOnly
selection
/>
allowAdditions
<
/Form.Field
>
additionLabel
=
{
'请在系统设置页面编辑分组倍率以添加新的分组:'
}
<
Form
.
Field
>
onChange
=
{
handleInputChange
}
<
Form
.
Input
value
=
{
inputs
.
group
}
label
=
'已绑定的邮箱账户'
autoComplete
=
'new-password'
name
=
'email'
options
=
{
groupOptions
}
value
=
{
email
}
/
>
autoComplete
=
'new-password'
<
/Form.Field
>
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
<
Form
.
Field
>
readOnly
<
Form
.
Input
/>
label
=
{
`剩余额度
${
renderQuotaWithPrompt
(
quota
)}
`
}
<
/Form.Field
>
name
=
'quota'
<
Button
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
placeholder
=
{
'请输入新的剩余额度'
}
<
Button
positive
onClick
=
{
submit
}
>
提交
<
/Button
>
onChange
=
{
handleInputChange
}
<
/Form
>
value
=
{
quota
}
<
/Segment
>
type
=
{
'number'
}
<
/
>
autoComplete
=
'new-password'
/>
<
/Form.Field
>
<
/
>
}
<
Form
.
Field
>
<
Form
.
Input
label
=
'已绑定的 GitHub 账户'
name
=
'github_id'
value
=
{
github_id
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'已绑定的微信账户'
name
=
'wechat_id'
value
=
{
wechat_id
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
/>
<
/Form.Field
>
<
Form
.
Field
>
<
Form
.
Input
label
=
'已绑定的邮箱账户'
name
=
'email'
value
=
{
email
}
autoComplete
=
'new-password'
placeholder
=
'此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readOnly
/>
<
/Form.Field
>
<
Button
onClick
=
{
handleCancel
}
>
取消
<
/Button
>
<
Button
positive
onClick
=
{
submit
}
>
提交
<
/Button
>
<
/Form
>
<
/Segment
>
<
/
>
);
);
};
};
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment