mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-06 00:33:43 +08:00
Compare commits
8 Commits
v0.2.6
...
v0.3.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9ea1d9d10 | ||
|
|
ea8e7c517b | ||
|
|
d1e9b86f05 | ||
|
|
6d1e5cb5dc | ||
|
|
01abed0a30 | ||
|
|
7c56a36a1c | ||
|
|
c48327ff91 | ||
|
|
a5406c6963 |
@@ -40,6 +40,9 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
<a href="https://openai.justsong.cn/">在线演示</a>
|
<a href="https://openai.justsong.cn/">在线演示</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
> **Warning**:从 `v0.2` 版本升级到 `v0.3` 版本需要手动迁移数据库,请手动执行[数据库迁移脚本](./bin/migration_v0.2-v0.3.sql)。
|
||||||
|
|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
||||||
+ [x] OpenAI 官方通道
|
+ [x] OpenAI 官方通道
|
||||||
|
|||||||
6
bin/migration_v0.2-v0.3.sql
Normal file
6
bin/migration_v0.2-v0.3.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
UPDATE users
|
||||||
|
SET quota = quota + (
|
||||||
|
SELECT SUM(remain_quota)
|
||||||
|
FROM tokens
|
||||||
|
WHERE tokens.user_id = users.id
|
||||||
|
)
|
||||||
@@ -50,10 +50,10 @@ var WeChatAccountQRCodeImageURL = ""
|
|||||||
var TurnstileSiteKey = ""
|
var TurnstileSiteKey = ""
|
||||||
var TurnstileSecretKey = ""
|
var TurnstileSecretKey = ""
|
||||||
|
|
||||||
var QuotaForNewUser = 100
|
var QuotaForNewUser = 0
|
||||||
|
|
||||||
var ChannelDisableThreshold = 5.0
|
var ChannelDisableThreshold = 5.0
|
||||||
var AutomaticDisableChannelEnabled = false
|
var AutomaticDisableChannelEnabled = false
|
||||||
|
var QuotaRemindThreshold = 1000
|
||||||
|
|
||||||
var RootUserEmail = ""
|
var RootUserEmail = ""
|
||||||
|
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ func GetTokenStatus(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AddToken(c *gin.Context) {
|
func AddToken(c *gin.Context) {
|
||||||
isAdmin := c.GetInt("role") >= common.RoleAdminUser
|
|
||||||
token := model.Token{}
|
token := model.Token{}
|
||||||
err := c.ShouldBindJSON(&token)
|
err := c.ShouldBindJSON(&token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -118,27 +117,14 @@ func AddToken(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cleanToken := model.Token{
|
cleanToken := model.Token{
|
||||||
UserId: c.GetInt("id"),
|
UserId: c.GetInt("id"),
|
||||||
Name: token.Name,
|
Name: token.Name,
|
||||||
Key: common.GetUUID(),
|
Key: common.GetUUID(),
|
||||||
CreatedTime: common.GetTimestamp(),
|
CreatedTime: common.GetTimestamp(),
|
||||||
AccessedTime: common.GetTimestamp(),
|
AccessedTime: common.GetTimestamp(),
|
||||||
ExpiredTime: token.ExpiredTime,
|
ExpiredTime: token.ExpiredTime,
|
||||||
}
|
RemainQuota: token.RemainQuota,
|
||||||
if isAdmin {
|
UnlimitedQuota: token.UnlimitedQuota,
|
||||||
cleanToken.RemainQuota = token.RemainQuota
|
|
||||||
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
|
||||||
} else {
|
|
||||||
userId := c.GetInt("id")
|
|
||||||
quota, err := model.GetUserQuota(userId)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
cleanToken.RemainQuota = quota
|
|
||||||
}
|
}
|
||||||
err = cleanToken.Insert()
|
err = cleanToken.Insert()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -148,10 +134,6 @@ func AddToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !isAdmin {
|
|
||||||
// update user quota
|
|
||||||
err = model.DecreaseUserQuota(c.GetInt("id"), cleanToken.RemainQuota)
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@@ -178,7 +160,6 @@ func DeleteToken(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateToken(c *gin.Context) {
|
func UpdateToken(c *gin.Context) {
|
||||||
isAdmin := c.GetInt("role") >= common.RoleAdminUser
|
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
statusOnly := c.Query("status_only")
|
statusOnly := c.Query("status_only")
|
||||||
token := model.Token{}
|
token := model.Token{}
|
||||||
@@ -209,7 +190,7 @@ func UpdateToken(c *gin.Context) {
|
|||||||
if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota {
|
if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainQuota <= 0 && !cleanToken.UnlimitedQuota {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "令牌可用次数已用尽,无法启用,请先修改令牌剩余次数,或者设置为无限次数",
|
"message": "令牌可用额度已用尽,无法启用,请先修改令牌剩余额度,或者设置为无限额度",
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -220,10 +201,8 @@ func UpdateToken(c *gin.Context) {
|
|||||||
// If you add more fields, please also update token.Update()
|
// If you add more fields, please also update token.Update()
|
||||||
cleanToken.Name = token.Name
|
cleanToken.Name = token.Name
|
||||||
cleanToken.ExpiredTime = token.ExpiredTime
|
cleanToken.ExpiredTime = token.ExpiredTime
|
||||||
if isAdmin {
|
cleanToken.RemainQuota = token.RemainQuota
|
||||||
cleanToken.RemainQuota = token.RemainQuota
|
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
||||||
cleanToken.UnlimitedQuota = token.UnlimitedQuota
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = cleanToken.Update()
|
err = cleanToken.Update()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -240,34 +219,3 @@ func UpdateToken(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type topUpRequest struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func TopUp(c *gin.Context) {
|
|
||||||
req := topUpRequest{}
|
|
||||||
err := c.ShouldBindJSON(&req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
quota, err := model.Redeem(req.Key, req.Id)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": false,
|
|
||||||
"message": err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"success": true,
|
|
||||||
"message": "",
|
|
||||||
"data": quota,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -654,3 +654,34 @@ func EmailBind(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type topUpRequest struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TopUp(c *gin.Context) {
|
||||||
|
req := topUpRequest{}
|
||||||
|
err := c.ShouldBindJSON(&req)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := c.GetInt("id")
|
||||||
|
quota, err := model.Redeem(req.Key, id)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": quota,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["TurnstileSiteKey"] = ""
|
common.OptionMap["TurnstileSiteKey"] = ""
|
||||||
common.OptionMap["TurnstileSecretKey"] = ""
|
common.OptionMap["TurnstileSecretKey"] = ""
|
||||||
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
|
common.OptionMap["QuotaForNewUser"] = strconv.Itoa(common.QuotaForNewUser)
|
||||||
|
common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
|
||||||
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
|
||||||
common.OptionMap["TopUpLink"] = common.TopUpLink
|
common.OptionMap["TopUpLink"] = common.TopUpLink
|
||||||
common.OptionMapRWMutex.Unlock()
|
common.OptionMapRWMutex.Unlock()
|
||||||
@@ -156,6 +157,8 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.TurnstileSecretKey = value
|
common.TurnstileSecretKey = value
|
||||||
case "QuotaForNewUser":
|
case "QuotaForNewUser":
|
||||||
common.QuotaForNewUser, _ = strconv.Atoi(value)
|
common.QuotaForNewUser, _ = strconv.Atoi(value)
|
||||||
|
case "QuotaRemindThreshold":
|
||||||
|
common.QuotaRemindThreshold, _ = strconv.Atoi(value)
|
||||||
case "ModelRatio":
|
case "ModelRatio":
|
||||||
err = common.UpdateModelRatioByJSONString(value)
|
err = common.UpdateModelRatioByJSONString(value)
|
||||||
case "TopUpLink":
|
case "TopUpLink":
|
||||||
|
|||||||
@@ -40,12 +40,12 @@ func GetRedemptionById(id int) (*Redemption, error) {
|
|||||||
return &redemption, err
|
return &redemption, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func Redeem(key string, tokenId int) (quota int, err error) {
|
func Redeem(key string, userId int) (quota int, err error) {
|
||||||
if key == "" {
|
if key == "" {
|
||||||
return 0, errors.New("未提供兑换码")
|
return 0, errors.New("未提供兑换码")
|
||||||
}
|
}
|
||||||
if tokenId == 0 {
|
if userId == 0 {
|
||||||
return 0, errors.New("未提供 token id")
|
return 0, errors.New("无效的 user id")
|
||||||
}
|
}
|
||||||
redemption := &Redemption{}
|
redemption := &Redemption{}
|
||||||
err = DB.Where("`key` = ?", key).First(redemption).Error
|
err = DB.Where("`key` = ?", key).First(redemption).Error
|
||||||
@@ -55,7 +55,7 @@ func Redeem(key string, tokenId int) (quota int, err error) {
|
|||||||
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
if redemption.Status != common.RedemptionCodeStatusEnabled {
|
||||||
return 0, errors.New("该兑换码已被使用")
|
return 0, errors.New("该兑换码已被使用")
|
||||||
}
|
}
|
||||||
err = IncreaseTokenQuota(tokenId, redemption.Quota)
|
err = IncreaseUserQuota(userId, redemption.Quota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
_ "gorm.io/driver/sqlite"
|
_ "gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
@@ -82,6 +83,16 @@ func GetTokenByIds(id int, userId int) (*Token, error) {
|
|||||||
return &token, err
|
return &token, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetTokenById(id int) (*Token, error) {
|
||||||
|
if id == 0 {
|
||||||
|
return nil, errors.New("id 为空!")
|
||||||
|
}
|
||||||
|
token := Token{Id: id}
|
||||||
|
var err error = nil
|
||||||
|
err = DB.First(&token, "id = ?", id).Error
|
||||||
|
return &token, err
|
||||||
|
}
|
||||||
|
|
||||||
func (token *Token) Insert() error {
|
func (token *Token) Insert() error {
|
||||||
var err error
|
var err error
|
||||||
err = DB.Create(token).Error
|
err = DB.Create(token).Error
|
||||||
@@ -116,26 +127,53 @@ func DeleteTokenById(id int, userId int) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
quota := token.RemainQuota
|
|
||||||
if quota != 0 {
|
|
||||||
if quota > 0 {
|
|
||||||
err = IncreaseUserQuota(userId, quota)
|
|
||||||
} else {
|
|
||||||
err = DecreaseUserQuota(userId, -quota)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return token.Delete()
|
return token.Delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
func IncreaseTokenQuota(id int, quota int) (err error) {
|
func DecreaseTokenQuota(tokenId int, quota int) (err error) {
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
|
if quota < 0 {
|
||||||
return err
|
return errors.New("quota 不能为负数!")
|
||||||
}
|
}
|
||||||
|
token, err := GetTokenById(tokenId)
|
||||||
func DecreaseTokenQuota(id int, quota int) (err error) {
|
if err != nil {
|
||||||
err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
|
return err
|
||||||
|
}
|
||||||
|
if token.RemainQuota < quota {
|
||||||
|
return errors.New("令牌额度不足")
|
||||||
|
}
|
||||||
|
userQuota, err := GetUserQuota(token.UserId)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if userQuota < quota {
|
||||||
|
return errors.New("用户额度不足")
|
||||||
|
}
|
||||||
|
quotaTooLow := userQuota >= common.QuotaRemindThreshold && userQuota-quota < common.QuotaRemindThreshold
|
||||||
|
noMoreQuota := userQuota-quota <= 0
|
||||||
|
if quotaTooLow || noMoreQuota {
|
||||||
|
go func() {
|
||||||
|
email, err := GetUserEmail(token.UserId)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("获取用户邮箱失败:" + 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-quota, topUpLink, topUpLink))
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("发送邮件失败:" + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
err = DB.Model(&Token{}).Where("id = ?", tokenId).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = DecreaseUserQuota(token.UserId, quota)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,12 +225,23 @@ func GetUserQuota(id int) (quota int, err error) {
|
|||||||
return quota, err
|
return quota, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserEmail(id int) (email string, err error) {
|
||||||
|
err = DB.Model(&User{}).Where("id = ?", id).Select("email").Find(&email).Error
|
||||||
|
return email, err
|
||||||
|
}
|
||||||
|
|
||||||
func IncreaseUserQuota(id int, quota int) (err error) {
|
func IncreaseUserQuota(id int, quota int) (err error) {
|
||||||
|
if quota < 0 {
|
||||||
|
return errors.New("quota 不能为负数!")
|
||||||
|
}
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func DecreaseUserQuota(id int, quota int) (err error) {
|
func DecreaseUserQuota(id int, quota int) (err error) {
|
||||||
|
if quota < 0 {
|
||||||
|
return errors.New("quota 不能为负数!")
|
||||||
|
}
|
||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
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)
|
||||||
|
selfRoute.POST("/topup", controller.TopUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
adminRoute := userRoute.Group("/")
|
adminRoute := userRoute.Group("/")
|
||||||
@@ -74,7 +75,6 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
tokenRoute.GET("/", controller.GetAllTokens)
|
tokenRoute.GET("/", controller.GetAllTokens)
|
||||||
tokenRoute.GET("/search", controller.SearchTokens)
|
tokenRoute.GET("/search", controller.SearchTokens)
|
||||||
tokenRoute.POST("/topup", controller.TopUp)
|
|
||||||
tokenRoute.GET("/:id", controller.GetToken)
|
tokenRoute.GET("/:id", controller.GetToken)
|
||||||
tokenRoute.POST("/", controller.AddToken)
|
tokenRoute.POST("/", controller.AddToken)
|
||||||
tokenRoute.PUT("/", controller.UpdateToken)
|
tokenRoute.PUT("/", controller.UpdateToken)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import NotFound from './pages/NotFound';
|
|||||||
import Setting from './pages/Setting';
|
import Setting from './pages/Setting';
|
||||||
import EditUser from './pages/User/EditUser';
|
import EditUser from './pages/User/EditUser';
|
||||||
import AddUser from './pages/User/AddUser';
|
import AddUser from './pages/User/AddUser';
|
||||||
import { API, showError, showNotice } from './helpers';
|
import { API, getLogo, getSystemName, showError, showNotice } from './helpers';
|
||||||
import PasswordResetForm from './components/PasswordResetForm';
|
import PasswordResetForm from './components/PasswordResetForm';
|
||||||
import GitHubOAuth from './components/GitHubOAuth';
|
import GitHubOAuth from './components/GitHubOAuth';
|
||||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
import PasswordResetConfirm from './components/PasswordResetConfirm';
|
||||||
@@ -21,6 +21,7 @@ import EditToken from './pages/Token/EditToken';
|
|||||||
import EditChannel from './pages/Channel/EditChannel';
|
import EditChannel from './pages/Channel/EditChannel';
|
||||||
import Redemption from './pages/Redemption';
|
import Redemption from './pages/Redemption';
|
||||||
import EditRedemption from './pages/Redemption/EditRedemption';
|
import EditRedemption from './pages/Redemption/EditRedemption';
|
||||||
|
import TopUp from './pages/TopUp';
|
||||||
|
|
||||||
const Home = lazy(() => import('./pages/Home'));
|
const Home = lazy(() => import('./pages/Home'));
|
||||||
const About = lazy(() => import('./pages/About'));
|
const About = lazy(() => import('./pages/About'));
|
||||||
@@ -62,6 +63,17 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadUser();
|
loadUser();
|
||||||
loadStatus().then();
|
loadStatus().then();
|
||||||
|
let systemName = getSystemName();
|
||||||
|
if (systemName) {
|
||||||
|
document.title = systemName;
|
||||||
|
}
|
||||||
|
let logo = getLogo();
|
||||||
|
if (logo) {
|
||||||
|
let linkElement = document.querySelector("link[rel~='icon']");
|
||||||
|
if (linkElement) {
|
||||||
|
linkElement.href = logo;
|
||||||
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -228,6 +240,16 @@ function App() {
|
|||||||
</PrivateRoute>
|
</PrivateRoute>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='/topup'
|
||||||
|
element={
|
||||||
|
<PrivateRoute>
|
||||||
|
<Suspense fallback={<Loading></Loading>}>
|
||||||
|
<TopUp />
|
||||||
|
</Suspense>
|
||||||
|
</PrivateRoute>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path='/about'
|
path='/about'
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ const headerButtons = [
|
|||||||
icon: 'dollar sign',
|
icon: 'dollar sign',
|
||||||
admin: true,
|
admin: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: '充值',
|
||||||
|
to: '/topup',
|
||||||
|
icon: 'cart',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: '用户',
|
name: '用户',
|
||||||
to: '/user',
|
to: '/user',
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ const OtherSetting = () => {
|
|||||||
Logo: '',
|
Logo: '',
|
||||||
HomePageContent: '',
|
HomePageContent: '',
|
||||||
});
|
});
|
||||||
let originInputs = {};
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||||
const [updateData, setUpdateData] = useState({
|
const [updateData, setUpdateData] = useState({
|
||||||
@@ -21,7 +20,7 @@ const OtherSetting = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option');
|
const res = await API.get('/api/option/');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let newInputs = {};
|
let newInputs = {};
|
||||||
@@ -31,7 +30,6 @@ const OtherSetting = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
setInputs(newInputs);
|
setInputs(newInputs);
|
||||||
originInputs = newInputs;
|
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@@ -43,7 +41,7 @@ const OtherSetting = () => {
|
|||||||
|
|
||||||
const updateOption = async (key, value) => {
|
const updateOption = async (key, value) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const res = await API.put('/api/option', {
|
const res = await API.put('/api/option/', {
|
||||||
key,
|
key,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -27,16 +27,17 @@ const SystemSetting = () => {
|
|||||||
TurnstileSecretKey: '',
|
TurnstileSecretKey: '',
|
||||||
RegisterEnabled: '',
|
RegisterEnabled: '',
|
||||||
QuotaForNewUser: 0,
|
QuotaForNewUser: 0,
|
||||||
|
QuotaRemindThreshold: 0,
|
||||||
ModelRatio: '',
|
ModelRatio: '',
|
||||||
TopUpLink: '',
|
TopUpLink: '',
|
||||||
AutomaticDisableChannelEnabled: '',
|
AutomaticDisableChannelEnabled: '',
|
||||||
ChannelDisableThreshold: 0,
|
ChannelDisableThreshold: 0,
|
||||||
});
|
});
|
||||||
let originInputs = {};
|
const [originInputs, setOriginInputs] = useState({});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const getOptions = async () => {
|
const getOptions = async () => {
|
||||||
const res = await API.get('/api/option');
|
const res = await API.get('/api/option/');
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
let newInputs = {};
|
let newInputs = {};
|
||||||
@@ -44,7 +45,7 @@ const SystemSetting = () => {
|
|||||||
newInputs[item.key] = item.value;
|
newInputs[item.key] = item.value;
|
||||||
});
|
});
|
||||||
setInputs(newInputs);
|
setInputs(newInputs);
|
||||||
originInputs = newInputs;
|
setOriginInputs(newInputs);
|
||||||
} else {
|
} else {
|
||||||
showError(message);
|
showError(message);
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ const SystemSetting = () => {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const res = await API.put('/api/option', {
|
const res = await API.put('/api/option/', {
|
||||||
key,
|
key,
|
||||||
value
|
value
|
||||||
});
|
});
|
||||||
@@ -96,6 +97,7 @@ const SystemSetting = () => {
|
|||||||
name === 'TurnstileSiteKey' ||
|
name === 'TurnstileSiteKey' ||
|
||||||
name === 'TurnstileSecretKey' ||
|
name === 'TurnstileSecretKey' ||
|
||||||
name === 'QuotaForNewUser' ||
|
name === 'QuotaForNewUser' ||
|
||||||
|
name === 'QuotaRemindThreshold' ||
|
||||||
name === 'ModelRatio' ||
|
name === 'ModelRatio' ||
|
||||||
name === 'TopUpLink'
|
name === 'TopUpLink'
|
||||||
) {
|
) {
|
||||||
@@ -114,6 +116,9 @@ const SystemSetting = () => {
|
|||||||
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
||||||
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
||||||
}
|
}
|
||||||
|
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
|
||||||
|
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
|
||||||
|
}
|
||||||
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
||||||
if (!verifyJSON(inputs.ModelRatio)) {
|
if (!verifyJSON(inputs.ModelRatio)) {
|
||||||
showError('模型倍率不是合法的 JSON 字符串');
|
showError('模型倍率不是合法的 JSON 字符串');
|
||||||
@@ -287,6 +292,16 @@ const SystemSetting = () => {
|
|||||||
type='link'
|
type='link'
|
||||||
placeholder='例如发卡网站的购买链接'
|
placeholder='例如发卡网站的购买链接'
|
||||||
/>
|
/>
|
||||||
|
<Form.Input
|
||||||
|
label='额度提醒阈值'
|
||||||
|
name='QuotaRemindThreshold'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='new-password'
|
||||||
|
value={inputs.QuotaRemindThreshold}
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
placeholder='低于此额度时将发送邮件提醒用户'
|
||||||
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ const TokensTable = () => {
|
|||||||
const [searching, setSearching] = useState(false);
|
const [searching, setSearching] = useState(false);
|
||||||
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
const [showTopUpModal, setShowTopUpModal] = useState(false);
|
||||||
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
|
||||||
const [redemptionCode, setRedemptionCode] = useState('');
|
|
||||||
const [topUpLink, setTopUpLink] = useState('');
|
|
||||||
|
|
||||||
const loadTokens = async (startIdx) => {
|
const loadTokens = async (startIdx) => {
|
||||||
const res = await API.get(`/api/token/?p=${startIdx}`);
|
const res = await API.get(`/api/token/?p=${startIdx}`);
|
||||||
@@ -77,13 +75,6 @@ const TokensTable = () => {
|
|||||||
.catch((reason) => {
|
.catch((reason) => {
|
||||||
showError(reason);
|
showError(reason);
|
||||||
});
|
});
|
||||||
let status = localStorage.getItem('status');
|
|
||||||
if (status) {
|
|
||||||
status = JSON.parse(status);
|
|
||||||
if (status.top_up_link) {
|
|
||||||
setTopUpLink(status.top_up_link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const manageToken = async (id, action, idx) => {
|
const manageToken = async (id, action, idx) => {
|
||||||
@@ -156,28 +147,6 @@ const TokensTable = () => {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const topUp = async () => {
|
|
||||||
if (redemptionCode === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const res = await API.post('/api/token/topup/', {
|
|
||||||
id: tokens[targetTokenIdx].id,
|
|
||||||
key: redemptionCode
|
|
||||||
});
|
|
||||||
const { success, message, data } = res.data;
|
|
||||||
if (success) {
|
|
||||||
showSuccess('充值成功!');
|
|
||||||
let newTokens = [...tokens];
|
|
||||||
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + targetTokenIdx;
|
|
||||||
newTokens[realIdx].remain_quota += data;
|
|
||||||
setTokens(newTokens);
|
|
||||||
setRedemptionCode('');
|
|
||||||
setShowTopUpModal(false);
|
|
||||||
} else {
|
|
||||||
showError(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form onSubmit={searchTokens}>
|
<Form onSubmit={searchTokens}>
|
||||||
@@ -279,15 +248,6 @@ const TokensTable = () => {
|
|||||||
>
|
>
|
||||||
复制
|
复制
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
size={'small'}
|
|
||||||
color={'yellow'}
|
|
||||||
onClick={() => {
|
|
||||||
setTargetTokenIdx(idx);
|
|
||||||
setShowTopUpModal(true);
|
|
||||||
}}>
|
|
||||||
充值
|
|
||||||
</Button>
|
|
||||||
<Popup
|
<Popup
|
||||||
trigger={
|
trigger={
|
||||||
<Button size='small' negative>
|
<Button size='small' negative>
|
||||||
@@ -355,39 +315,6 @@ const TokensTable = () => {
|
|||||||
</Table.Row>
|
</Table.Row>
|
||||||
</Table.Footer>
|
</Table.Footer>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
<Modal
|
|
||||||
onClose={() => setShowTopUpModal(false)}
|
|
||||||
onOpen={() => setShowTopUpModal(true)}
|
|
||||||
open={showTopUpModal}
|
|
||||||
size={'mini'}
|
|
||||||
>
|
|
||||||
<Modal.Header>通过兑换码为令牌「{tokens[targetTokenIdx]?.name}」充值</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
<Modal.Description>
|
|
||||||
{/*<Image src={status.wechat_qrcode} fluid />*/}
|
|
||||||
{
|
|
||||||
topUpLink && <p>
|
|
||||||
<a target='_blank' href={topUpLink}>点击此处获取兑换码</a>
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
<Form size='large'>
|
|
||||||
<Form.Input
|
|
||||||
fluid
|
|
||||||
placeholder='兑换码'
|
|
||||||
name='redemptionCode'
|
|
||||||
value={redemptionCode}
|
|
||||||
onChange={(e) => {
|
|
||||||
setRedemptionCode(e.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button color='' fluid size='large' onClick={topUp}>
|
|
||||||
充值
|
|
||||||
</Button>
|
|
||||||
</Form>
|
|
||||||
</Modal.Description>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -182,6 +182,14 @@ const UsersTable = () => {
|
|||||||
>
|
>
|
||||||
邮箱地址
|
邮箱地址
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
|
<Table.HeaderCell
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
onClick={() => {
|
||||||
|
sortUser('quota');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
剩余额度
|
||||||
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -215,6 +223,7 @@ const UsersTable = () => {
|
|||||||
<Table.Cell>{user.username}</Table.Cell>
|
<Table.Cell>{user.username}</Table.Cell>
|
||||||
<Table.Cell>{user.display_name}</Table.Cell>
|
<Table.Cell>{user.display_name}</Table.Cell>
|
||||||
<Table.Cell>{user.email ? user.email : '无'}</Table.Cell>
|
<Table.Cell>{user.email ? user.email : '无'}</Table.Cell>
|
||||||
|
<Table.Cell>{user.quota}</Table.Cell>
|
||||||
<Table.Cell>{renderRole(user.role)}</Table.Cell>
|
<Table.Cell>{renderRole(user.role)}</Table.Cell>
|
||||||
<Table.Cell>{renderStatus(user.status)}</Table.Cell>
|
<Table.Cell>{renderStatus(user.status)}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
@@ -284,7 +293,7 @@ const UsersTable = () => {
|
|||||||
|
|
||||||
<Table.Footer>
|
<Table.Footer>
|
||||||
<Table.Row>
|
<Table.Row>
|
||||||
<Table.HeaderCell colSpan='6'>
|
<Table.HeaderCell colSpan='7'>
|
||||||
<Button size='small' as={Link} to='/user/add' loading={loading}>
|
<Button size='small' as={Link} to='/user/add' loading={loading}>
|
||||||
添加新的用户
|
添加新的用户
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Header, Segment } from 'semantic-ui-react';
|
import { Button, Form, Header, Message, Segment } from 'semantic-ui-react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { API, isAdmin, showError, showSuccess, timestamp2string } from '../../helpers';
|
import { API, showError, showSuccess, timestamp2string } from '../../helpers';
|
||||||
|
|
||||||
const EditToken = () => {
|
const EditToken = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -14,7 +14,6 @@ const EditToken = () => {
|
|||||||
expired_time: -1,
|
expired_time: -1,
|
||||||
unlimited_quota: false
|
unlimited_quota: false
|
||||||
};
|
};
|
||||||
const isAdminUser = isAdmin();
|
|
||||||
const [inputs, setInputs] = useState(originInputs);
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
|
||||||
|
|
||||||
@@ -107,25 +106,22 @@ const EditToken = () => {
|
|||||||
required={!isEdit}
|
required={!isEdit}
|
||||||
/>
|
/>
|
||||||
</Form.Field>
|
</Form.Field>
|
||||||
{
|
<Message>注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。</Message>
|
||||||
isAdminUser && <>
|
<Form.Field>
|
||||||
<Form.Field>
|
<Form.Input
|
||||||
<Form.Input
|
label='额度'
|
||||||
label='额度'
|
name='remain_quota'
|
||||||
name='remain_quota'
|
placeholder={'请输入额度'}
|
||||||
placeholder={'请输入额度'}
|
onChange={handleInputChange}
|
||||||
onChange={handleInputChange}
|
value={remain_quota}
|
||||||
value={remain_quota}
|
autoComplete='new-password'
|
||||||
autoComplete='new-password'
|
type='number'
|
||||||
type='number'
|
disabled={unlimited_quota}
|
||||||
disabled={unlimited_quota}
|
/>
|
||||||
/>
|
</Form.Field>
|
||||||
</Form.Field>
|
<Button type={'button'} style={{ marginBottom: '14px' }} onClick={() => {
|
||||||
<Button type={'button'} style={{marginBottom: '14px'}} onClick={() => {
|
setUnlimitedQuota();
|
||||||
setUnlimitedQuota();
|
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button>
|
||||||
}}>{unlimited_quota ? '取消无限额度' : '设置为无限额度'}</Button>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
<Form.Field>
|
<Form.Field>
|
||||||
<Form.Input
|
<Form.Input
|
||||||
label='过期时间'
|
label='过期时间'
|
||||||
|
|||||||
94
web/src/pages/TopUp/index.js
Normal file
94
web/src/pages/TopUp/index.js
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Button, Form, Grid, Header, Segment, Statistic } from 'semantic-ui-react';
|
||||||
|
import { API, showError, showSuccess } from '../../helpers';
|
||||||
|
|
||||||
|
const TopUp = () => {
|
||||||
|
const [redemptionCode, setRedemptionCode] = useState('');
|
||||||
|
const [topUpLink, setTopUpLink] = useState('');
|
||||||
|
const [userQuota, setUserQuota] = useState(0);
|
||||||
|
|
||||||
|
const topUp = async () => {
|
||||||
|
if (redemptionCode === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await API.post('/api/user/topup', {
|
||||||
|
key: redemptionCode
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess('充值成功!');
|
||||||
|
setUserQuota((quota) => {
|
||||||
|
return quota + data;
|
||||||
|
});
|
||||||
|
setRedemptionCode('');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTopUpLink = () => {
|
||||||
|
if (!topUpLink) {
|
||||||
|
showError('超级管理员未设置充值链接!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open(topUpLink, '_blank');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUserQuota = async ()=>{
|
||||||
|
let res = await API.get(`/api/user/self`);
|
||||||
|
const {success, message, data} = res.data;
|
||||||
|
if (success) {
|
||||||
|
setUserQuota(data.quota);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let status = localStorage.getItem('status');
|
||||||
|
if (status) {
|
||||||
|
status = JSON.parse(status);
|
||||||
|
if (status.top_up_link) {
|
||||||
|
setTopUpLink(status.top_up_link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getUserQuota().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Segment>
|
||||||
|
<Header as='h3'>充值额度</Header>
|
||||||
|
<Grid columns={2} stackable>
|
||||||
|
<Grid.Column>
|
||||||
|
<Form>
|
||||||
|
<Form.Input
|
||||||
|
placeholder='兑换码'
|
||||||
|
name='redemptionCode'
|
||||||
|
value={redemptionCode}
|
||||||
|
onChange={(e) => {
|
||||||
|
setRedemptionCode(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button color='green' onClick={openTopUpLink}>
|
||||||
|
获取兑换码
|
||||||
|
</Button>
|
||||||
|
<Button color='yellow' onClick={topUp}>
|
||||||
|
充值
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</Grid.Column>
|
||||||
|
<Grid.Column>
|
||||||
|
<Statistic.Group widths='one'>
|
||||||
|
<Statistic>
|
||||||
|
<Statistic.Value>{userQuota}</Statistic.Value>
|
||||||
|
<Statistic.Label>剩余额度</Statistic.Label>
|
||||||
|
</Statistic>
|
||||||
|
</Statistic.Group>
|
||||||
|
</Grid.Column>
|
||||||
|
</Grid>
|
||||||
|
</Segment>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export default TopUp;
|
||||||
Reference in New Issue
Block a user