第3章 用户登录.md 29 KB

谷粒随享

第3章 用户登录

学习目标:

  • 了解小程序/移动端登录业务需求
  • 认证状态校验
    • 自定义注解
    • 认证切面
  • 用户登录
    • 微信登录
    • 初次登录(初始化账户(余额)记录)
    • 非初次登录
    • 完成登录(响应登录token)
  • 获取用户信息
  • 用户信息修改

1、认证状态校验

1.1 自定义注解

​ 当用户在查询专辑列表的时候,要求用户必须是登录状态。就应该让用户登录,所以在此我们自定义一个注解来表示访问此功能时必须要登录。

注解作用:哪些需要登录才能访问必须要添加,那些需要获取到用户Id控制层方法也必须加这个注解.

service-util 模块中添加登录注解

package com.atguigu.tingshu.common.login;

import java.lang.annotation.*;


/**
 * 用户验证身份自定义注解
 * 元注解解释
 * @Target:注解使用位置,认证注解使用使用在controller层方法上
 * @Retention:注解保留声明周期 SOURCE-->CLASS-->RUNTIME   例如:CLASS 该注解会保留到字节码,运行时没有
 * @Inherited:是否可以被继承
 * @Documented:是否产生java文档
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface GuiGuLogin {


    /**
     * 要求该注解修饰方法必须登录才能调用
     * @return
     */
    boolean require() default true;

}

1.2 切面类

RequestContextHolder类持有上下文的Request容器。

用法:

//  获取请求对象
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
//  转化为ServletRequestAttributes
ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;
//  获取到HttpServletRequest 对象
HttpServletRequest request = sra.getRequest();
//	获取到HttpServletResponse 对象
HttpServletResponse response = sra.getResponse();

request 和 response 如何与 当前进行挂钩的?看底层源码

首先分析RequestContextHolder这个类,里面有两个ThreadLocal保存当前线程下的request

public abstract class RequestContextHolder {
    // 得到存储进去的request
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
    //可被子线程继承的reques
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");			

再看getRequestAttributes() 方法,相当于直接获取ThreadLocal里面的值,这样就保证了每一次获取到的Request是该请求的request.

@Nullable
public static RequestAttributes getRequestAttributes() {
    RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
    if (attributes == null) {
        attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
    }
    return attributes;
}

request和response等是什么时候设置进去的?

springMVC 核心类DispatcherServlet 继承关系

  1. HttpServletBean 进行初始化工作
  2. FrameworkServlet 初始化 WebApplicationContext,并提供service方法预处理请
  3. DispatcherServlet 具体分发处理.

那么就可以在FrameworkServlet查看到该类重写了service(),doGet(),doPost()...等方法,这些实现里面都有一个预处理方法processRequest(request, response);,所以定位到了我们要找的位置

查看processRequest(request, response);的实现,具体可以分为以下几步:

  1. 获取上一个请求的参数
  2. 重新建立新的参数
  3. 设置到XXContextHolder
  4. 父类的service()处理请求
  5. 恢复request(恢复原来的LocaleContext和ServiceRequestAttributes到LocaleContextHolder和RequestContextHolder,避免影响Servlet以外的处理,如Filter
  6. 发布事件(发布ServletRequestHandlerEvent消息,这个请求是否执行成功都会发布消息)

自定义切面类

package com.atguigu.tingshu.common.login;

import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.execption.GuiguException;
import com.atguigu.tingshu.common.result.ResultCodeEnum;
import com.atguigu.tingshu.common.util.AuthContextHolder;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.reflect.Method;

/**
 * 验证用户身份注解增强切面类
 *
 * @author: atguigu
 * @create: 2024-02-22 10:14
 */
@Slf4j
@Aspect
@Component
public class LoginAspect {

    @Autowired
    private RedisTemplate redisTemplate;


    /**
     * 环绕通知逻辑,作用到切入点上(所有使用@GuiguLogin方法)
     *
     * @param pjp 目标方法对象
     * @return 目标方法执行结果
     * @throws Throwable
     */
    @Around("@annotation(guiGuLogin)")
    public Object authAround(ProceedingJoinPoint pjp, GuiGuLogin guiGuLogin) throws Throwable {

        //todo 采用切入点对象获取方法上注解属性值
        //Signature signature = pjp.getSignature();
        //MethodSignature methodSignature = (MethodSignature) signature;
        //Method method = methodSignature.getMethod();
        //GuiGuLogin annotation = method.getAnnotation(GuiGuLogin.class);
        //boolean require = annotation.require();

        //一 前置通知
        log.info("----------前置通知.....");
        //1.获取请求对象HttpServletRquest
        //1.1 从请求上下文中获取RequestAttributes接口-从ThreadLocal<RequestAttributes>中获取到的  思考:什么时候存进去的??
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //1.2 将RequestAttributes接口转为实现类ServletRequestAttributes
        ServletRequestAttributes sra = (ServletRequestAttributes) requestAttributes;

        //1.3 获取请求对象
        HttpServletRequest request = sra.getRequest();

        //1.4 获取小程序端token令牌
        String token = request.getHeader("token");

        //2.验证token令牌
        //2.1 拼接用户登录key 形式:user:login:token令牌  登录存入Value:UserInfoVo对象
        String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
        UserInfoVo userInfoVo = (UserInfoVo) redisTemplate.opsForValue().get(loginKey);

        //3.token没有查询到对应用户信息 目标方法要求登录方可访问 抛出异常:业务状态码:208,小程序引导用户登录
        if (guiGuLogin.require() && userInfoVo == null) {
            throw new GuiguException(ResultCodeEnum.LOGIN_AUTH);
        }

        //4.如果用户登录,将用户ID存入ThreadLocal 方便后续controller、service、mapper都可以获取当前登录用户ID
        if (userInfoVo != null) {
            AuthContextHolder.setUserId(userInfoVo.getId());
        }
        Object retVal;
        try {
            //二 执行目标方法(凡是在方法上加@GuiGuLogin注解)
            retVal = pjp.proceed();

        } finally {
            //三 后置通知
            log.info("----------后置通知.....");
            //5.清理ThreadLocal中用户信息-避免内存泄漏
            AuthContextHolder.removeUserId();
        }
        return retVal;
    }
}

在查看专辑列表的时候,添加注解。在点击查看专辑列表的时候,就会提示我们需要进行登录!

1.3 使用认证校验注解

在所有业务微服务中,com.atguigu.tingshu.*.api.XXXXController类中下所有需要登录才可以访问方法上加注解@GuiGuLogin 以下接口测试需要完成登录代码后才进行测试

AlbumInfoApiController

/**
 * 该接口登录才能访问,目前并未实现登录无法获取用户ID,动态获取到用户ID
 * 提供给内容创作者/运营人员保存专辑
 * @param albumInfoVo
 * @return
 */
@Operation(summary = "保存专辑")
@PostMapping("/albumInfo/saveAlbumInfo")
@GuiGuLogin//(required = true)
public Result saveAlbumInfo(@RequestBody @Validated AlbumInfoVo albumInfoVo){//...}
    
/**
 * 该接口登录才能访问
 * 查询当前用户专辑分页列表
 * @param page 页码
 * @param limit 页大小
 * @param albumInfoQuery 查询条件对象
 * @return
 */
@Operation(summary = "查询当前用户专辑分页列表")
@PostMapping("/albumInfo/findUserAlbumPage/{page}/{limit}")
@GuiGuLogin
public Result<Page<AlbumListVo>> getUserAlbumPage(
		@PathVariable int page,
		@PathVariable int limit,
		@RequestBody AlbumInfoQuery albumInfoQuery){//...}


/**
 * 该接口登录才能访问
 * 根据专辑ID删除专辑
 * @param id
 * @return
 */
@GuiGuLogin
@Operation(summary = "根据专辑ID删除专辑")
@DeleteMapping("/albumInfo/removeAlbumInfo/{id}")
public Result removeAlbumInfo(@PathVariable Long id){//...}


/**
 *
 * @param trackInfoVo
 * @return
 */
@GuiGuLogin
@Operation(summary = "新增声音")
@PostMapping("/trackInfo/saveTrackInfo")
public Result saveTrackInfo(@RequestBody @Validated TrackInfoVo trackInfoVo)
    
/**
 * 该接口登录才能访问
 * 修改专辑信息
 * @param id 专辑ID
 * @param albumInfoVo 修改专辑信息VO对象
 * @return
 */
@GuiGuLogin
@Operation(summary = "修改专辑信息")
@PutMapping("/albumInfo/updateAlbumInfo/{id}")
public Result updateAlbumInfo(@PathVariable Long id, @RequestBody @Validated AlbumInfoVo albumInfoVo){//...}

/**
 * 该接口登录才能访问
 * 查询当前登录用户专辑列表
 * @return
 */
@GuiGuLogin
@Operation(summary = "查询当前登录用户专辑列表")
@GetMapping("/albumInfo/findUserAllAlbumList")
public Result<List<AlbumInfo>> getUserAllAlbumList(){//...}

TrackInfoApiController

/**
 * 该接口需要登录才能访问
 * 保存专辑下声音
 *
 * @param trackInfoVo 声音信息VO对象
 * @return
 */
@GuiGuLogin
@Operation(summary = "保存专辑下声音")
@PostMapping("/trackInfo/saveTrackInfo")
public Result saveTrackInfo(@RequestBody @Validated TrackInfoVo trackInfoVo){//...}
    
/**
 * 该接口登录才能访问
 * 分页获取当前登录用户声音列表
 *
 * @param page           页码
 * @param limit          页大小
 * @param trackInfoQuery 查询条件
 * @return 分页对象
 */
@GuiGuLogin
@Operation(summary = "分页获取当前登录用户声音列表")
@PostMapping("/trackInfo/findUserTrackPage/{page}/{limit}")
public Result<Page<TrackListVo>> getUserTrackPage(@PathVariable int page, @PathVariable int limit, @RequestBody TrackInfoQuery trackInfoQuery){//...}
    
/**
 * 该接口登录才可访问
 * 修改声音信息
 *
 * @param id
 * @param trackInfoVo
 * @return
 */
@GuiGuLogin
@Operation(summary = "修改声音信息")
@PutMapping("/trackInfo/updateTrackInfo/{id}")
public Result updateTrackInfo(@PathVariable Long id, @RequestBody @Validated TrackInfoVo trackInfoVo){//...}
    
    
/**
 * 该接口登录才可访问
 * 根据ID删除声音
 * @param id
 * @return
 */
@GuiGuLogin
@Operation(summary = "根据ID删除声音")
@DeleteMapping("/trackInfo/removeTrackInfo/{id}")
public Result removeTrackInfo(@PathVariable Long id){//...}

2、用户登录

2.1 微信登录

小程序登录 | 微信开放文档 (qq.com)

说明

  1. 小程序微信授权登录(学生测试使用) 测试账号申请入口https://mp.weixin.qq.com/wxamp/sandbox?doc=1
  2. 小程序调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器(已完成)。
  3. 应用服务器端调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台账号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台账号) 和 会话密钥 session_key

之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。

注意事项

  1. 会话密钥 session_key 是对用户数据进行 加密签名 的密钥。为了应用自身的数据安全,开发者服务器不应该把会话密钥下发到小程序,也不应该对外提供这个密钥
  2. 临时登录凭证 code 只能使用一次

这里我们采用微信开发 Java SDK(非官方提供)简化开发。支持微信支付、开放平台、公众号、企业号/企业微信、小程序等的后端开发,官方地址:http://wxjava.fly2you.cn/zh-CN/

  1. service-user模块中导入依赖

    <dependency>
       <groupId>com.github.binarywang</groupId>
       <artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
       <version>4.5.0</version>
    </dependency>
    
  2. 在nacos注册中心增加配置信息(已添加)

    wx:
     miniapp:
       appid: wxcc651fcbab275e33  # 小程序微信公众平台appId 改成同学申请测试号应用id
       secret: 5f353399a2eae7ff6ceda383e924c5f6  # 小程序微信公众平台api秘钥 改成同学申请测试号秘钥
       msgDataFormat: JSON
    
  3. 在IOC容器中会自动产生对象类型,可以自动注入

  • WxMaService
  • WxMaConfig
  1. 当时导入小程序,选择测试号,产生用与测试的APPID(微信用不了)

image-20231020154902093

  1. 进入微信开发者工具,编辑器->project.config.json文件修改appId: wxcc651fcbab275e33 改为自己申请的应用ID:appid

image-20231020155121005

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/41

service-user模块完成微信登录WxLoginApiController

package com.atguigu.tingshu.user.api;

import com.atguigu.tingshu.common.result.Result;
import com.atguigu.tingshu.user.service.UserInfoService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@Tag(name = "微信授权登录接口")
@RestController
@RequestMapping("/api/user/wxLogin")
@Slf4j
public class WxLoginApiController {

    @Autowired
    private UserInfoService userInfoService;


    /**
     * 微信小程序登录,将微信账户openId跟本地用户关联,返回自定义登陆态(token令牌)
     * @param code 小程序端提交临时凭据,用于服务器端获取微信openId
     * @return {"token":"UUID令牌"}
     */
    @Operation(summary = "微信小程序登录,将微信账户openId跟本地用户关联,返回自定义登陆态")
    @GetMapping("/wxLogin/{code}")
    public Result<Map<String, String>> wxLogin(@PathVariable String code){
        Map<String, String> map = userInfoService.wxLogin(code);
        return Result.ok(map);
    }
}

UserInfoService

package com.atguigu.tingshu.user.service;

import com.atguigu.tingshu.model.user.UserInfo;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.Map;

public interface UserInfoService extends IService<UserInfo> {

    /**
     * 微信小程序登录,将微信账户openId跟本地用户关联,返回自定义登陆态(token令牌)
     * @param code 小程序端提交临时凭据,用于服务器端获取微信openId
     * @return {"token":"UUID令牌"}
     */
    Map<String, String> wxLogin(String code);
}

UserInfoServiceImpl

package com.atguigu.tingshu.user.service.impl;

import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.WxMaJscode2SessionResult;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.IdUtil;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import com.atguigu.tingshu.common.constant.RedisConstant;
import com.atguigu.tingshu.common.service.KafkaService;
import com.atguigu.tingshu.model.user.UserInfo;
import com.atguigu.tingshu.user.mapper.UserInfoMapper;
import com.atguigu.tingshu.user.service.UserInfoService;
import com.atguigu.tingshu.vo.user.UserInfoVo;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

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

@Slf4j
@Service
@SuppressWarnings({"all"})
public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> implements UserInfoService {

    @Autowired
    private UserInfoMapper userInfoMapper;


    @Autowired
    private WxMaService wxMaService;


    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private KafkaService kafkaService;


    /**
     * 微信小程序登录,将微信账户openId跟本地用户关联,返回自定义登陆态(token令牌)
     *
     * @param code 小程序端提交临时凭据,用于服务器端获取微信openId
     * @return {"token":"UUID令牌"}
     */
    @Override
    public Map<String, String> wxLogin(String code) {
        try {
            //1.获取当前登录微信账户唯一标识:wxOpenId
            WxMaJscode2SessionResult sessionInfo = wxMaService.getUserService().getSessionInfo(code);
            String openid = sessionInfo.getOpenid();

            //2.判断当前微信是否为初次登录小程序
            LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(UserInfo::getWxOpenId, openid);
            UserInfo userInfo = userInfoMapper.selectOne(queryWrapper);

            //3.初次登录,初始化用户记录以及账户记录(余额)
            if (userInfo == null) {
                //3.1 保存用户记录-将微信用户openID关联自定义用户
                userInfo = new UserInfo();
                userInfo.setWxOpenId(openid);
                userInfo.setNickname("听友"+IdUtil.getSnowflakeNextId());
                userInfo.setAvatarUrl("https://oss.aliyuncs.com/aliyun_id_photo_bucket/default_handsome.jpg");
                userInfo.setIsVip(0);
                userInfo.setVipExpireTime(null);
                userInfoMapper.insert(userInfo);
                //3.2 发送Kafka消息 消息:用户ID
                kafkaService.sendKafkaMessage(KafkaConstant.QUEUE_USER_REGISTER, userInfo.getId().toString());
            }
            //4.产生登录令牌(自定义登陆态)基于用户信息跟token进行关联
            //4.1 生成自定义token令牌
            String token = IdUtil.randomUUID();
            //4.2 构建登录key 形式:user:login:token
            String loginKey = RedisConstant.USER_LOGIN_KEY_PREFIX + token;
            //4.3 将token作为Key 将用户信息UserInfoVo作为Value写入Redis
            UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class);
            redisTemplate.opsForValue().set(loginKey, userInfoVo, RedisConstant.USER_LOGIN_KEY_TIMEOUT, TimeUnit.SECONDS);

            //5.封装登录结果
            Map<String, String> map = new HashMap<>();
            map.put("token", token);
            return map;
        } catch (Exception e) {
            log.error("[用户服务]微信登录异常:{}", e);
            throw new RuntimeException(e);
        }
    }

}

2.2 初始化账户(余额)

kafka-util模块中定义KafkaService 工具类

package com.atguigu.tingshu.common.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.CompletableFuture;

@Service
public class KafkaService {

    private static final Logger logger = LoggerFactory.getLogger(KafkaService.class);

    @Autowired
    private KafkaTemplate kafkaTemplate;


    /**
     * 发送Kafka消息
     *
     * @param topic 话题名称
     * @param data  业务数据 StringSerializer
     */
    public void sendKafkaMessage(String topic, String data) {
        this.sendKafkaMessage(topic, null, data);
    }

    /**
     * 发送Kafka消息
     *
     * @param topic 话题名称
     * @param key   可选参数:消息Key
     * @param data  业务数据 StringSerializer
     */
    public void sendKafkaMessage(String topic, String key, String data) {
        //1.发送消息后得到异步任务
        CompletableFuture sendResultCompletableFuture = kafkaTemplate.send(topic, key, data);
        //2.获取异步任务正常回调
        //3.获取异步任务异常回调
        sendResultCompletableFuture.whenCompleteAsync((t, e) -> {
            if (e != null) {
                logger.error("[Kafka生产者]发送消息异常:{}", e);
            } else {
                logger.info("[Kafka生产者]发送消息成功,话题名称:{}, key:{}, 消息:{}", topic, key, data);
            }
        });
    }
}

service-acount 微服务中监听消息:

package com.atguigu.tingshu.account.receiver;

import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.constant.KafkaConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;

/**
 * @author: atguigu
 * @create: 2024-02-22 15:40
 */
@Slf4j
@Component
public class AccountReceiver {


    @Autowired
    private UserAccountService userAccountService;



    /**
     * 每个消费者:1.是否需要做幂等性处理  2.是否需要进行事务管理
     * 初始化账户记录
     *
     * @param consumerRecord
     */
    @KafkaListener(topics = KafkaConstant.QUEUE_USER_REGISTER)
    public void initAccount(ConsumerRecord<String, String> consumerRecord) {
        String userId = consumerRecord.value();
        if (StringUtils.isNotBlank(userId)) {
            log.info("[账户服务]消费者,监听到初始化账户消息:{}", userId);
            userAccountService.initAccount(Long.valueOf(userId));
        }
    }
}

**UserAccountService接口 **初始化账户信息:

package com.atguigu.tingshu.account.service;

import com.atguigu.tingshu.model.account.UserAccount;
import com.baomidou.mybatisplus.extension.service.IService;

import java.math.BigDecimal;

public interface UserAccountService extends IService<UserAccount> {


    /**
     * 保存账户记录
     *
     * @param userId
     */
    void initAccount(Long userId);

    /**
     * 新增账户变动日志
     * @param userId 用户ID
     * @param title 内容
     * @param tradeType 交易类型
     * @param amount 交易金额
     * @param orderNo 订单编号
     */
    void saveUserAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo);
}

UserAccountServiceImpl实现类:

package com.atguigu.tingshu.account.service.impl;

import com.atguigu.tingshu.account.mapper.UserAccountDetailMapper;
import com.atguigu.tingshu.account.mapper.UserAccountMapper;
import com.atguigu.tingshu.account.service.UserAccountService;
import com.atguigu.tingshu.common.constant.SystemConstant;
import com.atguigu.tingshu.model.account.UserAccount;
import com.atguigu.tingshu.model.account.UserAccountDetail;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Slf4j
@Service
@SuppressWarnings({"all"})
public class UserAccountServiceImpl extends ServiceImpl<UserAccountMapper, UserAccount> implements UserAccountService {

    @Autowired
    private UserAccountMapper userAccountMapper;

    @Autowired
    private UserAccountDetailMapper userAccountDetailMapper;

    /**
     * 保存账户记录
     * 业务幂等性通过数据表中:用户ID设置唯一约束来保证
     *
     * @param userId
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void initAccount(Long userId) {
        //1.新增用户账户记录
        UserAccount userAccount = new UserAccount();
        userAccount.setUserId(userId);

        BigDecimal giveAmount = new BigDecimal("10");
        userAccount.setTotalAmount(giveAmount);
        userAccount.setAvailableAmount(giveAmount);
        userAccount.setTotalIncomeAmount(giveAmount);
        userAccount.setLockAmount(new BigDecimal("0"));
        userAccount.setTotalPayAmount(new BigDecimal("0"));
        userAccountMapper.insert(userAccount);
        //2.新增账户变动日志
        this.saveUserAccountDetail(userId, "充值赠送:"+giveAmount, SystemConstant.ACCOUNT_TRADE_TYPE_DEPOSIT, giveAmount, null);
    }

    @Override
    public void saveUserAccountDetail(Long userId, String title, String tradeType, BigDecimal amount, String orderNo) {
        UserAccountDetail userAccountDetail = new UserAccountDetail();
        userAccountDetail.setUserId(userId);
        userAccountDetail.setTitle(title);
        userAccountDetail.setTradeType(tradeType);
        userAccountDetail.setAmount(amount);
        userAccountDetail.setOrderNo(orderNo);
        userAccountDetailMapper.insert(userAccountDetail);
    }
}

TIPS:以前在AuthContextHolder硬编码的用户ID可以修改为动态获取

image-20231014214718202

3、获取登录用户信息

获取用户信息 ,将查询到的信息放入这个实体类UserInfoVo中

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/43

WxLoginApiController 控制器

/**
 * 该接口必须才能访问
 * 获取当前登录用户信息
 *
 * @return
 */
@GuiGuLogin
@GetMapping("/getUserInfo")
public Result<UserInfoVo> getUserInfo() {
    //1.获取当前登录用户ID
    Long userId = AuthContextHolder.getUserId();
    //2.调用业务逻辑层获取用户信息
    UserInfoVo userInfoVo = userInfoService.getUserInfo(userId);
    return Result.ok(userInfoVo);
}

UserInfoService接口:

/**
 * 获取用户信息
 * @param userId
 * @return
 */
UserInfoVo getUserInfo(Long userId);

UserInfoServiceImpl实现类:

/**
 * 查询用户基本信息
 *
 * @param userId
 * @return
 */
@Override
public UserInfoVo getUserInfo(Long userId) {
    //1.根据主键ID查询用户对象
    UserInfo userInfo = userInfoMapper.selectById(userId);
    //2.将用户PO对象转为VO对象
    return BeanUtil.copyProperties(userInfo, UserInfoVo.class);
}

4、更新用户信息方法

需求:登录成功之后,可以修改用户基本信息。

YAPI接口地址:http://192.168.200.6:3000/project/11/interface/api/45

WxLoginApiController控制器

/**
 * 修改当前登录用户基本信息
 * @param userInfoVo
 * @return
 */
@Operation(summary = "修改当前登录用户基本信息")
@GuiGuLogin
@PostMapping("/updateUser")
public Result updateUser(@RequestBody UserInfoVo userInfoVo){
    //1.获取当前登录用户ID
    Long userId = AuthContextHolder.getUserId();
    //2.调用业务逻辑层修改用户信息
    userInfoService.updateUser(userId, userInfoVo);
    return Result.ok();
}

UserInfoService接口

/**
 * 修改用户基本信息
 * @param userId
 * @param userInfoVo
 */
void updateUser(Long userId, UserInfoVo userInfoVo);

UserInfoServiceImpl实现类

/**
 * 修改用户基本信息(限定只能修改账户昵称、头像)
 *
 * @param userId
 * @param userInfoVo
 */
@Override
public void updateUser(Long userId, UserInfoVo userInfoVo) {
    UserInfo userInfo = new UserInfo();
    userInfo.setId(userId);
    userInfo.setNickname(userInfoVo.getNickname());
    userInfo.setAvatarUrl(userInfoVo.getAvatarUrl());
    userInfoMapper.updateById(userInfo);
}