u
This commit is contained in:
@@ -1,158 +1,140 @@
|
|||||||
import React, { useState, useEffect, createContext, useContext } from 'react';
|
import React, { useState, useEffect, createContext, useContext } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
useQuery,
|
useQuery,
|
||||||
useQueryClient,
|
useQueryClient,
|
||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import 'dayjs/locale/zh-cn';
|
import 'dayjs/locale/zh-cn';
|
||||||
import type {
|
import type {
|
||||||
User, AuthContextType
|
AuthContextType
|
||||||
} from '@/share/types';
|
} from '@/share/types';
|
||||||
import { authClient } from '@/client/api';
|
import { authClient } from '@/client/api';
|
||||||
|
import type { InferResponseType, InferRequestType } from 'hono/client';
|
||||||
|
|
||||||
|
type User = InferResponseType<typeof authClient.me.$get, 200>;
|
||||||
|
|
||||||
|
|
||||||
// 创建认证上下文
|
// 创建认证上下文
|
||||||
const AuthContext = createContext<AuthContextType | null>(null);
|
const AuthContext = createContext<AuthContextType<User> | null>(null);
|
||||||
|
|
||||||
// 认证提供器组件
|
// 认证提供器组件
|
||||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// 声明handleLogout函数
|
// 声明handleLogout函数
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
try {
|
try {
|
||||||
// 如果已登录,调用登出API
|
// 如果已登录,调用登出API
|
||||||
if (token) {
|
if (token) {
|
||||||
await authClient.logout.$post();
|
await authClient.logout.$post();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('登出请求失败:', error);
|
console.error('登出请求失败:', error);
|
||||||
} finally {
|
} finally {
|
||||||
// 清除本地状态
|
// 清除本地状态
|
||||||
setToken(null);
|
setToken(null);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
localStorage.removeItem('token');
|
localStorage.removeItem('token');
|
||||||
// 清除Authorization头
|
// 清除Authorization头
|
||||||
delete axios.defaults.headers.common['Authorization'];
|
delete axios.defaults.headers.common['Authorization'];
|
||||||
console.log('登出时已删除全局Authorization头');
|
console.log('登出时已删除全局Authorization头');
|
||||||
// 清除所有查询缓存
|
// 清除所有查询缓存
|
||||||
queryClient.clear();
|
queryClient.clear();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用useQuery检查登录状态
|
||||||
|
const { isLoading } = useQuery({
|
||||||
|
queryKey: ['auth', 'status', token],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!token) {
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setUser(null);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 设置全局默认请求头
|
||||||
|
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
||||||
|
// 使用API验证当前用户
|
||||||
|
const res = await authClient.me.$get();
|
||||||
|
if (res.status !== 200) {
|
||||||
|
const result = await res.json();
|
||||||
|
throw new Error(result.message)
|
||||||
}
|
}
|
||||||
};
|
const currentUser = await res.json();
|
||||||
|
setUser(currentUser);
|
||||||
|
setIsAuthenticated(true);
|
||||||
|
return { isValid: true, user: currentUser };
|
||||||
|
} catch (error) {
|
||||||
|
return { isValid: false };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: !!token,
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
retry: false
|
||||||
|
});
|
||||||
|
|
||||||
// 使用useQuery检查登录状态
|
const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise<void> => {
|
||||||
const { isLoading } = useQuery({
|
try {
|
||||||
queryKey: ['auth', 'status', token],
|
// 使用AuthAPI登录
|
||||||
queryFn: async () => {
|
const response = await authClient.login.$post({
|
||||||
if (!token) {
|
json: {
|
||||||
setIsAuthenticated(false);
|
username,
|
||||||
setUser(null);
|
password
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 设置全局默认请求头
|
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
|
|
||||||
// 使用API验证当前用户
|
|
||||||
const res = await authClient.me.$get();
|
|
||||||
if(res.status !== 200){
|
|
||||||
const result = await res.json();
|
|
||||||
throw new Error(result.message)
|
|
||||||
}
|
|
||||||
const currentUser = await res.json();
|
|
||||||
setUser(currentUser);
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
return { isValid: true, user: currentUser };
|
|
||||||
} catch (error) {
|
|
||||||
// 如果API调用失败,自动登出
|
|
||||||
// handleLogout();
|
|
||||||
return { isValid: false };
|
|
||||||
}
|
|
||||||
},
|
|
||||||
enabled: !!token,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
retry: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置请求拦截器
|
|
||||||
useEffect(() => {
|
|
||||||
// 设置响应拦截器处理401错误
|
|
||||||
const responseInterceptor = axios.interceptors.response.use(
|
|
||||||
(response) => response,
|
|
||||||
(error) => {
|
|
||||||
if (error.response?.status === 401) {
|
|
||||||
console.log('检测到401错误,执行登出操作');
|
|
||||||
handleLogout();
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// 清理拦截器
|
|
||||||
return () => {
|
|
||||||
axios.interceptors.response.eject(responseInterceptor);
|
|
||||||
};
|
|
||||||
}, [token]);
|
|
||||||
|
|
||||||
const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise<void> => {
|
|
||||||
try {
|
|
||||||
// 使用AuthAPI登录
|
|
||||||
const response = await authClient.login.$post({
|
|
||||||
json: {
|
|
||||||
username,
|
|
||||||
password
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (response.status !== 200) {
|
|
||||||
const result = await response.json()
|
|
||||||
throw new Error(result.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
// 保存token和用户信息
|
|
||||||
const { token: newToken, user: newUser } = result;
|
|
||||||
|
|
||||||
// 设置全局默认请求头
|
|
||||||
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
|
|
||||||
|
|
||||||
// 保存状态
|
|
||||||
setToken(newToken);
|
|
||||||
setUser(newUser);
|
|
||||||
setIsAuthenticated(true);
|
|
||||||
localStorage.setItem('token', newToken);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('登录失败:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
})
|
||||||
|
if (response.status !== 200) {
|
||||||
|
const result = await response.json()
|
||||||
|
throw new Error(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
const result = await response.json()
|
||||||
<AuthContext.Provider
|
|
||||||
value={{
|
// 保存token和用户信息
|
||||||
user,
|
const { token: newToken, user: newUser } = result;
|
||||||
token,
|
|
||||||
login: handleLogin,
|
// 设置全局默认请求头
|
||||||
logout: handleLogout,
|
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
|
||||||
isAuthenticated,
|
|
||||||
isLoading
|
// 保存状态
|
||||||
}}
|
setToken(newToken);
|
||||||
>
|
setUser(newUser);
|
||||||
{children}
|
setIsAuthenticated(true);
|
||||||
</AuthContext.Provider>
|
localStorage.setItem('token', newToken);
|
||||||
);
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('登录失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
user,
|
||||||
|
token,
|
||||||
|
login: handleLogin,
|
||||||
|
logout: handleLogout,
|
||||||
|
isAuthenticated,
|
||||||
|
isLoading
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用上下文的钩子
|
// 使用上下文的钩子
|
||||||
export const useAuth = () => {
|
export const useAuth = () => {
|
||||||
const context = useContext(AuthContext);
|
const context = useContext(AuthContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useAuth必须在AuthProvider内部使用');
|
throw new Error('useAuth必须在AuthProvider内部使用');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany, JoinTable, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
|
||||||
import { Role, RoleSchema } from './role.entity';
|
import { Role, RoleSchema } from './role.entity';
|
||||||
import { MessageEntity } from './message.entity';
|
import { z } from '@hono/zod-openapi';
|
||||||
import { z } from 'zod';
|
import { DeleteStatus, DisabledStatus } from '@/share/types';
|
||||||
|
|
||||||
@Entity({ name: 'users' })
|
@Entity({ name: 'users' })
|
||||||
export class UserEntity {
|
export class UserEntity {
|
||||||
@@ -26,19 +26,19 @@ export class UserEntity {
|
|||||||
@Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '真实姓名' })
|
@Column({ name: 'name', type: 'varchar', length: 255, nullable: true, comment: '真实姓名' })
|
||||||
name!: string | null;
|
name!: string | null;
|
||||||
|
|
||||||
@Column({ name: 'is_disabled', type: 'int', default: 0, comment: '是否禁用(0:启用,1:禁用)' })
|
@Column({ name: 'avatar', type: 'varchar', length: 255, nullable: true, comment: '头像' })
|
||||||
isDisabled!: number;
|
avatar!: string | null;
|
||||||
|
|
||||||
@Column({ name: 'is_deleted', type: 'int', default: 0, comment: '是否删除(0:未删除,1:已删除)' })
|
@Column({ name: 'is_disabled', type: 'int', default: DisabledStatus.ENABLED, comment: '是否禁用(0:启用,1:禁用)' })
|
||||||
isDeleted!: number;
|
isDisabled!: DisabledStatus;
|
||||||
|
|
||||||
|
@Column({ name: 'is_deleted', type: 'int', default: DeleteStatus.NOT_DELETED, comment: '是否删除(0:未删除,1:已删除)' })
|
||||||
|
isDeleted!: DeleteStatus;
|
||||||
|
|
||||||
@ManyToMany(() => Role)
|
@ManyToMany(() => Role)
|
||||||
@JoinTable()
|
@JoinTable()
|
||||||
roles!: Role[];
|
roles!: Role[];
|
||||||
|
|
||||||
@OneToMany(() => MessageEntity, (message) => message.sender)
|
|
||||||
senderMessages!: MessageEntity[];
|
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
@@ -76,12 +76,16 @@ export const UserSchema = z.object({
|
|||||||
example: '张三',
|
example: '张三',
|
||||||
description: '真实姓名'
|
description: '真实姓名'
|
||||||
}),
|
}),
|
||||||
isDisabled: z.number().int().min(0).max(1).default(0).openapi({
|
avatar: z.string().max(255).nullable().openapi({
|
||||||
example: 0,
|
example: 'https://example.com/avatar.jpg',
|
||||||
|
description: '用户头像'
|
||||||
|
}),
|
||||||
|
isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({
|
||||||
|
example: DisabledStatus.ENABLED,
|
||||||
description: '是否禁用(0:启用,1:禁用)'
|
description: '是否禁用(0:启用,1:禁用)'
|
||||||
}),
|
}),
|
||||||
isDeleted: z.number().int().min(0).max(1).default(0).openapi({
|
isDeleted: z.number().int().min(0).max(1).default(DeleteStatus.NOT_DELETED).openapi({
|
||||||
example: 0,
|
example: DeleteStatus.NOT_DELETED,
|
||||||
description: '是否删除(0:未删除,1:已删除)'
|
description: '是否删除(0:未删除,1:已删除)'
|
||||||
}),
|
}),
|
||||||
roles: z.array(RoleSchema).optional().openapi({
|
roles: z.array(RoleSchema).optional().openapi({
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
export interface DeviceStatus {
|
|
||||||
signalStrength: number;
|
|
||||||
carrier: string;
|
|
||||||
mode: '短信' | '电话' | '语音' | '余额查询';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SmsItem {
|
|
||||||
id: string;
|
|
||||||
phone: string;
|
|
||||||
content: string;
|
|
||||||
taskId: string;
|
|
||||||
status: 'pending' | 'success' | 'failed';
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SmsResponse {
|
|
||||||
data: {
|
|
||||||
list: SmsItem[];
|
|
||||||
status: DeviceStatus;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 短信接口配置
|
|
||||||
export interface SmsConfig {
|
|
||||||
apiUrl: string;
|
|
||||||
username: string;
|
|
||||||
encryptedPassword: string; // 加密存储的密码
|
|
||||||
timeout?: number; // 超时时间(毫秒)
|
|
||||||
maxRetries?: number; // 最大重试次数
|
|
||||||
enableMock?: boolean; // 是否启用模拟模式
|
|
||||||
}
|
|
||||||
|
|
||||||
// API请求/响应类型
|
|
||||||
export interface SmsApiRequest {
|
|
||||||
phone: string;
|
|
||||||
content: string;
|
|
||||||
signName?: string;
|
|
||||||
templateCode?: string;
|
|
||||||
username?: string; // 认证用户名
|
|
||||||
password?: string; // 认证密码
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SmsApiResponse {
|
|
||||||
success: boolean;
|
|
||||||
code: string;
|
|
||||||
message?: string;
|
|
||||||
data?: {
|
|
||||||
taskId: string;
|
|
||||||
serialNumbers?: string[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 错误类型
|
|
||||||
export interface SmsError {
|
|
||||||
code: string;
|
|
||||||
message: string;
|
|
||||||
timestamp: string;
|
|
||||||
stack?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 性能指标
|
|
||||||
export interface SmsMetrics {
|
|
||||||
requestCount: number;
|
|
||||||
successCount: number;
|
|
||||||
failureCount: number;
|
|
||||||
averageLatency: number;
|
|
||||||
lastRequestTime?: string;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import { randomBytes } from 'crypto';
|
|
||||||
import { writeFileSync, existsSync, readFileSync, appendFileSync } from 'fs';
|
|
||||||
|
|
||||||
export function generateJwtSecret(): string {
|
|
||||||
if (existsSync('.env')) {
|
|
||||||
const envContent = readFileSync('.env', 'utf-8');
|
|
||||||
if (envContent.includes('JWT_SECRET')) {
|
|
||||||
return 'JWT_SECRET已存在';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const secret = randomBytes(64).toString('hex');
|
|
||||||
const envLine = `JWT_SECRET=${secret}\n`;
|
|
||||||
|
|
||||||
if (existsSync('.env')) {
|
|
||||||
appendFileSync('.env', envLine);
|
|
||||||
} else {
|
|
||||||
writeFileSync('.env', envLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
return secret;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkRequiredEnvVars(): boolean {
|
|
||||||
const requiredVars = [
|
|
||||||
'DB_HOST',
|
|
||||||
'DB_PORT',
|
|
||||||
'DB_USERNAME',
|
|
||||||
'DB_PASSWORD',
|
|
||||||
'DB_DATABASE',
|
|
||||||
'JWT_SECRET'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!existsSync('.env')) return false;
|
|
||||||
|
|
||||||
const envContent = readFileSync('.env', 'utf-8');
|
|
||||||
return requiredVars.every(varName => envContent.includes(varName));
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import Dysmsapi20170525, * as $Dysmsapi20170525 from '@alicloud/dysmsapi20170525';
|
|
||||||
import OpenApi, * as $OpenApi from '@alicloud/openapi-client';
|
|
||||||
import Util, * as $Util from '@alicloud/tea-util';
|
|
||||||
import process from 'node:process'
|
|
||||||
|
|
||||||
type SMSError = {
|
|
||||||
message?: string;
|
|
||||||
data?: {
|
|
||||||
Recommend?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export class SMS {
|
|
||||||
/**
|
|
||||||
* 创建阿里云短信客户端
|
|
||||||
* @throws {Error} 当阿里云配置缺失时抛出错误
|
|
||||||
*/
|
|
||||||
static createClient(): Dysmsapi20170525 {
|
|
||||||
const accessKeyId = process.env.ALICLOUD_ACCESS_KEY;
|
|
||||||
const accessKeySecret = process.env.ALICLOUD_SECRET_KEY;
|
|
||||||
|
|
||||||
if (!accessKeyId || !accessKeySecret) {
|
|
||||||
throw new Error('阿里云配置缺失');
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = new $OpenApi.Config({
|
|
||||||
accessKeyId,
|
|
||||||
accessKeySecret,
|
|
||||||
endpoint: 'dysmsapi.aliyuncs.com'
|
|
||||||
});
|
|
||||||
|
|
||||||
return new (Dysmsapi20170525 as any).default(config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送验证码短信
|
|
||||||
* @param phoneNumber 接收手机号
|
|
||||||
* @param code 验证码
|
|
||||||
* @param templateCode 短信模板代码(可选)
|
|
||||||
* @param signName 短信签名(可选)
|
|
||||||
* @returns Promise<boolean> 发送是否成功
|
|
||||||
*/
|
|
||||||
static async sendVerificationSMS(
|
|
||||||
phoneNumber: string,
|
|
||||||
code: string,
|
|
||||||
templateCode?: string,
|
|
||||||
signName?: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
const client = this.createClient();
|
|
||||||
|
|
||||||
const sendSmsRequest = new $Dysmsapi20170525.SendSmsRequest({
|
|
||||||
signName: signName || process.env.SMS_DEFAULT_SIGN_NAME || '多八多',
|
|
||||||
templateCode: templateCode || process.env.SMS_DEFAULT_TEMPLATE_CODE || 'SMS_164760103',
|
|
||||||
phoneNumbers: phoneNumber,
|
|
||||||
templateParam: JSON.stringify({ code })
|
|
||||||
});
|
|
||||||
|
|
||||||
const runtime = new $Util.RuntimeOptions({});
|
|
||||||
await client.sendSmsWithOptions(sendSmsRequest, runtime);
|
|
||||||
return true;
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const err = error as SMSError;
|
|
||||||
console.error('短信发送失败:', err.message);
|
|
||||||
|
|
||||||
if (err.data?.Recommend) {
|
|
||||||
console.error('建议:', err.data.Recommend);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,3 +13,34 @@ export interface AuthContextType<T> {
|
|||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 启用/禁用状态枚举
|
||||||
|
export enum EnableStatus {
|
||||||
|
DISABLED = 0, // 禁用
|
||||||
|
ENABLED = 1 // 启用
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启用/禁用状态中文映射
|
||||||
|
export const EnableStatusNameMap: Record<EnableStatus, string> = {
|
||||||
|
[EnableStatus.DISABLED]: '禁用',
|
||||||
|
[EnableStatus.ENABLED]: '启用'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除状态枚举
|
||||||
|
export enum DeleteStatus {
|
||||||
|
NOT_DELETED = 0, // 未删除
|
||||||
|
DELETED = 1 // 已删除
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除状态中文映射
|
||||||
|
export const DeleteStatusNameMap: Record<DeleteStatus, string> = {
|
||||||
|
[DeleteStatus.NOT_DELETED]: '未删除',
|
||||||
|
[DeleteStatus.DELETED]: '已删除'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 启用/禁用状态枚举
|
||||||
|
export enum DisabledStatus {
|
||||||
|
DISABLED = 1, // 禁用
|
||||||
|
ENABLED = 0 // 启用
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user