返回对象(common)

package top.yxqz.springboot.common;

import lombok.Data;

@Data
public class Result<T> {
    private String code;
    private String msg;
    private T data;
    
    public Result() {
    }

    public Result(T data) {
        this.data = data;
    }
    
    public Result(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
    
    public Result(String code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public static <T> Result<T> success() {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg());
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMsg(), data);
    }

    public static <T> Result<T> success(String msg, T data) {
        return new Result<>(ResultCode.SUCCESS.getCode(), msg, data);
    }

    public static <T> Result<T> error() {
        return new Result<>(ResultCode.ERROR.getCode(), ResultCode.ERROR.getMsg());
    }

    public static <T> Result<T> error(String msg) {
        return new Result<>(ResultCode.ERROR.getCode(), msg);
    }

    public static <T> Result<T> error(String code, String msg) {
        return new Result<>(code, msg);
    }

    public static <T> Result<T> error(String code, String msg, T data) {
        return new Result<>(code, msg, data);
    }
    
    /**
     * 判断操作是否成功
     */
    public boolean isSuccess() {
        return ResultCode.SUCCESS.getCode().equals(this.code);
    }
    
    /**
     * 获取错误消息
     */
    public String getMessage() {
        return this.msg;
    }
}

返回状态密码(common)

package top.yxqz.springboot.common;

public enum ResultCode {
    SUCCESS("200", "操作成功"),
    ERROR("-1", "操作失败"),
    VALIDATE_FAILED("404", "参数检验失败"),
    UNAUTHORIZED("401", "暂未登录或token已经过期"),
    FORBIDDEN("403", "没有相关权限"),
    SYSTEM_ERROR("500", "系统错误"),
    
    // 参数相关错误
    PARAM_ERROR("400", "参数错误"),
    PARAM_MISSING("4001", "缺少必要参数"),
    PARAM_INVALID("4002", "参数格式不正确"),
    
    // 文件操作相关错误
    FILE_NOT_FOUND("5001", "文件不存在"),
    FILE_UPLOAD_FAILED("5002", "文件上传失败"),
    FILE_DELETE_FAILED("5003", "文件删除失败"),
    FILE_SIZE_EXCEEDED("5004", "文件大小超过限制"),
    FILE_TYPE_NOT_SUPPORTED("5005", "不支持的文件类型"),
    FILE_NAME_INVALID("5006", "文件名不合法"),
    FILE_CONTENT_INVALID("5007", "文件内容不合法"),
    FILE_SAVE_FAILED("5008", "文件保存失败"),
    
    // 业务相关错误
    BUSINESS_ERROR("6000", "业务处理失败"),
    ACCOUNT_SAME("6001", "用户名已存在"),
    USER_NOT_EXIST("6002", "用户不存在");

    private String code;
    private String msg;

    ResultCode(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public String code() {
        return code;
    }

    public String getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }
} 

业务异常类(exception)

package top.yxqz.springboot.exception;

import lombok.Getter;

/**
 * 业务异常类
 * @author 余小小
 */
@Getter
public class BusinessException extends RuntimeException {
    
    private final String code;

    public BusinessException(String message) {
        super(message);
        this.code = "BUSINESS_ERROR";
    }

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(String message, Throwable cause) {
        super(message, cause);
        this.code = "BUSINESS_ERROR";
    }

    public BusinessException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
}

全局异常(exception)

package top.yxqz.springboot.exception;

import jakarta.validation.ConstraintViolationException;
import top.yxqz.springboot.common.Result;
import top.yxqz.springboot.common.ResultCode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    /**
     * 处理业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }

    /**
     * 处理服务异常
     */
    @ExceptionHandler(ServiceException.class)
    public Result<?> handleServiceException(ServiceException e) {
        log.error("服务异常: {}", e.getMessage(), e);
        return Result.error(e.getCode(), e.getMessage());
    }

    /**
     * 参数验证异常
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        log.error(e.getMessage(), e);
        String message = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(ResultCode.VALIDATE_FAILED.getCode(), message);
    }

    /**
     * 约束违反异常
     */
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolationException(ConstraintViolationException e) {
        log.error(e.getMessage(), e);
        return Result.error(ResultCode.VALIDATE_FAILED.getCode(), e.getMessage());
    }

    /**
     * 处理其他异常
     */
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error(e.getMessage(), e);
        return Result.error(ResultCode.SYSTEM_ERROR.getCode(), "系统错误");
    }
} 

服务异常(exception)

package top.yxqz.springboot.exception;

import lombok.Getter;

/**
 * @author 余小小
 */
@Getter
public class ServiceException extends RuntimeException {
    private final String code;
    private final String msg;

    public ServiceException(String code, String msg) {
        super(msg);
        this.code = code;
        this.msg = msg;
    }

    public ServiceException(String msg) {
        super(msg);
        this.code = "-1";
        this.msg = msg;
    }
} 

日期工具类(util)

package top.yxqz.springboot.util;

import java.time.LocalDate;

public class DateUtils {
    /**
     * 计算上个月的第一天。
     * @return 上个月第一天的LocalDate对象。
     */
    public static LocalDate getLastMonthFirstDay() {
        LocalDate today = LocalDate.now(); // 获取当前日期
        int currentMonth = today.getMonthValue(); // 获取当前月份
        int currentYear = today.getYear(); // 获取当前年份

        // 如果当前月份是1月,则上个月是去年的12月
        if (currentMonth == 1) {
            return LocalDate.of(currentYear - 1, 12, 1);
        } else {
            // 否则,上个月的第一天就是当前年加上上个月的月份减1,日期为1
            return LocalDate.of(currentYear, currentMonth - 1, 1);
        }
    }
}

文件操作工具类(util)(后期引入)

package top.yxqz.springboot.util;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import top.yxqz.springboot.enums.FileBusinessTypeEnum;
import top.yxqz.springboot.enums.FileTypeEnum;
import top.yxqz.springboot.exception.BusinessException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

/**
 * 文件操作工具类
 * 专注于文件的基础操作:保存、删除、检查等
 * 文件类型验证统一使用FileTypeEnum
 * 
 * @author 余小小
 */

public class FileUtil {
    private final static Logger log = LoggerFactory.getLogger(FileUtil.class);
    public final static String FILE_BASE_PATH = System.getProperty("user.dir") + "/files/";
    private static final String ROOT_PATH = "/files/";
    
    /**
     * 将访问路径转换为相对物理路径
     * @param filename 访问路径,可能包含/files/前缀
     * @return 相对物理路径,不包含前缀
     */
    public static String convertToRelativePath(String filename) {
        if (StrUtil.isBlank(filename)) {
            return filename;
        }
        
        // 如果包含ROOT_PATH前缀,去掉它,避免路径重复
        if (filename.startsWith(ROOT_PATH)) {
            return filename.substring(ROOT_PATH.length());
        }
        // 如果路径有前导斜杠,移除它
        else if (filename.startsWith("/")) {
            return filename.substring(1);
        }
        
        return filename;
    }

    /**
     * 最大文件大小(500MB)
     */
    private static final long maxFileSize = 524288000L; // 500MB (500 * 1024 * 1024)
    
    /**
     * 获取最大文件大小
     * @return 最大文件大小(字节)
     */
    public static long getMaxFileSize() {
        return maxFileSize;
    }


    /**
     * 安全的文件保存方法
     * 统一使用FileTypeEnum进行文件类型验证
     * 
     * @param file 上传的文件
     * @param folderName 子目录名称,位于relativeDir(可选)
     * @param relativeDir 基础目录(应与FileTypeEnum的code对应)
     * @return 文件访问路径
     * @throws BusinessException 文件保存失败时抛出
     */
    public static String saveFile(MultipartFile file,  String relativeDir,String folderName) {
        try {
            log.info("开始保存文件,原始文件名:{},目标目录:{}", file.getOriginalFilename(), relativeDir);

          validateBasicFile(file);
          String originalFilename = file.getOriginalFilename();
            


            // 获取文件扩展名
            String extension = getFileExtension(originalFilename);
            if (StrUtil.isBlank(extension)) {
                log.error("文件没有扩展名:{}", originalFilename);
                throw new BusinessException("文件没有扩展名");
            }
            
            // 生成唯一文件名
            long timestamp = System.currentTimeMillis();
            String uniqueFilename = timestamp + extension.toLowerCase();

            // 构造安全的保存路径
            Path fileDirectory = buildSafeFilePath(relativeDir, folderName);
            if (fileDirectory == null) {
                throw new BusinessException("构建文件保存路径失败");
            }

            // 创建目录
            if (!Files.exists(fileDirectory)) {
                Files.createDirectories(fileDirectory);
                log.info("创建目录:{}", fileDirectory);
            }
            
            // 保存文件
            Path uploadFilePath = fileDirectory.resolve(uniqueFilename);
            File uploadFile = uploadFilePath.toFile();

            file.transferTo(uploadFile);
            log.info("文件保存成功:{}", uploadFile.getAbsolutePath());
            
            // 返回相对路径
            String relativePath = ROOT_PATH + relativeDir + "/" +
                (StrUtil.isNotBlank(folderName) ? folderName + "/" : "") + uniqueFilename;
            
            log.info("返回文件访问路径:{}", relativePath);
            return relativePath;
            
        } catch (IOException e) {
            log.error("文件保存异常,文件名:{},错误:{}", file.getOriginalFilename(), e.getMessage(), e);
            throw new BusinessException("文件保存失败: " + e.getMessage());
        } catch (BusinessException e) {
            // 业务异常直接抛出
            throw e;
        } catch (Exception e) {
            log.error("文件保存时发生未知异常,文件名:{},错误:{}", file.getOriginalFilename(), e.getMessage(), e);
            throw new BusinessException("文件保存失败: " + e.getMessage());
        }
    }

    /**
     * 保存图片的便捷方法
     */
    public static String saveImage(MultipartFile file, String folderName) {
        return saveFile(file, folderName, "img");
    }

    /**
     * 保存视频的便捷方法
     */
    public static String saveVideo(MultipartFile file, String folderName) {
        return saveFile(file, folderName, "video");
    }
    
    /**
     * 安全的文件删除方法
     */
    public static boolean deleteFile(String filename) {
        try {
            log.info("开始删除文件:{}", filename);

            validateName(filename);

            // 转换为相对物理路径
            filename = convertToRelativePath(filename);
            
            // 获取文件的绝对路径
            Path filePath = Paths.get(FILE_BASE_PATH, filename);
            
            // 验证文件路径是否在允许的目录内
            Path basePath = Paths.get(FILE_BASE_PATH);
            if (!filePath.toAbsolutePath().startsWith(basePath.toAbsolutePath())) {
                log.error("文件路径超出允许范围:{}", filePath);
                return false;
            }
            
            if (Files.exists(filePath)) {
                Files.delete(filePath);
                log.info("文件删除成功:{}", filePath);
                return true;
            } else {
                log.warn("文件不存在:{}", filePath);
                return false;
            }
            
        } catch (Exception e) {
            log.error("文件删除异常,文件名:{},错误:{}", filename, e.getMessage(), e);
            return false;
        }
    }

    /**
     * 写入文件内容
     */
    public static void writeToFile(String fileName, String content) throws IOException {
        log.info("开始写入文件:{}", fileName);



        if (StrUtil.isBlank(content)) {
            throw new IllegalArgumentException("内容不能为空");
        }

        //安全性检查
        validateName(fileName);

        // 创建文件对象
        File file = new File(fileName);
        log.info("文件绝对路径:{}", file.getAbsolutePath());

        // 使用 try-with-resources 确保 FileWriter 在使用完毕后自动关闭
        try (FileWriter fileWriter = new FileWriter(file)) {
            fileWriter.write(content);
            log.info("文件写入成功:{}", fileName);
        } catch (IOException e) {
            log.error("文件写入失败,文件名:{},错误:{}", fileName, e.getMessage(), e);
            throw e;
        }
    }
    
    /**
     * 检查文件是否存在
     */
    public static boolean fileExists(String filename) {
        if (StrUtil.isBlank(filename)) {
            return false;
        }
        
        try {
            // 转换为相对物理路径
            filename = convertToRelativePath(filename);
            
            Path filePath = Paths.get(FILE_BASE_PATH, filename);
            return Files.exists(filePath);
        } catch (Exception e) {
            log.error("检查文件存在性异常,文件名:{},错误:{}", filename, e.getMessage());
            return false;
        }
    }
    
    /**
     * 获取文件大小
     */
    public static long getFileSize(String filename) {
        if (StrUtil.isBlank(filename)) {
            return -1;
        }
        
        try {
            // 转换为相对物理路径
            filename = convertToRelativePath(filename);
            
            Path filePath = Paths.get(FILE_BASE_PATH, filename);
            if (Files.exists(filePath)) {
                return Files.size(filePath);
            }
            return -1;
        } catch (Exception e) {
            log.error("获取文件大小异常,文件名:{},错误:{}", filename, e.getMessage());
            return -1;
        }
    }
    
    // ==================== 私有辅助方法 ====================
    
    /**
     * 获取文件扩展名
     */
    public static String getFileExtension(String filename) {
        if (StrUtil.isBlank(filename)) {
            return "";
        }
        
        int dotIndex = filename.lastIndexOf('.');
        if (dotIndex > 0 && dotIndex < filename.length() - 1) {
            return filename.substring(dotIndex);
        }
        return "";
    }



    /**
     * 构建安全的文件路径
     */
    private static Path buildSafeFilePath(String relativeDir, String folderName) {
        try {
            Path projectRootPath = Paths.get(FILE_BASE_PATH);

            String  filePath=relativeDir;
            // 如果folderName不为null,则在指定目录后面加入folderName
            if (StringUtils.isNotBlank(folderName)) {
                // 验证folderName的安全性
                validateName(folderName);
                filePath=relativeDir + File.separator + folderName;

            }



            return projectRootPath.resolve(filePath);
        } catch (Exception e) {
            log.error("构建文件路径失败,baseDir:{},folderName:{},错误:{}", relativeDir, folderName, e.getMessage());
            return null;
        }
    }



    public static void validateBasicFile(MultipartFile file) {
        //验证是否为空
        if (file.isEmpty()) {
            throw new BusinessException("上传文件不能为空");
        }
        //文件大小验证
        if (file.getSize() > maxFileSize) {
            throw new BusinessException(String.format(
                    "文件大小超出限制,当前: %d 字节,最大允许: %d 字节",
                    file.getSize(), maxFileSize
            ));
        }
        //文件名安全性检查
        String originalName = file.getOriginalFilename();
        validateName(originalName);

    }

    /**
     * 文件名安全性与非空性检查
     * @param name 文件(夹)名
     */
   public static void validateName(String name){
       //验证文件名是否为空
       if (!StringUtils.isNotBlank(name)) {
           throw new BusinessException("文件(夹)名不能为空");
       }

       // 检查危险字符
        String[] dangerousChars = {"..",  "\\", ":", "*", "?", "\"", "<", ">", "|"};
        for (String dangerousChar : dangerousChars) {
            if (name.contains(dangerousChar)) {
                throw new BusinessException("文件(夹)名包含非法字符: " + dangerousChar);
            }
        }
    }


    /**
     * 解析文件类型为目录名(小写)
     */
    public static String parseFileTypeToRelativeDir(String fileType) {
        if (StrUtil.isBlank(fileType)) {
            return FileTypeEnum.OTHER.getCode();
        }
        if(!(FileTypeEnum.isAllowType(fileType))) {
            return "commom";
        }
        return fileType.toLowerCase();
    }

    /**
     * 解析业务文件类型为目录名
     */
    public static String parseBussinessFileTypeToFolerName(String bussinessType) {
        if (StrUtil.isBlank(bussinessType)) {
            return FileTypeEnum.OTHER.getCode();
        }
        if(!FileBusinessTypeEnum.isAllowedFileBussinessType(bussinessType)) {
            return "commom";
        }
        return bussinessType.toLowerCase();
    }

    /**
     * 构建完整的文件路径(用于检查文件是否存在)
     * @param originalFilename 原始文件名
     * @param relativeDir 相对目录
     * @param folderName 子目录名称(可选)
     * @return 完整的文件路径(相对于FILE_BASE_PATH)
     */
    public static String buildFullFilePath(String originalFilename, String relativeDir, String folderName) {
        if (StrUtil.isBlank(originalFilename)) {
            return null;
        }
        
        StringBuilder pathBuilder = new StringBuilder();
        pathBuilder.append("files/").append(relativeDir).append("/");
        
        if (StrUtil.isNotBlank(folderName)) {
            pathBuilder.append(folderName).append("/");
        }
        
        pathBuilder.append(originalFilename);
        
        return pathBuilder.toString();
    }

    /**
     * 构建带时间戳的唯一文件路径
     * @param originalFilename 原始文件名
     * @param relativeDir 相对目录
     * @param folderName 子目录名称(可选)
     * @return 带时间戳的文件路径
     */
    public static String buildUniqueFilePath(String originalFilename, String relativeDir, String folderName) {
        if (StrUtil.isBlank(originalFilename)) {
            return null;
        }
        
        // 获取文件扩展名
        String extension = getFileExtension(originalFilename);
        if (StrUtil.isBlank(extension)) {
            return null;
        }
        
        // 生成唯一文件名
        long timestamp = System.currentTimeMillis();
        String uniqueFilename = timestamp + extension.toLowerCase();
        
        return buildFullFilePath(uniqueFilename, relativeDir, folderName);
    }


}

JWT工具类(util)(后期引入)

package top.yxqz.springboot.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;
import top.yxqz.springboot.dto.response.UserDetailResponseDTO;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * JWT工具类 - 用于JWT token的生成、验证和用户信息获取
 *
 * 主要功能:
 * 1. 生成JWT token(包含userId、username、roleType)
 * 2. 验证JWT token有效性和过期检查
 * 3. 从请求属性中获取当前用户信息(由JwtAuthenticationFilter设置)
 *
 * 使用说明:
 * - Token的解析和提取由JwtAuthenticationFilter负责
 * - Controller中使用getCurrentUser()和getCurrentUserId()获取当前用户信息
 * - 不要直接操作token,所有token相关逻辑在Filter中处理
 *
 * Token传输规范:
 * - 只支持标准的 Authorization: Bearer <token> 方式
 *
 * 安全特性:
 * - 使用HMAC256算法签名
 * - Token有效期7天
 * - 完善的异常处理和日志记录
 */
@Slf4j
public class JwtTokenUtils {

    /**
     * JWT密钥
     */
    private static final String SECRET = "drone_management_system_jwt_secret_key_2024";

    /**
     * Token过期时间(7天)
     */
    private static final long EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000L;

    /**
     * Token发行者
     */
    private static final String ISSUER = "drone-management-system";

    /**
     * 生成JWT token
     * @param userId 用户ID
     * @param username 用户名
     * @param roleType 角色代码
     * @return JWT token
     */
    public static String generateToken(Long userId, String username, String roleType) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(SECRET);
            Date expireDate = new Date(System.currentTimeMillis() + EXPIRE_TIME);

            return JWT.create()
                    .withClaim("userId", userId)
                    .withClaim("username", username)
                    .withClaim("roleType", roleType)
                    .withExpiresAt(expireDate)
                    .withIssuedAt(new Date())
                    .withIssuer(ISSUER)
                    .sign(algorithm);
        } catch (Exception e) {
            log.error("生成JWT token失败", e);
            throw new RuntimeException("生成JWT token失败", e);
        }
    }

    /**
     * 验证JWT token有效性
     * @param token JWT token
     * @return 解码后的JWT
     * @throws JWTVerificationException token验证失败
     */
    public static DecodedJWT verifyToken(String token) throws JWTVerificationException {
        Algorithm algorithm = Algorithm.HMAC256(SECRET);
        JWTVerifier verifier = JWT.require(algorithm)
                .withIssuer(ISSUER)
                .build();
        return verifier.verify(token);
    }

    /**
     * 检查token是否过期
     * @param token JWT token
     * @return 是否过期
     */
    public static boolean isTokenExpired(String token) {
        try {
            DecodedJWT jwt = verifyToken(token);
            return jwt.getExpiresAt().before(new Date());
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 获取当前请求的用户ID(从RequestContextHolder获取)
     * @return 当前用户ID,获取失败返回null
     */
    public static Long getCurrentUserId() {
        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                Object userId = request.getAttribute("currentUserId");
                if (userId instanceof Long) {
                    return (Long) userId;
                }
            }
        } catch (Exception e) {
            log.error("获取当前用户ID失败", e);
        }
        return null;
    }

    /**
     * 获取当前请求的用户信息(从请求属性中获取)
     * 注意:此方法依赖于JwtAuthenticationFilter设置的请求属性
     * @return 当前用户对象,获取失败返回null
     */
    public static UserDetailResponseDTO getCurrentUser() {
        try {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            if (attributes != null) {
                HttpServletRequest request = attributes.getRequest();
                return (UserDetailResponseDTO)request.getAttribute("currentUser");
            }
        } catch (Exception e) {
            log.error("获取当前用户对象失败", e);
        }
        return null;
    }

    /**
     * 获取当前用户ID的字符串形式
     * @return 当前用户ID字符串,获取失败返回null
     */
    public static String getCurrentUserIdAsString() {
        Long userId = getCurrentUserId();
        return userId != null ? String.valueOf(userId) : null;
    }

    /**
     * 获取当前用户类型/角色
     * @return 当前用户类型,获取失败返回null
     */
    public static String getCurrentUserType() {
        UserDetailResponseDTO currentUser = getCurrentUser();
        if(currentUser==null)return null;
        return currentUser.getUserType();

    }

    /**
     * 判断当前用户是否为管理员
     * @return 是否为管理员
     */
    public static boolean isAdmin() {


                return "ADMIN".equals(getCurrentUserType());


    }
}
最后修改:2025 年 12 月 29 日
如果觉得我的文章对你有用,请随意赞赏