feat: 引入到期时间概念,当非default的分组时将有到期时间概念
fix: 修复旧用户的vip被降级,尽量不对原先用户做变动
This commit is contained in:
parent
adba54acd3
commit
b86a3acd37
@ -95,6 +95,9 @@ func SyncOptions(frequency int) {
|
||||
for {
|
||||
time.Sleep(time.Duration(frequency) * time.Second)
|
||||
logger.SysLog("syncing options from database")
|
||||
if config.IsMasterNode {
|
||||
checkAndDowngradeUsers()
|
||||
}
|
||||
loadOptionsFromDatabase()
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,9 @@ import (
|
||||
"github.com/songquanpeng/one-api/common/logger"
|
||||
"github.com/songquanpeng/one-api/common/random"
|
||||
"gorm.io/gorm"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -47,6 +49,7 @@ type User struct {
|
||||
Group string `json:"group" gorm:"type:varchar(32);default:'default'"`
|
||||
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"`
|
||||
ExpirationDate int64 `json:"expiration_date" gorm:"column:expiration_date"` // Expiration date of the user's subscription or account.
|
||||
}
|
||||
|
||||
func GetMaxUserId() int {
|
||||
@ -210,6 +213,25 @@ func (user *User) ValidateAndFill() (err error) {
|
||||
if !okay || user.Status != UserStatusEnabled {
|
||||
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
|
||||
}
|
||||
|
||||
@ -435,3 +457,40 @@ func GetUsernameById(id int) (username string) {
|
||||
DB.Model(&User{}).Where("id = ?", id).Select("username").Find(&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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@
|
||||
"@tabler/icons-react": "^2.44.0",
|
||||
"apexcharts": "3.35.3",
|
||||
"axios": "^0.27.2",
|
||||
"date-fns": "^3.6.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"formik": "^2.2.9",
|
||||
"framer-motion": "^6.3.16",
|
||||
@ -27,6 +28,7 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-apexcharts": "1.4.0",
|
||||
"react-datepicker": "^7.3.0",
|
||||
"react-device-detect": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
|
@ -2,7 +2,9 @@ import PropTypes from 'prop-types';
|
||||
import * as Yup from 'yup';
|
||||
import { Formik } from 'formik';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
@ -17,7 +19,11 @@ import {
|
||||
Select,
|
||||
MenuItem,
|
||||
IconButton,
|
||||
FormHelperText
|
||||
FormHelperText,
|
||||
TextField,
|
||||
Typography,
|
||||
Switch,
|
||||
FormControlLabel
|
||||
} from '@mui/material';
|
||||
|
||||
import Visibility from '@mui/icons-material/Visibility';
|
||||
@ -44,6 +50,17 @@ const validationSchema = Yup.object().shape({
|
||||
is: false,
|
||||
then: Yup.number().min(0, '额度 不能小于 0'),
|
||||
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: '',
|
||||
password: '',
|
||||
group: 'default',
|
||||
quota: 0
|
||||
quota: 0,
|
||||
expiration_date: null
|
||||
};
|
||||
|
||||
const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
@ -65,6 +83,12 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
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;
|
||||
if (values.is_edit) {
|
||||
res = await API.put(`/api/user/`, { ...values, id: parseInt(userId) });
|
||||
@ -95,16 +119,23 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const loadUser = async () => {
|
||||
const loadUser = useCallback(async () => {
|
||||
let res = await API.get(`/api/user/${userId}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
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);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
}, [userId]);
|
||||
|
||||
const fetchGroups = async () => {
|
||||
try {
|
||||
@ -122,159 +153,203 @@ const EditModal = ({ open, userId, onCancel, onOk }) => {
|
||||
} else {
|
||||
setInputs(originInputs);
|
||||
}
|
||||
}, [userId]);
|
||||
}, [userId, loadUser]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||
{userId ? '编辑用户' : '新建用户'}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-username-label">用户名</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-username-label"
|
||||
label="用户名"
|
||||
type="text"
|
||||
value={values.username}
|
||||
name="username"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'username' }}
|
||||
aria-describedby="helper-text-channel-username-label"
|
||||
/>
|
||||
{touched.username && errors.username && (
|
||||
<FormHelperText error id="helper-tex-channel-username-label">
|
||||
{errors.username}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.display_name && errors.display_name)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-display_name-label">显示名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-display_name-label"
|
||||
label="显示名称"
|
||||
type="text"
|
||||
value={values.display_name}
|
||||
name="display_name"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'display_name' }}
|
||||
aria-describedby="helper-text-channel-display_name-label"
|
||||
/>
|
||||
{touched.display_name && errors.display_name && (
|
||||
<FormHelperText error id="helper-tex-channel-display_name-label">
|
||||
{errors.display_name}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.password && errors.password)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-password-label">密码</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-password-label"
|
||||
label="密码"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={values.password}
|
||||
name="password"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'password' }}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
onMouseDown={handleMouseDownPassword}
|
||||
edge="end"
|
||||
size="large"
|
||||
>
|
||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
aria-describedby="helper-text-channel-password-label"
|
||||
/>
|
||||
{touched.password && errors.password && (
|
||||
<FormHelperText error id="helper-tex-channel-password-label">
|
||||
{errors.password}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{values.is_edit && (
|
||||
<>
|
||||
<FormControl fullWidth error={Boolean(touched.quota && errors.quota)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-quota-label">额度</InputLabel>
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||
{userId ? '编辑用户' : '新建用户'}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||
{({ errors, handleBlur, handleChange, handleSubmit, setFieldValue, touched, values, isSubmitting }) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl fullWidth error={Boolean(touched.username && errors.username)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-username-label">用户名</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-quota-label"
|
||||
label="额度"
|
||||
type="number"
|
||||
value={values.quota}
|
||||
name="quota"
|
||||
endAdornment={<InputAdornment position="end">{renderQuotaWithPrompt(values.quota)}</InputAdornment>}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-quota-label"
|
||||
disabled={values.unlimited_quota}
|
||||
id="channel-username-label"
|
||||
label="用户名"
|
||||
type="text"
|
||||
value={values.username}
|
||||
name="username"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'username' }}
|
||||
aria-describedby="helper-text-channel-username-label"
|
||||
/>
|
||||
|
||||
{touched.quota && errors.quota && (
|
||||
<FormHelperText error id="helper-tex-channel-quota-label">
|
||||
{errors.quota}
|
||||
</FormHelperText>
|
||||
{touched.username && errors.username && (
|
||||
<FormHelperText error id="helper-tex-channel-username-label">
|
||||
{errors.username}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.group && errors.group)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-group-label">分组</InputLabel>
|
||||
<Select
|
||||
id="channel-group-label"
|
||||
label="分组"
|
||||
value={values.group}
|
||||
name="group"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
<FormControl fullWidth error={Boolean(touched.display_name && errors.display_name)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-display_name-label">显示名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-display_name-label"
|
||||
label="显示名称"
|
||||
type="text"
|
||||
value={values.display_name}
|
||||
name="display_name"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'display_name' }}
|
||||
aria-describedby="helper-text-channel-display_name-label"
|
||||
/>
|
||||
{touched.display_name && errors.display_name && (
|
||||
<FormHelperText error id="helper-tex-channel-display_name-label">
|
||||
{errors.display_name}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.password && errors.password)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-password-label">密码</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-password-label"
|
||||
label="密码"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={values.password}
|
||||
name="password"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'password' }}
|
||||
endAdornment={
|
||||
<InputAdornment position="end">
|
||||
<IconButton
|
||||
aria-label="toggle password visibility"
|
||||
onClick={handleClickShowPassword}
|
||||
onMouseDown={handleMouseDownPassword}
|
||||
edge="end"
|
||||
size="large"
|
||||
>
|
||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}
|
||||
}}
|
||||
>
|
||||
{groupOptions.map((option) => {
|
||||
return (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{touched.group && errors.group && (
|
||||
<FormHelperText error id="helper-tex-channel-group-label">
|
||||
{errors.group}
|
||||
</FormHelperText>
|
||||
aria-describedby="helper-text-channel-password-label"
|
||||
/>
|
||||
{touched.password && errors.password && (
|
||||
<FormHelperText error id="helper-tex-channel-password-label">
|
||||
{errors.password}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
</>
|
||||
)}
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||
提交
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{values.is_edit && (
|
||||
<>
|
||||
<FormControl fullWidth error={Boolean(touched.quota && errors.quota)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-quota-label">额度</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-quota-label"
|
||||
label="额度"
|
||||
type="number"
|
||||
value={values.quota}
|
||||
name="quota"
|
||||
endAdornment={<InputAdornment position="end">{renderQuotaWithPrompt(values.quota)}</InputAdornment>}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-quota-label"
|
||||
disabled={values.unlimited_quota}
|
||||
/>
|
||||
|
||||
{touched.quota && errors.quota && (
|
||||
<FormHelperText error id="helper-tex-channel-quota-label">
|
||||
{errors.quota}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.group && errors.group)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-group-label">分组</InputLabel>
|
||||
<Select
|
||||
id="channel-group-label"
|
||||
label="分组"
|
||||
value={values.group}
|
||||
name="group"
|
||||
onBlur={handleBlur}
|
||||
onChange={(e) => {
|
||||
handleChange(e);
|
||||
if (e.target.value === 'default') {
|
||||
setFieldValue('expiration_date', null);
|
||||
}
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{groupOptions.map((option) => {
|
||||
return (
|
||||
<MenuItem key={option} value={option}>
|
||||
{option}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{touched.group && errors.group && (
|
||||
<FormHelperText error id="helper-tex-channel-group-label">
|
||||
{errors.group}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</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>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||
提交
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@ -285,4 +360,4 @@ EditModal.propTypes = {
|
||||
userId: PropTypes.number,
|
||||
onCancel: PropTypes.func,
|
||||
onOk: PropTypes.func
|
||||
};
|
||||
};
|
Loading…
Reference in New Issue
Block a user