feat: 引入到期时间概念,当非default的分组时将有到期时间概念

fix: 修复旧用户的vip被降级,尽量不对原先用户做变动
This commit is contained in:
jinjianmingming 2024-07-17 11:37:24 +08:00
parent adba54acd3
commit b86a3acd37
4 changed files with 288 additions and 149 deletions

View File

@ -95,6 +95,9 @@ func SyncOptions(frequency int) {
for { for {
time.Sleep(time.Duration(frequency) * time.Second) time.Sleep(time.Duration(frequency) * time.Second)
logger.SysLog("syncing options from database") logger.SysLog("syncing options from database")
if config.IsMasterNode {
checkAndDowngradeUsers()
}
loadOptionsFromDatabase() loadOptionsFromDatabase()
} }
} }

View File

@ -10,7 +10,9 @@ import (
"github.com/songquanpeng/one-api/common/logger" "github.com/songquanpeng/one-api/common/logger"
"github.com/songquanpeng/one-api/common/random" "github.com/songquanpeng/one-api/common/random"
"gorm.io/gorm" "gorm.io/gorm"
"log"
"strings" "strings"
"time"
) )
const ( const (
@ -47,6 +49,7 @@ type User struct {
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"`
InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"` InviterId int `json:"inviter_id" gorm:"type:int;column:inviter_id;index"`
ExpirationDate int64 `json:"expiration_date" gorm:"column:expiration_date"` // Expiration date of the user's subscription or account.
} }
func GetMaxUserId() int { func GetMaxUserId() int {
@ -210,6 +213,25 @@ func (user *User) ValidateAndFill() (err error) {
if !okay || user.Status != UserStatusEnabled { if !okay || user.Status != UserStatusEnabled {
return errors.New("用户名或密码错误,或用户已被封禁") return errors.New("用户名或密码错误,或用户已被封禁")
} }
// 校验用户是不是非default,如果是非default,判断到期时间如果过期了降级为default
if user.ExpirationDate > 0 {
// 将时间戳转换为 time.Time 类型
expirationTime := time.Unix(user.ExpirationDate, 0)
// 获取当前时间
currentTime := time.Now()
// 比较当前时间和到期时间
if expirationTime.Before(currentTime) {
// 降级为 default
user.Group = "default"
err := DB.Model(user).Updates(user).Error
if err != nil {
fmt.Printf("用户: %s, 降级为 default 时发生错误: %v\n", user.Username, err)
return err
}
fmt.Printf("用户: %s, 特权组过期降为 default\n", user.Username)
}
}
return nil return nil
} }
@ -435,3 +457,40 @@ func GetUsernameById(id int) (username string) {
DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username) DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&username)
return username return username
} }
func checkAndDowngradeUsers() {
var users []User
// 查询所有 Group 不为 "default" 的用户
// 构建查询条件
query := DB.Where("`Group` <> ?", "default"). // Group 不等于 "default"
Where("`username` <> ?", "root"). // username 不等于 "root"
Where("`expiration_date` IS NOT NULL"). // expiration_date 不为空
Where("`expiration_date` != ?", -1) // expiration_date 不等于 -1
// 执行查询并处理错误
if err := query.Find(&users).Error; err != nil {
log.Printf("查询用户失败: %v", err)
return
}
currentTime := time.Now()
for _, user := range users {
if user.Group != "default" {
// 将时间戳转换为 time.Time 类型
expirationTime := time.Unix(user.ExpirationDate, 0)
// 比较当前时间和到期时间
if expirationTime.Before(currentTime) {
// 降级为 default
user.Group = "default"
if err := DB.Model(&user).Updates(user).Error; err != nil {
log.Printf("更新用户 %s 失败: %v", user.Username, err)
} else {
fmt.Printf("用户: %s, 特权组过期降为 default\n", user.Username)
}
}
}
}
}

View File

@ -17,6 +17,7 @@
"@tabler/icons-react": "^2.44.0", "@tabler/icons-react": "^2.44.0",
"apexcharts": "3.35.3", "apexcharts": "3.35.3",
"axios": "^0.27.2", "axios": "^0.27.2",
"date-fns": "^3.6.0",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"formik": "^2.2.9", "formik": "^2.2.9",
"framer-motion": "^6.3.16", "framer-motion": "^6.3.16",
@ -27,6 +28,7 @@
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-apexcharts": "1.4.0", "react-apexcharts": "1.4.0",
"react-datepicker": "^7.3.0",
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-perfect-scrollbar": "^1.5.8", "react-perfect-scrollbar": "^1.5.8",

View File

@ -2,7 +2,9 @@ import PropTypes from 'prop-types';
import * as Yup from 'yup'; import * as Yup from 'yup';
import { Formik } from 'formik'; import { Formik } from 'formik';
import { useTheme } from '@mui/material/styles'; import { useTheme } from '@mui/material/styles';
import { useState, useEffect } from 'react'; import { useState, useEffect, useCallback } from 'react';
import { format } from 'date-fns';
import { import {
Dialog, Dialog,
DialogTitle, DialogTitle,
@ -17,7 +19,11 @@ import {
Select, Select,
MenuItem, MenuItem,
IconButton, IconButton,
FormHelperText FormHelperText,
TextField,
Typography,
Switch,
FormControlLabel
} from '@mui/material'; } from '@mui/material';
import Visibility from '@mui/icons-material/Visibility'; import Visibility from '@mui/icons-material/Visibility';
@ -44,6 +50,17 @@ const validationSchema = Yup.object().shape({
is: false, is: false,
then: Yup.number().min(0, '额度 不能小于 0'), then: Yup.number().min(0, '额度 不能小于 0'),
otherwise: Yup.number() otherwise: Yup.number()
}),
expiration_date: Yup.mixed().when('group', {
is: (group) => group !== 'default',
then: Yup.mixed().test(
'expiration_date-required',
'到期时间 不能为空',
function (value) {
const { expiration_date } = this.parent;
return expiration_date === -1 || !!expiration_date;
}
),
}) })
}); });
@ -53,7 +70,8 @@ const originInputs = {
display_name: '', display_name: '',
password: '', password: '',
group: 'default', group: 'default',
quota: 0 quota: 0,
expiration_date: null
}; };
const EditModal = ({ open, userId, onCancel, onOk }) => { const EditModal = ({ open, userId, onCancel, onOk }) => {
@ -65,6 +83,12 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
const submit = async (values, { setErrors, setStatus, setSubmitting }) => { const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
setSubmitting(true); setSubmitting(true);
// 将到期时间转换为 Unix 时间戳
if (values.expiration_date && values.expiration_date !== -1) {
const date = new Date(values.expiration_date);
values.expiration_date = Math.floor(date.getTime() / 1000); // 转换为秒级的 Unix 时间戳
}
let res; let res;
if (values.is_edit) { if (values.is_edit) {
res = await API.put(`/api/user/`, { ...values, id: parseInt(userId) }); res = await API.put(`/api/user/`, { ...values, id: parseInt(userId) });
@ -95,16 +119,23 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
event.preventDefault(); event.preventDefault();
}; };
const loadUser = async () => { const loadUser = useCallback(async () => {
let res = await API.get(`/api/user/${userId}`); let res = await API.get(`/api/user/${userId}`);
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
data.is_edit = true; data.is_edit = true;
// 将 Unix 时间戳转换为日期字符串
if (data.expiration_date && data.expiration_date !== -1) {
const date = new Date(data.expiration_date * 1000); // 转换为毫秒级的时间戳
data.expiration_date = format(date, 'yyyy-MM-dd\'T\'HH:mm:ss'); // 格式化为 datetime-local 格式
}
setInputs(data); setInputs(data);
} else { } else {
showError(message); showError(message);
} }
}; }, [userId]);
const fetchGroups = async () => { const fetchGroups = async () => {
try { try {
@ -122,7 +153,7 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
} else { } else {
setInputs(originInputs); setInputs(originInputs);
} }
}, [userId]); }, [userId, loadUser]);
return ( return (
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}> <Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
@ -132,7 +163,7 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
<Divider /> <Divider />
<DialogContent> <DialogContent>
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}> <Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => ( {({ errors, handleBlur, handleChange, handleSubmit, setFieldValue, touched, values, isSubmitting }) => (
<form noValidate onSubmit={handleSubmit}> <form noValidate onSubmit={handleSubmit}>
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.otherInput }}> <FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.otherInput }}>
<InputLabel htmlFor="channel-username-label">用户名</InputLabel> <InputLabel htmlFor="channel-username-label">用户名</InputLabel>
@ -239,7 +270,12 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
value={values.group} value={values.group}
name="group" name="group"
onBlur={handleBlur} onBlur={handleBlur}
onChange={handleChange} onChange={(e) => {
handleChange(e);
if (e.target.value === 'default') {
setFieldValue('expiration_date', null);
}
}}
MenuProps={{ MenuProps={{
PaperProps: { PaperProps: {
style: { style: {
@ -264,6 +300,45 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
</FormControl> </FormControl>
</> </>
)} )}
{values.group !== 'default' && (
<FormControl fullWidth error={Boolean(touched.expiration_date && errors.expiration_date)} sx={{ ...theme.typography.otherInput }}>
<TextField
id="channel-expiration_date-label"
label="到期时间"
type="datetime-local" // 修改为 datetime-local
value={values.expiration_date}
name="expiration_date"
onBlur={handleBlur}
onChange={handleChange}
InputLabelProps={{
shrink: true
}}
inputProps={{
max: '9999-12-31T23:59:59' // 设置最大日期和时间
}}
aria-describedby="helper-text-channel-expiration_date-label"
disabled={values.expiration_date === -1}
/>
{touched.expiration_date && errors.expiration_date && (
<FormHelperText error id="helper-tex-channel-expiration_date-label">
{errors.expiration_date}
</FormHelperText>
)}
<FormControlLabel
control={
<Switch
checked={values.expiration_date === -1}
onChange={(e) => setFieldValue('expiration_date', e.target.checked ? -1 : '')}
name="permanent"
color="primary"
/>
}
label="永不过期"
/>
</FormControl>
)}
<DialogActions> <DialogActions>
<Button onClick={onCancel}>取消</Button> <Button onClick={onCancel}>取消</Button>
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary"> <Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">