diff --git a/common/constants.go b/common/constants.go
index 1ad0770d..3ca6ba90 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -50,10 +50,10 @@ var WeChatAccountQRCodeImageURL = ""
var TurnstileSiteKey = ""
var TurnstileSecretKey = ""
-var QuotaForNewUser = 100
+var QuotaForNewUser = 0
var ChannelDisableThreshold = 5.0
var AutomaticDisableChannelEnabled = false
-var QuotaRemindThreshold = 1000 // TODO: QuotaRemindThreshold
+var QuotaRemindThreshold = 1000
var RootUserEmail = ""
diff --git a/controller/token.go b/controller/token.go
index b8b26d4b..6fbf22df 100644
--- a/controller/token.go
+++ b/controller/token.go
@@ -100,7 +100,6 @@ func GetTokenStatus(c *gin.Context) {
}
func AddToken(c *gin.Context) {
- isAdmin := c.GetInt("role") >= common.RoleAdminUser
token := model.Token{}
err := c.ShouldBindJSON(&token)
if err != nil {
@@ -118,27 +117,14 @@ func AddToken(c *gin.Context) {
return
}
cleanToken := model.Token{
- UserId: c.GetInt("id"),
- Name: token.Name,
- Key: common.GetUUID(),
- CreatedTime: common.GetTimestamp(),
- AccessedTime: common.GetTimestamp(),
- ExpiredTime: token.ExpiredTime,
- }
- if isAdmin {
- 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
+ UserId: c.GetInt("id"),
+ Name: token.Name,
+ Key: common.GetUUID(),
+ CreatedTime: common.GetTimestamp(),
+ AccessedTime: common.GetTimestamp(),
+ ExpiredTime: token.ExpiredTime,
+ RemainQuota: token.RemainQuota,
+ UnlimitedQuota: token.UnlimitedQuota,
}
err = cleanToken.Insert()
if err != nil {
@@ -148,10 +134,6 @@ func AddToken(c *gin.Context) {
})
return
}
- if !isAdmin {
- // update user quota
- err = model.DecreaseUserQuota(c.GetInt("id"), cleanToken.RemainQuota)
- }
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -240,34 +222,3 @@ func UpdateToken(c *gin.Context) {
})
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
-}
diff --git a/controller/user.go b/controller/user.go
index bebece0b..51328d78 100644
--- a/controller/user.go
+++ b/controller/user.go
@@ -654,3 +654,34 @@ func EmailBind(c *gin.Context) {
})
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
+}
diff --git a/model/redemption.go b/model/redemption.go
index d499108a..b731acf7 100644
--- a/model/redemption.go
+++ b/model/redemption.go
@@ -40,12 +40,12 @@ func GetRedemptionById(id int) (*Redemption, error) {
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 == "" {
return 0, errors.New("未提供兑换码")
}
- if tokenId == 0 {
- return 0, errors.New("未提供 token id")
+ if userId == 0 {
+ return 0, errors.New("无效的 user id")
}
redemption := &Redemption{}
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 {
return 0, errors.New("该兑换码已被使用")
}
- err = IncreaseTokenQuota(tokenId, redemption.Quota)
+ err = IncreaseUserQuota(userId, redemption.Quota)
if err != nil {
return 0, err
}
diff --git a/model/token.go b/model/token.go
index 66fc911e..f24330d1 100644
--- a/model/token.go
+++ b/model/token.go
@@ -2,6 +2,7 @@ package model
import (
"errors"
+ "fmt"
_ "gorm.io/driver/sqlite"
"gorm.io/gorm"
"one-api/common"
@@ -82,6 +83,16 @@ func GetTokenByIds(id int, userId int) (*Token, error) {
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 {
var err error
err = DB.Create(token).Error
@@ -116,26 +127,53 @@ func DeleteTokenById(id int, userId int) (err error) {
if err != nil {
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()
}
-func IncreaseTokenQuota(id int, quota int) (err error) {
- err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota + ?", quota)).Error
- return err
-}
-
-func DecreaseTokenQuota(id int, quota int) (err error) {
- err = DB.Model(&Token{}).Where("id = ?", id).Update("remain_quota", gorm.Expr("remain_quota - ?", quota)).Error
+func DecreaseTokenQuota(tokenId int, quota int) (err error) {
+ if quota < 0 {
+ return errors.New("quota 不能为负数!")
+ }
+ token, err := GetTokenById(tokenId)
+ if err != nil {
+ 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,为了不影响您的使用,请及时充值。
充值链接:%s", 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
}
diff --git a/model/user.go b/model/user.go
index b121753d..a54351c7 100644
--- a/model/user.go
+++ b/model/user.go
@@ -225,12 +225,23 @@ func GetUserQuota(id int) (quota int, err error) {
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) {
+ if quota < 0 {
+ return errors.New("quota 不能为负数!")
+ }
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota + ?", quota)).Error
return err
}
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
return err
}
diff --git a/router/api-router.go b/router/api-router.go
index 9e7f580d..5cd86e3e 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -37,6 +37,7 @@ func SetApiRouter(router *gin.Engine) {
selfRoute.PUT("/self", controller.UpdateSelf)
selfRoute.DELETE("/self", controller.DeleteSelf)
selfRoute.GET("/token", controller.GenerateAccessToken)
+ selfRoute.POST("/topup", controller.TopUp)
}
adminRoute := userRoute.Group("/")
@@ -74,7 +75,6 @@ func SetApiRouter(router *gin.Engine) {
{
tokenRoute.GET("/", controller.GetAllTokens)
tokenRoute.GET("/search", controller.SearchTokens)
- tokenRoute.POST("/topup", controller.TopUp)
tokenRoute.GET("/:id", controller.GetToken)
tokenRoute.POST("/", controller.AddToken)
tokenRoute.PUT("/", controller.UpdateToken)
diff --git a/web/src/App.js b/web/src/App.js
index cb0a3d9d..b2699858 100644
--- a/web/src/App.js
+++ b/web/src/App.js
@@ -21,6 +21,7 @@ import EditToken from './pages/Token/EditToken';
import EditChannel from './pages/Channel/EditChannel';
import Redemption from './pages/Redemption';
import EditRedemption from './pages/Redemption/EditRedemption';
+import TopUp from './pages/TopUp';
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
@@ -239,6 +240,16 @@ function App() {
}
/>
+
+ }>
+
+
+
+ }
+ />
{
const [searching, setSearching] = useState(false);
const [showTopUpModal, setShowTopUpModal] = useState(false);
const [targetTokenIdx, setTargetTokenIdx] = useState(0);
- const [redemptionCode, setRedemptionCode] = useState('');
- const [topUpLink, setTopUpLink] = useState('');
const loadTokens = async (startIdx) => {
const res = await API.get(`/api/token/?p=${startIdx}`);
@@ -77,13 +75,6 @@ const TokensTable = () => {
.catch((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) => {
@@ -156,28 +147,6 @@ const TokensTable = () => {
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 (
<>
-
-
-
>
);
};
diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js
index bba7f5e9..2540bfda 100644
--- a/web/src/pages/Token/EditToken.js
+++ b/web/src/pages/Token/EditToken.js
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { Button, Form, Header, Segment } from 'semantic-ui-react';
import { useParams } from 'react-router-dom';
-import { API, isAdmin, showError, showSuccess, timestamp2string } from '../../helpers';
+import { API, showError, showSuccess, timestamp2string } from '../../helpers';
const EditToken = () => {
const params = useParams();
@@ -14,7 +14,6 @@ const EditToken = () => {
expired_time: -1,
unlimited_quota: false
};
- const isAdminUser = isAdmin();
const [inputs, setInputs] = useState(originInputs);
const { name, remain_quota, expired_time, unlimited_quota } = inputs;
@@ -107,25 +106,21 @@ const EditToken = () => {
required={!isEdit}
/>
- {
- isAdminUser && <>
-
-
-
-
- >
- }
+
+
+
+
{
+ 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 (
+
+
+
+
+ {
+ setRedemptionCode(e.target.value);
+ }}
+ />
+
+
+
+
+
+
+
+ {userQuota}
+ 剩余额度
+
+
+
+
+
+ );
+};
+
+
+export default TopUp;