十分钟写一个基于springboot+vue+redis+mysql的银行转账与用户后台管理系统,redis实现用户登录与缓存

界面效果

用户管理界面

 详情页面

 编辑页面

 删除功能

 用户界面

 查询余额

 取出余额

 存款

 转账





后端包结构

bean包下代码

管理员类

package com.example.qqqundatabasedemo.bean.concreteuser;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/4 12:40
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("q_adminuser")
public class AdminUser {

    @TableField("user_name")
    private String userName;
    @TableField("admin_pwd")
    private String adminPwd;

}

普通用户类

package com.example.qqqundatabasedemo.bean.concreteuser;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/4 12:40
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("q_bankuser")
public class BankUser {

    @TableField("card_id")
    @TableId
    private String cardId;
    @TableField("card_pwd")
    private String cardPwd;
    @TableField("user_name")
    private String userName;
    private String phone;
    private BigDecimal account;


}

dto下代码

package com.example.qqqundatabasedemo.bean.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;

import java.math.BigDecimal;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 23:20
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class QueryBalanceDTO {

    private String cardId;
    private String cardPwd;
    private String transferId;
    private BigDecimal money;

}

config包下代码

跨域配置类

package com.example.qqqundatabasedemo.config;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 12:58
 */

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class CorsConfig {
    // 当前跨域请求最大有效时长。这里默认1天
    private static final long MAX_AGE = 24 * 60 * 60;

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
        corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
        corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
        corsConfiguration.setMaxAge(MAX_AGE);
        source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
        return new CorsFilter(source);
    }

}

MP分页配置

package com.example.qqqundatabasedemo.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 12:24
 */
@Configuration
public class MyBatisPlusConfig {

    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }

}

redis配置

package com.example.qqqundatabasedemo.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/4 22:49
 */
@Configuration
public class RedisConfig {

    @Bean
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://秘密").setPassword("秘密");
        return Redisson.create(config);
    }


}

web配置

package com.example.qqqundatabasedemo.config;

import com.example.qqqundatabasedemo.interceptor.AdminLoginInterceptor;
import com.example.qqqundatabasedemo.interceptor.RefreshTokenInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.annotation.Resource;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 21:10
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate));

        registry.addInterceptor(new AdminLoginInterceptor())
                .excludePathPatterns("/admin/login"
                ,"/admin/logout");

    }
}

controller包下代码

管理员控制器

package com.example.qqqundatabasedemo.controller;

import com.example.qqqundatabasedemo.bean.concreteuser.AdminUser;
import com.example.qqqundatabasedemo.bean.concreteuser.BankUser;
import com.example.qqqundatabasedemo.service.AdminService;
import com.example.qqqundatabasedemo.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/4 13:00
 */

@RestController
@RequestMapping("admin")
public class AdminController {

    @Autowired
    private AdminService adminService;

    @GetMapping("handle")
    public Result getUserList(@RequestParam(defaultValue = "1") long page, @RequestParam(defaultValue = "5") long pageSize) {
        return adminService.queryUserByPage(page, pageSize);
    }

    @GetMapping("handle/{id}")
    public Result queryUser(@PathVariable("id") String id) {
        return adminService.queryUserById(id);
    }

    @DeleteMapping("handle/{id}")
    public Result removeUser(@PathVariable("id") String id) {
        return adminService.deleteUserById(id);
    }

    @PatchMapping("handle")
    public Result addUser(@RequestBody BankUser user) {
        return adminService.addUser(user);
    }

    @PostMapping("handle")
    public Result changeUser(@RequestBody BankUser user) {
        return adminService.updateUserById(user);
    }

    @PostMapping("login")
    public Result login(AdminUser user) {

        return adminService.adminLogin(user);

    }

    @PostMapping("logout")
    public Result logout(String token) {

        return adminService.logout(token);

    }

}

普通用户控制器

package com.example.qqqundatabasedemo.controller;

import com.example.qqqundatabasedemo.bean.dto.QueryBalanceDTO;
import com.example.qqqundatabasedemo.service.BankUserService;
import com.example.qqqundatabasedemo.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 22:48
 */
@RestController
@RequestMapping("user")
public class BankUserController {
    @Autowired
    private BankUserService bankUserService;

    @PostMapping("handle")
    public Result queryBalance(QueryBalanceDTO queryBalanceDTO) {
        return bankUserService.queryBalance(queryBalanceDTO);
    }

    @PostMapping("withdraw")
    public Result withdrawBalance(QueryBalanceDTO queryBalanceDTO) {
        return bankUserService.withDrawBalance(queryBalanceDTO);
    }

    @PostMapping("deposit")
    public Result depositBalance(QueryBalanceDTO queryBalanceDTO) {
        return bankUserService.depositBalance(queryBalanceDTO);
    }

    @PostMapping("transfer")
    public Result transferBalance(QueryBalanceDTO queryBalanceDTO) {
        return bankUserService.transferBalance(queryBalanceDTO);
    }

}

handler包下代码

package com.example.qqqundatabasedemo.handler;

import com.example.qqqundatabasedemo.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/4 22:46
 */
@ControllerAdvice
@Slf4j
public class ControllerExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result exceptionHandle(HttpServletRequest request, Exception e) {
        log.error("Request URL :{} Exception:{}",request.getRequestURL(),e);
        return Result.error("Request URL :" + request.getRequestURL() + "Exception:" + e);
    }

}

interceptor包下代码

刷新登陆状态拦截器

package com.example.qqqundatabasedemo.interceptor;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import com.example.qqqundatabasedemo.bean.concreteuser.AdminUser;
import com.example.qqqundatabasedemo.utils.AdminUserHolder;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.example.qqqundatabasedemo.utils.RedisConstants.LOGIN_TOKEN_KEY;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 20:57
 */

public class RefreshTokenInterceptor implements HandlerInterceptor {

    private final StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("authentication");
        if (StrUtil.isBlank(token)) return true;
        Map<Object, Object> adminUserMap = stringRedisTemplate.opsForHash().entries(LOGIN_TOKEN_KEY + token);
        if (!MapUtil.isEmpty(adminUserMap)) {
            AdminUser adminUser = BeanUtil.fillBeanWithMap(adminUserMap, new AdminUser(), false);
            AdminUserHolder.saveAdminUser(adminUser);
            stringRedisTemplate.expire(LOGIN_TOKEN_KEY + token, 30L, TimeUnit.MINUTES);
        }
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        AdminUserHolder.removeAdminUser();
    }
}

验证登录状态拦截器

package com.example.qqqundatabasedemo.interceptor;

import com.example.qqqundatabasedemo.utils.AdminUserHolder;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 21:13
 */

public class AdminLoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (AdminUserHolder.getAdminUser() == null) {
            response.setStatus(401);
            response.sendRedirect("https://localhost/admin/login");
            return false;
        } else return true;
    }
}

mapper包下代码

管理员mapper接口

package com.example.qqqundatabasedemo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.qqqundatabasedemo.bean.concreteuser.AdminUser;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

@Repository
@Mapper
public interface AdminUserMapper extends BaseMapper<AdminUser> {
}

普通用户mapper接口

package com.example.qqqundatabasedemo.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.qqqundatabasedemo.bean.concreteuser.BankUser;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/4 12:54
 */

@Repository
@Mapper
public interface BankUserMapper extends BaseMapper<BankUser> {
}

service包下代码

管理员服务层接口

package com.example.qqqundatabasedemo.service;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.qqqundatabasedemo.bean.concreteuser.AdminUser;
import com.example.qqqundatabasedemo.bean.concreteuser.BankUser;
import com.example.qqqundatabasedemo.mapper.AdminUserMapper;
import com.example.qqqundatabasedemo.vo.Result;

public interface AdminService{


    Result queryUserByPage(long page, long pageSize);

    Result queryUserById(String id);

    Result deleteUserById(String id);

    Result addUser(BankUser user);

    int deleteById(String id);

    Result updateUserById(BankUser user);

    Result adminLogin(AdminUser user);

    Result logout(String token);
}

管理员服务层接口实现类代码

package com.example.qqqundatabasedemo.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.qqqundatabasedemo.bean.concreteuser.AdminUser;
import com.example.qqqundatabasedemo.bean.concreteuser.BankUser;
import com.example.qqqundatabasedemo.mapper.AdminUserMapper;
import com.example.qqqundatabasedemo.mapper.BankUserMapper;
import com.example.qqqundatabasedemo.service.AdminService;
import com.example.qqqundatabasedemo.utils.RedisUtil;
import com.example.qqqundatabasedemo.vo.Result;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;

import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

import static com.example.qqqundatabasedemo.utils.RedisConstants.*;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 12:28
 */
@Service
public class AdminServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser> implements AdminService  {

    private final Random random = new Random();

    @Autowired
    private BankUserMapper bankUserMapper;

    @Autowired
    private AdminUserMapper adminUserMapper;

    @Autowired
    private RedisUtil redisUtil;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryUserByPage(long page, long pageSize) {
        Page<BankUser> pObj = new Page<>(page, pageSize);
        IPage<BankUser> userIPage = bankUserMapper.selectPage(pObj, null);
        return Result.success(userIPage);
    }

    @Override
    public Result queryUserById(String id) {
        BankUser bankUser = queryUserMutex(id);
        return Result.success(bankUser);
    }

    @Override
    public Result deleteUserById(String id) {
        AdminService proxy = (AdminService) AopContext.currentProxy();
        String msg = proxy.deleteById(id) > 0 ? "删除成功" : "删除失败";

        return "删除成功".equals(msg) ? Result.success(msg) : Result.error(msg);
    }

    @Override
    public Result addUser(BankUser user) {
        String msg = addByUser(user) > 0 ? "添加成功" : "添加失败";
        return "添加成功".equals(msg) ? Result.success(msg) : Result.error(msg);
    }

    public int addByUser(BankUser user) {
        long rawId = random.nextLong();
        long cardId = rawId > 0 ? rawId : -1 * rawId;
        user.setCardId(Long.toString(cardId));
        return bankUserMapper.insert(user);
    }

    @Transactional
    public int deleteById(String id) {
        int result = bankUserMapper.deleteById(id);
        if (result > 0) {
            stringRedisTemplate.delete(CACHE_USER_KEY + id);
        }
        return result;
    }

    @Transactional
    @Override
    public Result updateUserById(BankUser user) {
        int result = bankUserMapper.updateById(user);
        if (result > 0) {
            stringRedisTemplate.delete(CACHE_USER_KEY + user.getCardId());
        }
        return result > 0 ? Result.success("更新成功") : Result.error("更新失败");
    }

    @Override
    public Result adminLogin(AdminUser user) {
        QueryWrapper<AdminUser> wrapper = new QueryWrapper<>();
        wrapper.eq("user_name", user.getUserName());
        wrapper.eq("admin_pwd",user.getAdminPwd());
        AdminUser adminUser = adminUserMapper.selectOne(wrapper);
        if (adminUser != null) {
            String token = UUID.randomUUID().toString(true);
            Map<String, Object> adminUserMap = BeanUtil.beanToMap(adminUser, new HashMap<>(),
                    new CopyOptions().setIgnoreNullValue(true).setFieldValueEditor((k , v) -> v.toString()));
            stringRedisTemplate.opsForHash().putAll(LOGIN_TOKEN_KEY + token, adminUserMap);
            stringRedisTemplate.expire(LOGIN_TOKEN_KEY + token, 30L, TimeUnit.MINUTES);
            return Result.success(token);
        }
        return Result.error("用户名或密码错误");
    }

    @Override
    public Result logout(String token) {
        Boolean result = stringRedisTemplate.delete(LOGIN_TOKEN_KEY + token);
        return Boolean.TRUE.equals(result) ? Result.success("已在服务器端注销用户信息") : Result.error("服务器端注销失败或没有要注销的用户,将在半小时内自动回收账号登录信息");
    }

    public BankUser queryById(String id) {
        return bankUserMapper.selectById(id);
    }

    public BankUser queryUserMutex(String id) {
        return redisUtil.getWithMutex(BankUser.class, CACHE_USER_KEY, LOCK_USER_KEY, id, CACHE_USER_TTL, CACHE_LOCK_TTL, TimeUnit.MINUTES, this::queryById);
    }



}

用户服务层接口

package com.example.qqqundatabasedemo.service;

import com.example.qqqundatabasedemo.bean.dto.QueryBalanceDTO;
import com.example.qqqundatabasedemo.vo.Result;

public interface BankUserService {

    Result queryBalance(QueryBalanceDTO dto);

    Result withDrawBalance(QueryBalanceDTO queryBalanceDTO);

    Result depositBalance(QueryBalanceDTO queryBalanceDTO);

    Result transferBalance(QueryBalanceDTO queryBalanceDTO);
}

用户服务层接口实现类

package com.example.qqqundatabasedemo.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.qqqundatabasedemo.bean.concreteuser.BankUser;
import com.example.qqqundatabasedemo.bean.dto.QueryBalanceDTO;
import com.example.qqqundatabasedemo.mapper.BankUserMapper;
import com.example.qqqundatabasedemo.service.BankUserService;
import com.example.qqqundatabasedemo.vo.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 23:21
 */
@Service
public class BankUserServiceImpl implements BankUserService {

    @Autowired
    private BankUserMapper bankUserMapper;

    @Override
    public Result queryBalance(QueryBalanceDTO dto) {
        QueryWrapper<BankUser> wrapper = new QueryWrapper<>();
        wrapper.eq("card_id", dto.getCardId());
        wrapper.eq("card_pwd", dto.getCardPwd());
        BankUser bankUser = bankUserMapper.selectOne(wrapper);
        if (bankUser != null) return Result.success(bankUser.getAccount());
        else return Result.error("银行卡号或密码错误");
    }

    @Override
    public Result withDrawBalance(QueryBalanceDTO dto) {
        QueryWrapper<BankUser> wrapper = new QueryWrapper<>();
        wrapper.eq("card_id", dto.getCardId());
        wrapper.eq("card_pwd", dto.getCardPwd());
        BankUser bankUser = bankUserMapper.selectOne(wrapper);
        if (bankUser != null) {
            if (bankUser.getAccount().subtract(dto.getMoney()).compareTo(BigDecimal.ZERO) < 0) {
                return Result.error("卡内余额不足,无法取款");
            } else {
                BigDecimal result = bankUser.getAccount().subtract(dto.getMoney());
                bankUser.setAccount(result);
                bankUserMapper.updateById(bankUser);
            }
            return Result.success(bankUser.getAccount());
        }
        else return Result.error("银行卡号或密码错误");
    }

    @Override
    public Result depositBalance(QueryBalanceDTO dto) {
        QueryWrapper<BankUser> wrapper = new QueryWrapper<>();
        wrapper.eq("card_id", dto.getCardId());
        wrapper.eq("card_pwd", dto.getCardPwd());
        BankUser bankUser = bankUserMapper.selectOne(wrapper);
        if (bankUser != null) {
            if (dto.getMoney().compareTo(BigDecimal.ZERO) < 0) {
                return Result.error("存款金额需大于零");
            }
            bankUser.setAccount(bankUser.getAccount().add(dto.getMoney()));
            bankUserMapper.updateById(bankUser);
            return Result.success(bankUser.getAccount());
        }
        else return Result.error("银行卡号或密码错误");
    }

    @Transactional
    @Override
    public Result transferBalance(QueryBalanceDTO dto) {
        QueryWrapper<BankUser> wrapper = new QueryWrapper<>();
        wrapper.eq("card_id", dto.getCardId());
        wrapper.eq("card_pwd", dto.getCardPwd());
        BankUser bankUser = bankUserMapper.selectOne(wrapper);
        if (bankUser != null) {
            if (bankUser.getAccount().subtract(dto.getMoney()).compareTo(BigDecimal.ZERO) < 0) {
                return Result.error("卡内余额不足,无法转账");
            } else {
                BankUser target = bankUserMapper.selectById(dto.getTransferId());
                if (target == null) {
                    return Result.error("转账用户不存在");
                } else {
                    StringBuilder stringBuilder = new StringBuilder("转帐前本人账户与目标账户的余额分别为" + bankUser.getAccount() + "      " + target.getAccount());
                    target.setAccount(target.getAccount().add(dto.getMoney()));
                    bankUserMapper.updateById(target);
                    bankUser.setAccount(bankUser.getAccount().subtract(dto.getMoney()));
                    bankUserMapper.updateById(bankUser);
                    stringBuilder.append("转账后本人账户与目标账户的余额分别为").append(bankUser.getAccount()).append("      ").append(target.getAccount());
                    return Result.success(stringBuilder);
                }
            }
        }
        else return Result.error("银行卡号或密码错误");

    }
}

utils包下代码

线程存储管理员登录信息类

package com.example.qqqundatabasedemo.utils;

import com.example.qqqundatabasedemo.bean.concreteuser.AdminUser;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 21:05
 */

public class AdminUserHolder {

    private static final ThreadLocal<AdminUser> tl = new ThreadLocal<>();

    public static void saveAdminUser(AdminUser adminUser) {
        tl.set(adminUser);
    }

    public static AdminUser getAdminUser() {
        return tl.get();
    }

    public static void removeAdminUser() {
        tl.remove();
    }

}

常量池

package com.example.qqqundatabasedemo.utils;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 13:32
 */

public class RedisConstants {

    public static final String CACHE_USER_KEY = "cache:user:";

    public static final String LOCK_USER_KEY = "lock:user:";

    public static final String LOGIN_TOKEN_KEY = "login:admin:token:";

    public static final long CACHE_LOCK_TTL = 3L;

    public static final long CACHE_USER_TTL = 30L;

}

redis工具类

package com.example.qqqundatabasedemo.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

import static com.example.qqqundatabasedemo.utils.RedisConstants.CACHE_LOCK_TTL;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/5 18:58
 */
@Component
public class RedisUtil {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public <E,ID> E getWithMutex(Class<E> clazz, String keyPrefix, String lockKeyPrefix, ID id, long timeout, long emptyTimeout, TimeUnit unit, Function<ID, E> function) {
        E value = null;
        try {
            // 从redis中查询相应ID的商品信息
            String shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
            // 查到则直接返回
            if (StrUtil.isNotBlank(shopJson))
            {
                return JSONUtil.toBean(shopJson, clazz);
            }
            // 未查到则查询MYSQL数据库,查询到则将数据存储到redis中并返回,查不到则返回404
            // 为防止缓存穿透,如果未查到数据则缓存空字符串放入redis中
            if ("".equals(shopJson)) {
                return null;
            }
            // 如果缓存中不存在这个数据或者缓存已过期,则互斥的进行缓存重建
            if (tryLock(lockKeyPrefix, id, "1")) {
                // 从redis中再次查询相应ID的商品信息
                shopJson = stringRedisTemplate.opsForValue().get(keyPrefix + id);
                // 查到则直接返回
                if (StrUtil.isNotBlank(shopJson))
                {
                    return JSONUtil.toBean(shopJson, clazz);
                }
                value = function.apply(id);
                if (value == null) {
                    // XXX stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_EMPTY_TTL, TimeUnit.MINUTES);
                    set(keyPrefix + id, "",  emptyTimeout, unit);
                    return null;
                } else {
                    // XXX stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
                    set(keyPrefix + id, value,  timeout, unit);
                }
            } else {
                Thread.sleep(20);
                return getWithMutex(clazz, keyPrefix, lockKeyPrefix, id, timeout, emptyTimeout, unit, function);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            unLock(lockKeyPrefix, id);
        }

        return value;
    }

    public <E> void set(String key, E obj, long timeout, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj), timeout, unit);
    }

    public<E> boolean tryLock(String keyPrefix, E id, String value) {
        Boolean absent = stringRedisTemplate.opsForValue().setIfAbsent(keyPrefix + id, value, CACHE_LOCK_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(absent);
    }

    public<E> void unLock(String keyPrefix, E id) {
        stringRedisTemplate.delete(keyPrefix + id);
    }

}

vo包下代码

package com.example.qqqundatabasedemo.vo;

import lombok.Data;

/**
 * @author bilibilidick
 * @version 2022 07
 * @date 2022/7/4 20:17
 */
@Data
public class Result {

    private int code;
    private Object msg;

    private Result(int code, Object msg) {
        this.code = code;
        this.msg = msg;
    }

    public static Result success(Object msg) {
        return new Result(200, msg);
    }

    public static Result error(Object msg) {
        return new Result(500, msg);
    }
}




前端包结构,使用Vue2书写前端

main.js代码 

import Vue from 'vue'
import App from './App.vue'
import VueRouter from "vue-router";
import ElementUI from 'element-ui';
import router from "@/route";
import 'element-ui/lib/theme-chalk/index.css';
import request from "@/network/request";
Vue.config.productionTip = false
Vue.use(ElementUI)
Vue.use(VueRouter)
Vue.prototype.$request = request
new Vue({
  render: h => h(App),
  router
}).$mount('#app')

路由配置

import VueRouter from "vue-router";
import Home from "@/page/Home";
import Admin from "@/page/Admin";
import Login from "@/page/Login";
import ElementUI from "element-ui";
import UserMenu from "@/page/UserMenu";
import UserQuery from "@/page/UserQuery";
import UserTransfer from "@/page/UserTransfer";

const router = new VueRouter({
    routes: [
        {
            path: '/home',
            name: 'home',
            component: Home,
            meta: {
                title: '主页'
            }
        },
        {
            path: '/admin',
            name: 'admin',
            component: Admin,
            meta: {
                title: '后台管理'
            }
        },
        {
            path: '/login',
            name: 'login',
            component: Login,
            meta: {
                title: '登录'
            }
        },
        {
            path: '/user',
            name: 'user',
            component: UserMenu,
            meta: {
                title: '用户操作菜单'
            },
            children: [
                {
                    path: 'userquery',
                    name: 'userquery',
                    component: UserQuery,
                    meta: {
                        title: '查询余额'
                    }
                },
                {
                    path: 'usertransfer',
                    name: 'usertransfer',
                    component: UserTransfer,
                    meta: {
                        title: '用户转账'
                    }
                }
            ]
        }
    ]
})

router.beforeEach((to, from, next) => {
    let token = sessionStorage.getItem('usertoken')
    if (token == null) {
        if (to.name === 'login' || to.name === 'home') {
            next()
            return
        } else {
            ElementUI.Notification({
                title: 'Code: 403',
                type: "error",
                message: '请先登录',
                showClose: true
            })
            next("/login")
            return
        }
    }
    if (to.name === 'login') {
            next('/home')
    } else next()


})

//全局后置路由守卫————初始化的时候被调用、每次路由切换之后被调用
router.afterEach((to, from) => {

    document.title = to.meta.title || '天气识别系统'
})

export default router

请求头与响应体的拦截器配置

import axios from "axios";
import ElementUI from "element-ui";
const instance = axios.create({
    baseURL: 'http://localhost:8080/'
})
instance.interceptors.request.use(config => {

        let token = window.sessionStorage.getItem('usertoken')

        if (token == null) {
            //
        } else {
            config['headers']['authentication'] = token

            //data.data.normal_login_token为发送Ajax获取到的token信息

        }
        /*if (typeof token != "undefined" && token != null && token !== 'undefined'){
            config['headers'].token = JSON.parse(token)
        }*/
        return config;
    },
    err => {
        //
    })

instance.interceptors.response.use(res => {
    if (res.data.code === 200) {
        console.log(res.data)
        ElementUI.Notification({
            title: 'Code: ' + res.data.code,
            type: "success",
            message: res.data.msg,
            showClose: true
        })
    } else if (res.data.code === 401) {
        window.sessionStorage.removeItem('usertoken')
        ElementUI.Notification({
            title: 'Code: ' + res.data.code,
            type: "error",
            message: res.data.msg,
            showClose: true
        })
    } else {
        ElementUI.Notification({
            title: 'Code: ' + res.data.code,
            type: "error",
            message: res.data.msg,
            showClose: true
        })
    }
    return res
})

export default instance

UserHeader

<template>
  <el-menu
      :default-active="activeIndex"
      class="el-menu-demo"
      mode="horizontal"
      @select="handleSelect"
      background-color="#545c64"
      text-color="#fff"
      active-text-color="#ffd04b">
    <el-menu-item index="1" @click.native="$router.push('userquery')">查询与存取款业务</el-menu-item>
    <el-menu-item index="2" @click.native="$router.push('usertransfer')">转账业务</el-menu-item>
    <el-menu-item index="3" @click.native="$router.push('/admin')">后台管理</el-menu-item>
  </el-menu>
</template>

<script>
export default {
  name: "UserHeader",
  data() {
    return {
      activeIndex: '1',
    };
  },
  methods: {
    handleSelect(key, keyPath) {
      console.log(key, keyPath);
    }
  }
}
</script>

<style scoped>

</style>

Admin

<template>
  <div>

    <el-dropdown split-button type="primary" @click="handleLogout">
      管理员身份注销
      <el-dropdown-menu slot="dropdown">
        <el-dropdown-item @click.native="$router.push({name:'userquery'})">用户页面</el-dropdown-item>
      </el-dropdown-menu>
    </el-dropdown>

    <el-table
        :data="tableData"
        border
        style="width: 100%">
      <el-table-column
          fixed
          prop="cardId"
          label="银行卡号"
          width="auto">
      </el-table-column>
      <el-table-column
          prop="userName"
          label="用户名"
          width="auto">
      </el-table-column>
      <el-table-column
          prop="account"
          label="账户余额"
          width="auto">
      </el-table-column>
      <el-table-column
          fixed="right"
          label="操作"
          width="auto">
        <template slot-scope="scope">
          <el-button type="text" @click=handleQuery(scope.row) size="small">详情</el-button>
          <el-button @click="handleClick(scope.row)" type="text" size="small">编辑</el-button>
          <el-button @click="handleDelete(scope.row)" type="text" size="small">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <div class="block" style="background-color: white">
      <el-pagination
          @current-change="handleCurrentChange"
          :current-page.sync="currentPage"
          :page-size="pageSize"
          layout="prev, pager, next, jumper"
          :total="dataTotal">
      </el-pagination>
    </div>
    <el-row>
      <el-button @click="handleAdd" type="success">新增</el-button>
    </el-row>

    <!-- Form -->
<!--    <el-button type="text" @click="dialogFormVisible = true">打开嵌套表单的 Dialog</el-button>-->

    <el-dialog title="用户信息" :visible.sync="dialogFormVisible">
      <el-form :model="form">
        <el-form-item label="用户Id(自动生成)" :label-width="formLabelWidth">
          <el-input readonly v-model="form.cardId" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="用户名" :label-width="formLabelWidth">
          <el-input v-model="form.userName" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="用户银行卡密码" :label-width="formLabelWidth">
          <el-input v-model="form.cardPwd" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="用户电话" :label-width="formLabelWidth">
          <el-input v-model="form.phone" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="用户余额" :label-width="formLabelWidth">
          <el-input v-model="form.account" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false;form = {}">取 消</el-button>
        <el-button type="primary" @click="handleUpdate">确 定</el-button>
      </div>
    </el-dialog>

    <el-dialog title="详细信息" :visible.sync="dialogTableVisible">
      <el-table :data="gridData">
        <el-table-column property="cardId" label="银行卡号" width="150"></el-table-column>
        <el-table-column property="userName" label="姓名" width="200"></el-table-column>
        <el-table-column property="cardPwd" label="银行卡密码"></el-table-column>
        <el-table-column property="phone" label="电话号码"></el-table-column>
        <el-table-column property="account" label="余额"></el-table-column>
      </el-table>
    </el-dialog>


  </div>
</template>

<script>
export default {
  name: "Admin",
  data() {
    return {
      activeIndex: '1',
      activeIndex2: '1',
      currentPage: 1,
      pageSize: 100,
      dataTotal: 1000,
      tableData: [],
      gridData: [],
      dialogFormVisible: false,
      dialogTableVisible: false,
      form: {},
      formLabelWidth: '120px'
    };
  },
  methods: {
    handleSelect(key, keyPath) {
      console.log(key, keyPath);
    },
    handleClick(row) {
      console.log(row);
      this.form = row
      this.dialogFormVisible = true
    },
    handleUpdate() {
      if (this.form.cardId !== undefined) {
        console.log("更新操作")
        this.dialogFormVisible = false
        console.log(JSON.stringify(this.form))
        this.$request.post("admin/handle", JSON.stringify(this.form),
            { headers: { "Content-Type": "application/json;charset=utf-8" } })
            .then(res => {
              //
              this.init(this.currentPage)
            })
      } else {
        console.log("新增操作")
        this.dialogFormVisible = false
        console.log(JSON.stringify(this.form))
        this.$request.patch("admin/handle", JSON.stringify(this.form),
            { headers: { "Content-Type": "application/json;charset=utf-8" } })
            .then(res => {
              //
              this.init(this.currentPage)
            })
      }
      this.form = {}
    },
    handleAdd() {
      this.dialogFormVisible = true
    },
    handleDelete(row) {
      this.$request.delete(`admin/handle/${row.cardId}`)
          .then(res => {
            this.init(this.currentPage)
          })
    },
    handleQuery(row) {
      this.dialogTableVisible = true
      this.gridData = []
      this.$request.get(`admin/handle/${row.cardId}`)
          .then(res => {
            console.log(res.data.msg)
            this.gridData.push(res.data.msg)
          })

    },
    handleCurrentChange(val) {
      this.init(val)
    },
    handleLogout() {
      let token = window.sessionStorage.getItem("usertoken")
      window.sessionStorage.removeItem("usertoken")
      this.$notify.success({
        title: 'Code: 200',
        message: '前端注销成功',
        showClose: true
      });
      let params = new URLSearchParams()
      params.append("token", token)
      this.$request.post("admin/logout", params)
          .then(res => {
            console.log(res.data.msg)
          })
      this.$router.push("/login")
    },
    init(params) {
      this.$request.get(`/admin/handle?page=${params}`)
          .then(res => {
            if (res.data.code === 200) {
              this.currentPage = res.data.msg.current
              this.tableData = res.data.msg.records
              this.pageSize = res.data.msg.size
              this.dataTotal = res.data.msg.total
              if (this.tableData.length === 0) {
                this.init(params - 1)
              }
            }
          })
    }

  },
  mounted() {
    this.init(this.currentPage)
  }
}
</script>

<style scoped>

</style>

Home

<template>
<div>我是主页</div>
</template>

<script>
export default {
  name: "Home"
}
</script>

<style scoped>

</style>

Login

<template>
  <div class="login_box">

    <div class="login_content">

      <el-form
          :model="ruleForm"
          status-icon
          :rules="rules"
          ref="ruleForm"
          label-width="65px"
          class="loginContainer"
      >

        <h1 style="text-align: center">用户登录</h1>
        <el-form-item label="用户名" prop="username">
          <el-input type="text" v-model="ruleForm.username" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="密码" prop="password">
          <el-input type="password" v-model="ruleForm.password" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="submitForm('ruleForm')">登录</el-button>
          <el-button type="primary" @click.native="toRegister">跳转到注册页面</el-button>
          <el-button style="margin-top: 20px;margin-left: 80px" @click="goHome" class="test">跳转到首页</el-button>
          <el-button style="margin-top: 20px;margin-left: 80px" @click="resetForm('ruleForm')" class="test">重置</el-button>
        </el-form-item>
      </el-form>
    </div>
  </div>
</template>

<script>
export default {
  name: 'Login',
  data() {
    return {
      ruleForm: {
        username: "",
        password: ""
      },
      rules: {
        username: [
          { required: true, message: "请输入用户名", trigger: "blur" },
          { min: 5, max: 20, message: "长度在 5 到 20 个字符", trigger: "blur" }
        ],
        password: [
          { required: true, message: "请输入密码", trigger: "blur" },
          { min: 6, max: 16, message: "长度在 6 到 16 个字符", trigger: "blur" }
        ]
      }
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          let params = new URLSearchParams();
          params.append('userName', this.ruleForm.username); //你要传给后台的参数值 key/value
          params.append('adminPwd', this.ruleForm.password); //你要传给后台的参数值 key/value
          this.$request.post('admin/login/',params).then(res => {
            if (res.data.code === 200) {
              window.sessionStorage.setItem('usertoken',res.data.msg)
              this.$router.push({
                name: 'admin'
              })
            }
          }).catch(err => {
            this.$notify.error({
              title: 'Code: 500',
              message: err.message + '\n请启动后端服务器或检查网络设置!',
              showClose: true
            });
          })
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
    toRegister() {
      this.$router.push({
        name: 'zhuce'
      })
    },
    goHome() {
      this.$router.push({
        name: 'zhuyaobufen'
      })
    }
  }
}
</script>

<style scoped>
.loginContainer {
  border-radius: 15px;
  background-clip: padding-box;
  margin: 188px auto;
  width: 400px;
  padding: 15px 34px 14px 35px;
  background: #fff;
  border: 5px solid #eaeaea;
  box-shadow: 0 0 25px #cac6c6;
}
</style>

UserMenu

<template>
  <div>
  <UserHeader/>
  <router-view/>
  </div>
</template>

<script>
import UserHeader from "@/components/UserHeader";

export default {
  name: "UserMenu",
  components: {
    UserHeader
  }
}
</script>

<style scoped>

</style>

UserQuery

<template>
  <div>
  <el-form style="background-color: white" :inline="true" :model="formInline" class="demo-form-inline">
    <el-form-item label="银行卡号码">
      <el-input v-model="formInline.cardId" placeholder="银行卡号"></el-input>
    </el-form-item>
    <el-form-item label="银行卡密码">
      <el-input v-model="formInline.cardPwd" placeholder="卡号密码"></el-input>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="checkBalance">查询余额</el-button>
      <el-button type="primary" @click="toWithDraw">取款</el-button>
      <el-button type="primary" @click="toDeposit">存款</el-button>
      <el-button type="primary" @click="toTransfer">转账</el-button>
    </el-form-item>
  </el-form>

  <el-dialog title="用户存款界面" :visible.sync="dialogDepositFormVisible">
    <el-form :model="form">
      <el-form-item  label="银行卡号" :label-width="formLabelWidth">
        <el-input readonly v-model="formInline.cardId" autocomplete="off"></el-input>
      </el-form-item>
      <el-form-item  label="卡号密码" :label-width="formLabelWidth">
        <el-input readonly v-model="formInline.cardPwd" autocomplete="off"></el-input>
      </el-form-item>
      <el-form-item label="金额" :label-width="formLabelWidth">
        <el-input v-model="form.money" autocomplete="off"></el-input>
      </el-form-item>
    </el-form>
    <div slot="footer" class="dialog-footer">
      <el-button @click="dialogDepositFormVisible = false">取 消</el-button>
      <el-button type="primary" @click="depositHandle">确 定</el-button>
    </div>
  </el-dialog>
  <el-dialog title="用户取款界面" :visible.sync="dialogWithDrawFormVisible">
      <el-form :model="form">
        <el-form-item  label="银行卡号" :label-width="formLabelWidth">
          <el-input readonly v-model="formInline.cardId" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item  label="卡号密码" :label-width="formLabelWidth">
          <el-input readonly v-model="formInline.cardPwd" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="金额" :label-width="formLabelWidth">
          <el-input v-model="form.money" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogWithDrawFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="withDrawHandle">确 定</el-button>
      </div>
    </el-dialog>
  <el-dialog title="用户转账界面" :visible.sync="dialogTransferFormVisible">
      <el-form :model="form">
        <el-form-item  label="银行卡号" :label-width="formLabelWidth">
          <el-input readonly v-model="formInline.cardId" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item  label="卡号密码" :label-width="formLabelWidth">
          <el-input readonly v-model="formInline.cardPwd" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="转账卡号" :label-width="formLabelWidth">
          <el-input v-model="form.transferId" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="转账金额" :label-width="formLabelWidth">
          <el-input v-model="form.money" autocomplete="off"></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogTransferFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="transferHandle">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
export default {
  name: "UserQuery",
  data() {
    return {
      formInline: {
        cardId: '',
        cardPwd: ''
      },
      dialogDepositFormVisible: false,
      dialogWithDrawFormVisible: false,
      dialogTransferFormVisible: false,
      form: {
        money: 0,
        transferId: ''
      },
      formLabelWidth: '120px'
    }
  },
  methods: {
    toWithDraw() {
      this.dialogWithDrawFormVisible = true
    },
    withDrawHandle() {
      let params = new URLSearchParams();
      params.append("cardId", this.formInline.cardId)
      params.append("cardPwd", this.formInline.cardPwd)
      params.append("money", this.form.money)
      this.$request.post("user/withdraw", params)
          .then(res => {
            if (res.data.code === 200) {
              this.$notify.success({
                title: 'Code: 200',
                message: '取款成功,卡内余额为' + res.data.msg,
                showClose: true
              });
            }

          })
      this.dialogWithDrawFormVisible = false
    },
    toDeposit() {
      this.dialogDepositFormVisible = true
    },
    depositHandle() {
      let params = new URLSearchParams();
      params.append("cardId", this.formInline.cardId)
      params.append("cardPwd", this.formInline.cardPwd)
      params.append("money", this.form.money)
      this.$request.post("user/deposit", params)
          .then(res => {
            if (res.data.code === 200) {
              this.$notify.success({
                title: 'Code: 200',
                message: '存款成功,卡内余额为' + res.data.msg,
                showClose: true
              });
            }
          })
      this.dialogDepositFormVisible = false
    },
    toTransfer() {
      this.dialogTransferFormVisible = true
    },
    transferHandle() {
      let params = new URLSearchParams();
      params.append("cardId", this.formInline.cardId)
      params.append("cardPwd", this.formInline.cardPwd)
      params.append("money", this.form.money)
      params.append("transferId", this.form.transferId)
      this.$request.post("user/transfer", params)
          .then(res => {
            if (res.data.code === 200) {
              this.$notify.success({
                title: 'Code: 200',
                message: '转账成功,卡内余额为' + res.data.msg,
                showClose: true
              });
            }
          })
      this.dialogTransferFormVisible = false
    },
    checkBalance() {
      let params = new URLSearchParams();
      params.append("cardId", this.formInline.cardId)
      params.append("cardPwd", this.formInline.cardPwd)
      this.$request.post("user/handle", params)
          .then(res => {
            if (res.data.code === 200) {
              console.log(res.data.msg)
              this.$notify.success({
                title: 'Code: 200',
                message: '卡内余额为' + res.data.msg,
                showClose: true
              });
            }
          })
    }
  }
}
</script>

<style scoped>

</style>

UserTransfer

<template>
<div>
  <el-steps :active="0" simple>
    <el-step title="填写银行卡号" icon="el-icon-edit"></el-step>
    <el-step title="填写银行卡密码" icon="el-icon-upload"></el-step>
    <el-step title="填写转账人银行卡号" icon="el-icon-picture"></el-step>
    <el-step title="填写转账金额" icon="el-icon-picture"></el-step>
    <el-step title="确认" icon="el-icon-picture"></el-step>
  </el-steps>
</div>
</template>

<script>
export default {
  name: "UserTransfer"
}
</script>

<style scoped>

</style>

评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值