diff --git a/README.md b/README.md
index ea44e5c7..984a0efb 100644
--- a/README.md
+++ b/README.md
@@ -52,12 +52,13 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
+ [x] 自定义渠道
2. 支持通过负载均衡的方式访问多个渠道。
3. 支持单个访问渠道设置多个 API Key,利用起来你的多个 API Key。
-4. 支持 HTTP SSE。
-5. 多种用户登录注册方式:
+4. 支持设置令牌的过期时间和使用次数。
+5. 支持 HTTP SSE。
+6. 多种用户登录注册方式:
+ 邮箱登录注册以及通过邮箱进行密码重置。
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
-6. 支持用户管理。
+7. 支持用户管理。
## 部署
### 基于 Docker 进行部署
diff --git a/common/constants.go b/common/constants.go
index 14ddd304..1298789e 100644
--- a/common/constants.go
+++ b/common/constants.go
@@ -87,8 +87,10 @@ const (
)
const (
- TokenStatusEnabled = 1 // don't use 0, 0 is the default value!
- TokenStatusDisabled = 2 // also don't use 0
+ TokenStatusEnabled = 1 // don't use 0, 0 is the default value!
+ TokenStatusDisabled = 2 // also don't use 0
+ TokenStatusExpired = 3
+ TokenStatusExhausted = 4
)
const (
diff --git a/controller/token.go b/controller/token.go
index e8433b69..c80ed4b1 100644
--- a/controller/token.go
+++ b/controller/token.go
@@ -98,6 +98,8 @@ func AddToken(c *gin.Context) {
Key: common.GetUUID(),
CreatedTime: common.GetTimestamp(),
AccessedTime: common.GetTimestamp(),
+ ExpiredTime: token.ExpiredTime,
+ RemainTimes: token.RemainTimes,
}
err = cleanToken.Insert()
if err != nil {
@@ -151,8 +153,27 @@ func UpdateToken(c *gin.Context) {
})
return
}
+ if token.Status == common.TokenStatusEnabled {
+ if cleanToken.Status == common.TokenStatusExpired && cleanToken.ExpiredTime <= common.GetTimestamp() {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "令牌已过期,无法启用,请先修改令牌过期时间",
+ })
+ return
+ }
+ if cleanToken.Status == common.TokenStatusExhausted && cleanToken.RemainTimes == 0 {
+ c.JSON(http.StatusOK, gin.H{
+ "success": false,
+ "message": "令牌可用次数已用尽,无法启用,请先修改令牌剩余次数",
+ })
+ return
+ }
+ }
+
cleanToken.Name = token.Name
cleanToken.Status = token.Status
+ cleanToken.ExpiredTime = token.ExpiredTime
+ cleanToken.RemainTimes = token.RemainTimes
err = cleanToken.Update()
if err != nil {
c.JSON(http.StatusOK, gin.H{
diff --git a/model/token.go b/model/token.go
index 5b3bed56..e42e8c84 100644
--- a/model/token.go
+++ b/model/token.go
@@ -15,6 +15,8 @@ type Token struct {
Name string `json:"name" gorm:"index" `
CreatedTime int64 `json:"created_time" gorm:"bigint"`
AccessedTime int64 `json:"accessed_time" gorm:"bigint"`
+ ExpiredTime int64 `json:"expired_time" gorm:"bigint;default:-1"` // -1 means never expired
+ RemainTimes int `json:"remain_times" gorm:"default:-1"` // -1 means infinite times
}
func GetAllUserTokens(userId int, startIdx int, num int) ([]*Token, error) {
@@ -38,13 +40,27 @@ func ValidateUserToken(key string) (token *Token, err error) {
err = DB.Where("key = ?", key).First(token).Error
if err == nil {
if token.Status != common.TokenStatusEnabled {
- return nil, errors.New("该 token 已被禁用")
+ return nil, errors.New("该 token 状态不可用")
+ }
+ if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() {
+ token.Status = common.TokenStatusExpired
+ err := token.SelectUpdate()
+ if err != nil {
+ common.SysError("更新 token 状态失败:" + err.Error())
+ }
+ return nil, errors.New("该 token 已过期")
}
go func() {
token.AccessedTime = common.GetTimestamp()
- err := token.Update()
+ if token.RemainTimes > 0 {
+ token.RemainTimes--
+ if token.RemainTimes == 0 {
+ token.Status = common.TokenStatusExhausted
+ }
+ }
+ err := token.SelectUpdate()
if err != nil {
- common.SysError("更新 token 访问时间失败:" + err.Error())
+ common.SysError("更新 token 失败:" + err.Error())
}
}()
return token, nil
@@ -74,6 +90,11 @@ func (token *Token) Update() error {
return err
}
+func (token *Token) SelectUpdate() error {
+ // This can update zero values
+ return DB.Model(token).Select("accessed_time", "remain_times", "status").Updates(token).Error
+}
+
func (token *Token) Delete() error {
var err error
err = DB.Delete(token).Error
diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js
index 59e6afd2..d1177f08 100644
--- a/web/src/components/TokensTable.js
+++ b/web/src/components/TokensTable.js
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
-import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react';
+import { Button, Form, Label, Message, Pagination, Table } from 'semantic-ui-react';
import { Link } from 'react-router-dom';
import { API, copy, showError, showSuccess, timestamp2string } from '../helpers';
@@ -13,6 +13,21 @@ function renderTimestamp(timestamp) {
);
}
+function renderStatus(status) {
+ switch (status) {
+ case 1:
+ return ;
+ case 2:
+ return ;
+ case 3:
+ return ;
+ case 4:
+ return ;
+ default:
+ return ;
+ }
+}
+
const TokensTable = () => {
const [tokens, setTokens] = useState([]);
const [loading, setLoading] = useState(true);
@@ -88,25 +103,6 @@ const TokensTable = () => {
}
};
- const renderStatus = (status) => {
- switch (status) {
- case 1:
- return ;
- case 2:
- return (
-
- );
- default:
- return (
-
- );
- }
- };
-
const searchTokens = async () => {
if (searchKeyword === '') {
// if keyword is blank, load files instead.
@@ -185,6 +181,14 @@ const TokensTable = () => {
>
状态
+
{
+ sortToken('remain_times');
+ }}
+ >
+ 剩余次数
+
{
@@ -201,6 +205,14 @@ const TokensTable = () => {
>
访问时间
+ {
+ sortToken('expired_time');
+ }}
+ >
+ 过期时间
+
操作
@@ -218,8 +230,10 @@ const TokensTable = () => {
{token.id}
{token.name ? token.name : '无'}
{renderStatus(token.status)}
+ {token.remain_times === -1 ? "无限制" : token.remain_times}
{renderTimestamp(token.created_time)}
{renderTimestamp(token.accessed_time)}
+ {token.expired_time === -1 ? "永不过期" : renderTimestamp(token.expired_time)}