From 01abed0a3028d49d5184b6e65ea4d6cd4799c515 Mon Sep 17 00:00:00 2001 From: JustSong Date: Tue, 16 May 2023 11:26:09 +0800 Subject: [PATCH] refactor: bind quota to account instead of token (close #64, #31) --- common/constants.go | 4 +- controller/token.go | 65 +++------------------ controller/user.go | 31 ++++++++++ model/redemption.go | 8 +-- model/token.go | 74 ++++++++++++++++++------ model/user.go | 11 ++++ router/api-router.go | 2 +- web/src/App.js | 11 ++++ web/src/components/Header.js | 6 ++ web/src/components/TokensTable.js | 73 ------------------------ web/src/pages/Token/EditToken.js | 37 ++++++------ web/src/pages/TopUp/index.js | 94 +++++++++++++++++++++++++++++++ 12 files changed, 240 insertions(+), 176 deletions(-) create mode 100644 web/src/pages/TopUp/index.js 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 ( <>
@@ -279,15 +248,6 @@ const TokensTable = () => { > 复制 - @@ -355,39 +315,6 @@ const TokensTable = () => { - - setShowTopUpModal(false)} - onOpen={() => setShowTopUpModal(true)} - open={showTopUpModal} - size={'mini'} - > - 通过兑换码为令牌「{tokens[targetTokenIdx]?.name}」充值 - - - {/**/} - { - topUpLink &&

- 点击此处获取兑换码 -

- } - - { - setRedemptionCode(e.target.value); - }} - /> - - -
-
-
); }; 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;