SpringBoot + SpringDataJPA + MySQL 实现简单的登录、注册功能

目录

前言

一、创建SpringBoot项目

二、配置数据库

三、定义实体类(Entity)

四、定义Repository

五、定义数据传输对象类(DTO)

六、定义常量和工具类(可选)

定义常量

定义工具类

 七、定义服务类(Service)

八、定义控制器类(Controller)

九、定义全局异常处理器

十、使用JJWT解析/生成Token

十一、完成注册部分


前言

最近在做一个前端的练习项目时需要用到后端服务,于是在学习了一些知识后,尝试用 SpringBoot + SpringDataJPA + MySQL 实现了简单的登录、注册功能,以及使用JJWT库进行Token的生成和解析。这篇文章用来记录我学习探索过程中的心得,同时也作为一篇适用于初学者的简单易懂的上手教程。

一、创建SpringBoot项目

在IDEA中新建项目,选择Spring Initializr,勾选以下依赖:

  • Spring Boot DevTools:提高Spring Boot应用程序的开发效率的开发工具
  • Lombok:通过注解自动生成getter、setter、构造函数等样板代码,以减少工作量,提高代码的简洁性
  • Spring Web:通过注解和配置文件定义和处理HTTP请求和响应,实现URL路由、参数绑定、数据验证等功能
  • Spring Data JPA:简化数据库访问,提供了一组用于常见数据库操作的方法
  • MySQL Driver:MySQL数据库的驱动程序

二、配置数据库

在MySQL中新建一个数据库

在src/main/resources中找到application.properties,并在其中添加以下内容:

# 指定JDBC驱动程序的类名
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

# 指定MySQL数据库的URL、用户名、密码
spring.datasource.url=jdbc:mysql://localhost:3306/YOUR_DATABASE
spring.datasource.username=YOUR_USERNAME
spring.datasource.password=YOUR_PASSWORD

# 指定JPA在启动时根据实体类的定义自动更新表结构
spring.jpa.hibernate.ddl-auto=update
# 指定JPA的数据库方言为MySQL8
spring.jpa.database-platform=org.hibernate.dialect.MySQL8Dialect
# 指定JPA在控制台打印SQL查询语句
spring.jpa.show-sql=true

注意将YOUR_DATEBASE、YOUR_USERNAME、YOUR_PASSWORD替换为自己的数据库名称、账号、密码

三、定义实体类(Entity)

每个实体类对应数据库中的一个表,通过定义属性和相关的getter、setter方法来表示数据;实体类的属性对应表中的字段(列),实体类的对象对应表中的记录(行);程序在启动时会根据定义的实体类自动在数据库中创建对应的表

在主程序包内新建一个软件包entity,用于存放实体类

在entity下新建一个User类,用于表示账户信息

package com.wbbb.demo01.entity;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.math.BigInteger;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "user")
public class User {
    @Id
    @Column(name = "user_id")
    private BigInteger userId;

    private String username;

    private String password;

    private String email;

    @Column(name = "create_time")
    private Long createTime;
}

@Getter@Setter@NoArgsConstructor@AllArgsConstructor 为Lombok的注解,为类中的每个属性生成getter和setter方法,为类生成无参和全参构造函数(参考:Lombok 看这篇就够了

@Entity 表示这是一个实体类

@Table 用于指定表名(表名与类名相同时可以省略,但要注意有无下划线的区别)

@Id 用于指定表的主键

@Column 用于指定属性在表中对应的字段(字段名与属性名相同时可以省略)

四、定义Repository

Repository是用于访问数据库的接口,只需定义一个接口并继承JpaRepository,即可获得内置的数据库操作方法,并且可以根据方法名自动实现对应的数据库增删改查操作

常用的内置方法参考:Spring Data JPA 之 JpaRepository

自定义方法名格式如:findByUsernameAndPassword、existsByUsernameOrEmail等

在主程序包内新建一个软件包repository,用于存放Repository接口

在repository中新建一个UserRepository接口,用于User表的增删改查操作:

package com.wbbb.demo01.repository;

import com.wbbb.demo01.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.math.BigInteger;

@Repository
public interface UserRepository extends JpaRepository<User, BigInteger> {
    User findByUsernameAndPassword(String username, String password);
    boolean existsByUsername(String username);
}

@Repository表示这是一个Repository接口

定义UserRepository接口用于User表的增删改查等操作,继承JpaRepository<User, BigInteger>,其中User为表对应的实体类,BigInteger为主键的类型

findByUsernameAndPassword、existsByUsername为自定义的方法,会根据方法名的语义自动实现对应的方法

五、定义数据传输对象类(DTO)

DTO(Data Transfer Object)是一种用于传输数据的对象,它通常用于在不同层之间传递数据,通常用于将实体类的数据转换为前端或其他外部系统所需的格式

以登录接口为例,前端的请求数据格式如下:

{
    "username": "xxx",
    "password": "xxx"
}

前端希望收到的响应数据格式如下:

{
    "code": 200,
    "message": "登录成功",
    "data": {
        "token": "xxx",
        "userId": xxx
    }
}

首先,在主程序包中新建一个软件包dto,用于存放DTO类

在DTO中新建两个软件包requestresponse,分别用于存放请求DTO和响应DTO

在request包中新建一个LoginRequestDto类,对应登录接口的请求体

package com.wbbb.demo01.dto.request;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class LoginRequestDto {
    private String username;
    private String password;
}

在response包中新建一个ResponseDto类,对应通用的响应体

package com.wbbb.demo01.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
public class Response<T> {
    private Integer code;
    private String message;
    private T data;
}

在response包中新建一个data包,用于存放响应体数据部分的DTO

在data包中新建一个LoginResponseDataDto类,对应登录接口的响应体的数据部分

package com.wbbb.demo01.dto.response.data;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.math.BigInteger;

@Getter
@Setter
@AllArgsConstructor
public class LoginResponseDataDto {
    private String token;
    private BigInteger userId;
}

六、定义常量和工具类(可选)

定义常量

在ResponseDto类中,属性code表示状态码,message表示信息。我们可以将常用的状态码和它们对应的默认信息封装成一个枚举类(enum)

在主程序包中新建一个软件包constant,用于存放常量

在constant包中新建一个枚举ResponseCode,用于枚举常用的响应状态码和对应的默认信息

package com.wbbb.demo01.dto.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum ResponseCode {
    SUCCESS(200, "操作成功"),
    BAD_REQUEST(400, "请求错误"),
    NOT_FOUND(404, "资源不存在"),
    CONFLICT(409, "资源冲突"),
    SERVER_ERROR(500, "服务器错误");

    private final Integer code;
    private final String message;
}

同时,在ResponseDto中添加一些静态工厂方法,便于快速创建不同状态的响应体

package com.wbbb.demo01.dto.response;

import com.wbbb.demo01.dto.constant.ResponseCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
public class ResponseDto<T> {
    private Integer code;
    private String message;
    private T data;

    // 200,操作成功
    public static <T> ResponseDto<T> success(T data) {
        return new ResponseDto<T>(ResponseCode.SUCCESS.getCode(), ResponseCode.SUCCESS.getMessage(), data);
    }
    public static <T> ResponseDto<T> success(T data, String message) {
        return new ResponseDto<T>(ResponseCode.SUCCESS.getCode(), message, data);
    }

    // 400,请求错误
    public static <T> ResponseDto<T> badRequest(T data) {
        return new ResponseDto<T>(ResponseCode.BAD_REQUEST.getCode(), ResponseCode.BAD_REQUEST.getMessage(), data);
    }
    public static <T> ResponseDto<T> badRequest(T data, String message) {
        return new ResponseDto<T>(ResponseCode.BAD_REQUEST.getCode(), message, data);
    }

    // 404,资源不存在
    public static <T> ResponseDto<T> notFound(T data) {
        return new ResponseDto<T>(ResponseCode.NOT_FOUND.getCode(), ResponseCode.NOT_FOUND.getMessage(), data);
    }
    public static <T> ResponseDto<T> notFound(T data, String message) {
        return new ResponseDto<T>(ResponseCode.NOT_FOUND.getCode(), message, data);
    }

    // 409,资源冲突
    public static <T> ResponseDto<T> conflict(T data) {
        return new ResponseDto<T>(ResponseCode.CONFLICT.getCode(), ResponseCode.CONFLICT.getMessage(), data);
    }
    public static <T> ResponseDto<T> conflict(T data, String message) {
        return new ResponseDto<T>(ResponseCode.CONFLICT.getCode(), message, data);
    }

    // 500,服务器错误
    public static <T> ResponseDto<T> serverError(T data) {
        return new ResponseDto<T>(ResponseCode.SERVER_ERROR.getCode(), ResponseCode.SERVER_ERROR.getMessage(), data);
    }
    public static <T> ResponseDto<T> serverError(T data, String message) {
        return new ResponseDto<T>(ResponseCode.SERVER_ERROR.getCode(), message, data);
    }
}

定义工具类

在项目中,我们可以将一些通用且有一定复杂性的功能封装为工具类,例如加密/解密、生成/解析Token、生成随机Id等

在主程序包中新建一个软件包util,用于存放工具类

在util包中新建一个CryptoUtil类,用于加密/解密,其中SHA256方法用于对字符串进行SHA256加密

package com.wbbb.demo01.util;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * 加密、解密工具
 */
public class CryptoUtil {
    /**
     * 对字符串进行SHA256加密
     */
    public static String SHA256(String s) {
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(s.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : digest) {
                sb.append(String.format("%02x", b & 0xff));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return null;
        }
    }
}

 在util包中新建一个TokenUtil类,用于生成/解析Token(这里暂时先返回userId,在JJWT章节中再正式生成)

package com.wbbb.demo01.util;

import com.wbbb.demo01.entity.User;

import java.math.BigInteger;

/**
 * Token生成、校验、解析工具
 */
public class TokenUtil {
    /**
     * 根据账户信息生成token
     */
    public static String generateToken(User user) {
        return user.getUserId().toString();
    }

    /**
     * 验证并解析token
     */
    public static BigInteger parseToken(String token) {
        return new BigInteger(token);
    }
}

 七、定义服务类(Service)

服务类用于封装业务逻辑,提供应用程序的核心功能,通常需要协调多个Repository,处理它们返回的数据并转换为DTO

在主程序包中新建软件包service,用于存放服务类

在service包中新建UserService类,用于处理User相关的业务逻辑

package com.wbbb.demo01.service;

import com.wbbb.demo01.dto.response.data.LoginResponseDataDto;
import com.wbbb.demo01.entity.User;
import com.wbbb.demo01.repository.UserRepository;
import com.wbbb.demo01.util.CryptoUtil;
import com.wbbb.demo01.util.TokenUtil;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

@AllArgsConstructor
@Service
public class UserService {
    private final UserRepository userRepository;

    /**
     * 检测账户名称是否可用
     */
    public boolean checkUsernameAvailable(String username) {
        return !userRepository.existsByUsername(username);
    }
    
    /**
     * 登录
     * @return 登录成功返回{ token, userId },失败返回null
     */
    public LoginResponseDataDto login(String username, String password) {
        password = CryptoUtil.SHA256(password);
        User user = userRepository.findByUsernameAndPassword(username, password);
        if (user == null)
            return null;
        return new LoginResponseDataDto(TokenUtil.generateToken(user), user.getUserId());
    }
}

@Service 表示这是一个服务类

程序在启动时,会自动将UserRepository接口实现类的对象赋值给userRepository

八、定义控制器类(Controller)

控制器类负责处理传入的HTTP请求,通常从服务类获取数据,将数据转换为DTO对象,并将结果返回给客户端

在主程序包中新建软件包controller,用于存放控制器类

在controller包中新建UserController类,用于处理"/user/*"下的HTTP请求

package com.wbbb.demo01.controller;

import com.wbbb.demo01.dto.request.LoginRequestDto;
import com.wbbb.demo01.dto.response.ResponseDto;
import com.wbbb.demo01.dto.response.data.LoginResponseDataDto;
import com.wbbb.demo01.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

@AllArgsConstructor
@CrossOrigin("*")
@RestController
@RequestMapping("/user")
public class UserController {
    private final UserService userService;

    /**
     * 检查账户名称是否可用
     */
    @GetMapping("/available")
    public ResponseDto<Boolean> checkUsernameAvailable(@RequestParam("username") String username) {
        if (username.isEmpty())
            return ResponseDto.success(false);
        return ResponseDto.success(userService.checkUsernameAvailable(username));
    }

    /**
     * 登录
     */
    @PostMapping("/login")
    public ResponseDto<LoginResponseDataDto> login(@RequestBody LoginRequestDto loginRequestDto) {
        LoginResponseDataDto loginResponseDataDto = userService.login(loginRequestDto.getUsername(), loginRequestDto.getPassword());
        if (loginResponseDataDto == null)
            return ResponseDto.notFound(null, "请核对您的密码和帐户名称并重试。");
        return ResponseDto.success(loginResponseDataDto, "登录成功");
    }
}

@RequestController 表示这是一个控制器类,相当于@Controller + @ResponseBody,用于使HTTP请求返回JSON格式数据

@RequestMapping("/user") 用于定义控制器类的根URL映射,所有以"/user"开头的请求都将映射到这个控制器类中

@CrossOrigin("*") 用于表示允许跨域请求

userService在程序启动时自动注入

@GetMapping("/available") 用于将"/user/available"接口的GET请求映射到checkUsernameAvailable方法中进行处理

@RequestParam("username") 用于将GET请求的查询参数“username”绑定到 username

@PostMapping("/login") 用于将"/user/login"接口的POST请求映射到login方法中进行处理

@RequestBody 用于将POST请求的正文绑定到loginRequestDto

到这里为止,登录的基本功能就完成了,后面我们继续完成注册功能和其他细节

九、定义全局异常处理器

在程序允许过程中若产生异常,我们希望它能返回指定格式的响应体,而不是直接抛出异常,例如:

{
    "code": 400,
    "message": "请求参数错误",
    "data": null
}

所以需要定义一个全局异常处理器,用于处理程序运行时发生的错误

在controller包中新建GlobalExceptionHandler

package com.wbbb.demo01.controller;

import com.wbbb.demo01.dto.response.ResponseDto;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

/**
 * 全局异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler({HttpMessageNotReadableException.class, MethodArgumentTypeMismatchException.class, MissingServletRequestParameterException.class})
    public ResponseDto<?> handleBadRequest(Exception e) {
        System.out.println(e.getMessage());
        return ResponseDto.badRequest(null, "请求参数错误");
    }
}

@RestControllerAdvice 表示这是一个全局异常处理器

@ExceptionHandler 表示这个方法用于处理指定的异常,这里对应的是请求体无法读取、方法参数类型不匹配、缺少请求参数这三种异常

十、使用JJWT解析/生成Token

JJWT(Java JSON Web Token)是一个用于在Java上生成和解析JWT(JSON Web Token)的库

GitHub:https://github.com/jwtk/jjwt

汉化文档参考:JJWT使用详解

在pom.xml中添加以下依赖并加载变更

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

可以在 https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api 中查看最新的版本号

重新编写TokenUtil

package com.wbbb.demo01.util;

import com.wbbb.demo01.entity.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;

/**
 * Token生成、校验、解析工具
 */
public class TokenUtil {
    private static final String key = "YOUR_SECRET_KEY";

    /**
     * 根据账户信息生成Token
     */
    public static String generateToken(User user) {
        return Jwts.builder()
                .claim("userId", user.getUserId())
                .signWith(Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)))
                .compact();
    }

    /**
     * 验证并解析Token
     */
    public static BigInteger parseToken(String token) {
        try {
            return Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8)))
                    .build()
                    .parseClaimsJws(token)
                    .getBody()
                    .get("userId", BigInteger.class);
        } catch (JwtException e) {
            return null;
        }
    }
}

将YOUR_SECRET_KEY换成你自己的密钥,JJWT会根据密钥选择对应的加密算法生成/解析带签名的JWT

.claim(key, value) 用于在Token中添加要携带的信息

十一、完成注册部分

前端的请求数据格式如下:

{
    "email": "xxx@xx.com",
    "username": "xxx",
    "password": "xxx"
}

在dto.request包下新建JoinRequestDto

package com.wbbb.demo01.dto.request;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class JoinRequestDto {
    private String email;
    private String username;
    private String password;
}

在util包下新建SymbolGenerateUtil工具类,用于生成标识符(如Id、初始名称等)

package com.wbbb.demo01.util;

import java.math.BigInteger;
import java.util.Random;

/**
 * 标识符生成工具
 */
public class SymbolGenerateUtil {
    /**
     * 生成账户Id
     */
    public static BigInteger generateUserId() {
        return new BigInteger("1" + System.currentTimeMillis() + new Random().nextInt(10));
    }
}

在util包下新建ValidateUtil工具类,用于校验一些内容是否合法(如邮箱、用户名等)

package com.wbbb.demo01.util;

import java.util.regex.Pattern;

/**
 * 校验工具
 */
public class ValidateUtil {
    /**
     * 校验邮箱地址是否合法
     */
    public static boolean isEmailValid(String email) {
        Pattern regex = Pattern.compile("^[\\w-]+(.[\\w-]+)*@([a-zA-Z0-9]+(-?[a-zA-Z0-9]+)+\\.)+[a-zA-Z]{2,4}$");
        return regex.matcher(email).matches();
    }
}

UserService类中新增方法join,处理注册的业务逻辑

package com.wbbb.demo01.service;

import com.wbbb.demo01.dto.response.data.LoginResponseDataDto;
import com.wbbb.demo01.entity.User;
import com.wbbb.demo01.repository.UserRepository;
import com.wbbb.demo01.util.CryptoUtil;
import com.wbbb.demo01.util.SymbolGenerateUtil;
import com.wbbb.demo01.util.TokenUtil;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;

import java.math.BigInteger;

@AllArgsConstructor
@Service
public class UserService {
    private final UserRepository userRepository;

    /**
     * 检测账户名称是否可用
     */
    public boolean checkUsernameAvailable(String username) {
        return !userRepository.existsByUsername(username);
    }

    /**
     * 注册
     */
    public void join(String email, String username, String password) {
        password = CryptoUtil.SHA256(password);
        BigInteger userId;
        do
            userId = SymbolGenerateUtil.generateUserId();
        while (userRepository.existsById(userId));
        User user = new User(userId, username, password, email, System.currentTimeMillis());
        userRepository.save(user);
    }

    /**
     * 登录
     * @return 登录成功返回{ token, userId },失败返回null
     */
    public LoginResponseDataDto login(String username, String password) {
        password = CryptoUtil.SHA256(password);
        User user = userRepository.findByUsernameAndPassword(username, password);
        if (user == null)
            return null;
        return new LoginResponseDataDto(TokenUtil.generateToken(user), user.getUserId());
    }
}

UserController类中新增join方法,用于处理"/user/join"接口的POST请求

package com.wbbb.demo01.controller;

import com.wbbb.demo01.dto.request.JoinRequestDto;
import com.wbbb.demo01.dto.request.LoginRequestDto;
import com.wbbb.demo01.dto.response.ResponseDto;
import com.wbbb.demo01.dto.response.data.LoginResponseDataDto;
import com.wbbb.demo01.service.UserService;
import com.wbbb.demo01.util.ValidateUtil;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;

@AllArgsConstructor
@CrossOrigin("*")
@RestController
@RequestMapping("/user")
public class UserController {
    private final UserService userService;

    /**
     * 检查账户名称是否可用
     */
    @GetMapping("/available")
    public ResponseDto<Boolean> checkUsernameAvailable(@RequestParam("username") String username) {
        if (username.isEmpty())
            return ResponseDto.success(false);
        return ResponseDto.success(userService.checkUsernameAvailable(username));
    }

    /**
     * 注册
     */
    @PostMapping("/join")
    public ResponseDto<?> join(@RequestBody JoinRequestDto joinRequestDto) {
        if (!ValidateUtil.isEmailValid(joinRequestDto.getEmail()))
            return ResponseDto.badRequest(null, "请输入有效的电子邮件地址");
        if (!userService.checkUsernameAvailable(joinRequestDto.getUsername()))
            return ResponseDto.conflict(null, "账户名称不可用");
        userService.join(joinRequestDto.getEmail(), joinRequestDto.getUsername(), joinRequestDto.getPassword());
        return ResponseDto.success(null, "注册成功");
    }

    /**
     * 登录
     */
    @PostMapping("/login")
    public ResponseDto<LoginResponseDataDto> login(@RequestBody LoginRequestDto loginRequestDto) {
        LoginResponseDataDto loginResponseDataDto = userService.login(loginRequestDto.getUsername(), loginRequestDto.getPassword());
        if (loginResponseDataDto == null)
            return ResponseDto.notFound(null, "请核对您的密码和帐户名称并重试。");
        return ResponseDto.success(loginResponseDataDto, "登录成功");
    }
}

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值