logo 范 · 拾光录
网址收集 关于作者 Github Gitee
杂文随笔5
Hexo博客:基础使用Hexo博客:Next主题Hexo博客:Next进阶使用Hexo博客:Next高级配置基于Node的WIKI管理
前端知识16
HTML常用知识CSS常用知识CSS美化checkbox复选框JavaScript常用知识JavaScript格式化时间戳JavaScript窗口宽高处理JavaScript黑夜主题切换实现方案JavaScript数字转大写简易图片查看器TypeScript基础知识Threejs基础三要素Threejs网格辅助和轨道控制器Threejs物体绘制Electron基础使用Nodejs基础知识animate.css页面动画
Vue框架19
Vite的使用及扩展Vue3父子组件Vue3使用Marked解析MarkdownMermaid图表生成库初始化页面加载动画Axios表单提交二维码解决方案NProgress加载进度条Vue3动态菜单实现Vue3使用ECharts图表Vue3处理Excel导入导出keep-alive页面缓存及setup问题Element:文件上传Element:结合Pinia实现动态菜单Element:图片上传组件Element:自定义统一弹窗组件Element:表格自定义指令控制按钮显示(鉴权)可视化大屏使用缩放适配分辨率
UniApp15
UniApp的基础使用封装网络请求工具及文件上传uni-app的开发记录微信小程序分享原生文件上传Pinia取消滚动条(兼容小程序)tabbar消息数量显示scroll-view上滑到底部加载数据状态栏高度动态设配数据共享与传递uview-plus导航栏实现背景融合Wot UIWot UI实现顶部背景图融合uni-app x
Java基础知识10
基础知识面向对象Lambda表达式常用API常用知识积累try-with-resource注解反射多线程经纬度距离计算
SpringBoot31
application配置Maven创建聚合项目全局异常处理锁机制项目启动初始化数据方式邮件功能集成原生定时任务异步集成阿里云OSS阿里OSS预签名上传基于hutool读excelJSR303WebSocketWebSocket版AI接口流式调用Smart-Doc接口文档生成器application配置信息加密雪花算法工具AOP实现请求参数脱敏思路JWT生成Token及工具类SpringBoot默认JSON与对象转换若依框架:安装使用若依框架:优化和调整文件上传若依框架:管理后台页面优化若依框架:后端接口代码优化SpringAISpringBoot实现AI接口流式调用服务启动时创建MySQL连接自建项目工程树形结构处理工具微信支付代码微信手机号登录
SpringMVC14
跨域处理拦截器RESTful风格伪前后端分离Jackson转换器调整Thymeleaf基于拦截器做权限校验AOP打印接口请求响应日志AOP打印接口请求响应耗时文件上传和回显POST请求加解密实现(AES)POST请求加解密实现(RSA+AES)参数动态校验实现方案真实IP和归属地
MyBatis8
MyBatis基本使用与配置Mapper使用相关MaBatis多数据源配置MyBatisPlus数据统计类处理方案MyBatisPlus条件查询正向工程的实现(H2)mybatis-plus-join
SpringCloud15
Netflix:微服务与搭建Netflix:服务的消费与提供Netflix:EurekaNetflix:ActuatorNetflix:RibbonNetflix:FeignNetflix:HystrixNetflix:ZuulAlibaba:简介与搭建Alibaba:Nacos注册中心Alibaba:RibbonAlibaba:OpenFeignAlibaba:Nacos配置中心Alibaba:GetewayAlibaba:Sentinel
MySQL6
MySQL基础知识MySQL多表查询与事务MySQL常用函数及解决方案MySQL视图MySQL索引安装MySQL
Redis7
Redis介绍和安装Redis配置文件Redis持久化Redis集群Redis语法基础Redis相关问题及解决方案SpringBoot集成Redis使用记录
MongoDB10
Linux安装MongoDBMongoDB基础语法MongoTemplate及SpringBoot配置MongoTemplate中Update操作MongoTemplate中聚合查询MongoTemplate日期归档示例项目使用相关知识归纳地理位置存储与距离查询MongoDB副本集与事务获取类名和属性名工具类
其他数据库1
H2数据库
Python编程6
Python基础知识Python语法yolo目标检测OpenCV的使用及树莓派平台condauv
工具集合13
IDEAMavenGradleGitNginx安装Nginx配置VSCodeJMeter压测DockerOllamaRustFSPicGoObs录制
Linux知识11
Linux常用命令Jar启动脚本VirtualBox安装CentOSVirtualBox安装Ubuntu树莓派安装及使用frp内网穿透ArchLinux:基础系统安装ArchLInux:图形化界面安装ArchLinux:常用软件ArchLinux:深度优化ArchLinux:Niri
创意设计2
Blender:入门知识UI设计基础知识
AI相关9
Claude CodeHermes AgentOpenAI基本使用OpenAI工具调用OpenAI记忆管理OpenAI推理执行OpenAI开发框架Langchainllama.cpp

数据准备

注意

在支付时会用到openid,所以需要在用户注册时就加上

请求VO

package com.fan.model.login.vo;

import jakarta.validation.constraints.NotBlank;
import lombok.Data;

@Data
public class UserLoginReqVo {

    /**
     * JsCode(开发接口wx.login获得)
     */
    @NotBlank(message = "JsCode不能为空")
    private String jsCode;

    /**
     * 动态令牌(open-type="getPhoneNumber")获得
     */
    @NotBlank(message = "动态令牌不能为空")
    private String code;

}

响应Vo

package com.fan.model.login.vo;

import lombok.Data;

@Data
public class UserLoginRespVo {

    /**
     * Token
     */
    private String token;

}

缓存Vo

package com.fan.model.login.vo;

import lombok.Data;

/**
 * 缓存用户信息
 */
@Data
public class CacheUserVo {

    /**
     * ID
     */
    private Long id;

    /**
     * 手机号
     */
    private String phone;

    /**
     * 微信openId
     */
    private String openId;

}

接口

package com.fan.model.login;

import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.fan.entity.TbUser;
import com.fan.entity.vo.ComResult;
import com.fan.enumEntity.RedisKeyEnum;
import com.fan.model.login.vo.CacheUserVo;
import com.fan.model.login.vo.UserLoginReqVo;
import com.fan.model.login.vo.UserLoginRespVo;
import com.fan.model.tbUser.TbUserService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.yulichang.query.MPJLambdaQueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

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

/**
 * 登录
 */
@Slf4j
@RestController
public class UserLoginController {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    // appId
    private final String appId = "xxxx";
    // AppSecret
    private final String secret = "xxxx";

    @Resource
    private TbUserService tbUserService;

    /**
     * 登录
     */
    @PostMapping("/login")
    public ComResult<UserLoginRespVo> login(@RequestBody @Validated UserLoginReqVo params) {
        log.info("授权请求参数:{}", params);
        // 获取微信凭证
        String wxToken = getWxToken();
        // 获取手机号
        String phone = getPhone(wxToken, params.getCode());
        // 查询用户是否已经注册
        MPJLambdaQueryWrapper<TbUser> wrapper = new MPJLambdaQueryWrapper<>();
        wrapper.selectAll(TbUser.class);
        wrapper.eq(TbUser::getPhone, phone);
        wrapper.last("limit 1");
        TbUser user = tbUserService.selectJoinOne(TbUser.class, wrapper);
        // 构建返回数据
        String token = String.valueOf(IdWorker.getId());
        UserLoginRespVo respVo = new UserLoginRespVo();
        respVo.setToken(token);
        // 缓存用户信息
        CacheUserVo cacheUserVo = new CacheUserVo();
        String cacheJson = "{}";
        if (Objects.nonNull(user)) {
            // 用户存在,缓存登录信息
            cacheUserVo.setId(user.getId());
            cacheUserVo.setPhone(user.getPhone());
            cacheUserVo.setOpenId(user.getOpenId());
        } else {
            // 用户不存在,创建用户,缓存登录信息
            // 获取openId,支付会用到
            String openId = openId(params.getJsCode());
            log.info("用户授权openId:{}", openId);
            if (Objects.nonNull(phone) && Objects.nonNull(openId)) {
                // 注册用户
                TbUser tbUser = new TbUser();
                tbUser.setPhone(phone);
                tbUser.setOpenId(openId);
                tbUser.setName("请填写姓名");
                boolean save = tbUserService.save(tbUser);
                if (save) {
                    cacheUserVo.setId(tbUser.getId());
                    cacheUserVo.setPhone(tbUser.getPhone());
                    cacheUserVo.setOpenId(tbUser.getOpenId());
                } else {
                    return ComResult.failMsg("登录失败");
                }
            }
        }
        // 将用户信息缓存到redis中
        try {
            cacheJson = new ObjectMapper().writeValueAsString(cacheUserVo);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        redisTemplate.opsForValue().set(RedisKeyEnum.KEY_APP_USER.value() + token, cacheJson, 30, TimeUnit.DAYS);
        return ComResult.data(respVo);
    }

    /**
     * 获取微信凭证
     * 小程序全局后台接口调用凭据,有效期最长为7200s,开发者需要进行妥善保存
     * 该接口调用频率限制为1万次每分钟,每天限制调用50万次
     * <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getStableAccessToken.html"/>
     */
    private String getWxToken() {
        // 优先从缓存读取
        String accessToken = redisTemplate.opsForValue().get(RedisKeyEnum.KEY_WX_ACCESS_TOKEN.value());
        if (Objects.isNull(accessToken)) {
            // 接口地址
            String url = "https://api.weixin.qq.com/cgi-bin/stable_token";
            // 请求参数
            Map<String, String> reqMap = new HashMap<>();
            reqMap.put("grant_type", "client_credential");
            reqMap.put("appid", appId);
            reqMap.put("secret", secret);
            // 请求参数转JSON字符串
            String reqJson;
            try {
                reqJson = new ObjectMapper().writeValueAsString(reqMap);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
            log.debug("获取微信凭证请求参数:{}", reqJson);
            // REST请求
            RestTemplate restTemplate = new RestTemplate();
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.parseMediaType("application/json; charset=UTF-8"));
            headers.add("Accept", MediaType.APPLICATION_JSON.toString());
            HttpEntity<String> formEntity = new HttpEntity<>(reqJson, headers);
            String response = restTemplate.postForEntity(url, formEntity, String.class).getBody();
            log.debug("获取微信凭证返回数据:{}", response);
            if (Objects.isNull(response)) {
                throw new RuntimeException("获取微信凭证失败");
            }
            // 解析返回数据
            Map<String, Object> respData;
            try {
                respData = new ObjectMapper().readValue(response, new TypeReference<>() {
                });
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
            accessToken = respData.get("access_token").toString();
            log.debug("微信凭证:{}", accessToken);
            if (Objects.isNull(accessToken)) {
                throw new RuntimeException("解析微信凭证失败");
            }
            // 缓存
            redisTemplate.opsForValue().set(RedisKeyEnum.KEY_WX_ACCESS_TOKEN.value(), accessToken, 7000, TimeUnit.SECONDS);
        }
        return accessToken;
    }

    /**
     * 根据动态令牌获取手机号
     * 每个code只能使用一次,code的有效期为5min
     * <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-info/phone-number/getPhoneNumber.html"/>
     */
    private String getPhone(String accessToken, String code) {
        // 接口地址
        String url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" + accessToken;
        // 请求参数
        Map<String, String> reqMap = new HashMap<>();
        reqMap.put("code", code);
        // 请求参数转JSON字符串
        String reqJson;
        try {
            reqJson = new ObjectMapper().writeValueAsString(reqMap);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        log.debug("微信获取手机号请求参数:{}", reqJson);
        // REST请求
        RestTemplate restTemplate = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
        headers.setContentType(type);
        headers.add("Accept", MediaType.APPLICATION_JSON.toString());
        HttpEntity<String> formEntity = new HttpEntity<>(reqJson, headers);
        String response = restTemplate.postForEntity(url, formEntity, String.class).getBody();
        log.debug("微信获取手机号返回数据:{}", response);
        if (Objects.isNull(response)) {
            throw new RuntimeException("根据code获取手机号失败");
        }
        // 解析返回数据
        Map<String, Object> respData;
        try {
            respData = new ObjectMapper().readValue(response, new TypeReference<>() {
            });
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        log.debug("解析返回数据:{}", respData.toString());
        if (!Objects.equals(respData.get("errcode"), 0)) {
            throw new RuntimeException("根据code获取手机号失败,状态码错误");
        }
        // 手机号
        Object phoneInfo = respData.get("phone_info");
        if (Objects.isNull(phoneInfo)) {
            throw new RuntimeException("phone_info为空");
        }
        log.info("根据code获取手机号,phone_info:{}", phoneInfo);
        // 解析phone_info
        Map<String, Object> phoneInfoMap = new ObjectMapper().convertValue(phoneInfo, new TypeReference<>() {
        });
        // purePhoneNumber没有区号的手机号
        // phoneNumber用户绑定的手机号(国外手机号会有区号)
        String phoneNumber = phoneInfoMap.get("purePhoneNumber").toString();
        log.debug("根据code获取手机号:{}", phoneNumber);
        if (Objects.isNull(phoneNumber)) {
            throw new RuntimeException("根据code获取手机号无法解析");
        }
        return phoneNumber;
    }

    /**
     * 从微信获取openid
     * <a href="https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html"/>
     */
    private String openId(String code) {
        // 接口地址
        String url = "https://api.weixin.qq.com/sns/jscode2session";
        url += "?appid=" + appId;
        url += "&secret=" + secret;
        url += "&grant_type=authorization_code";
        url += "&js_code=" + code;
        log.info("微信获取openid:{}", url);
        // REST请求
        RestTemplate restTemplate = new RestTemplate();
        String response = restTemplate.getForObject(url, String.class);
        log.info("微信获取openid返回数据:{}", response);
        Map<String, Object> respData;
        try {
            respData = new ObjectMapper().readValue(response, new TypeReference<>() {
            });
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
        return respData.get("openid").toString();
    }

}

前端uniapp写法

<template>
  <scroll-view class="org-container" scroll-y enhanced :show-scrollbar="false">
    <view class="container">
      <view class="com-header">
        <wd-navbar fixed placeholder title="授权" left-arrow safeAreaInsetTop @click-left="handleClickLeft"></wd-navbar>
      </view>
      <view class="login-box">
        <view class="icon">
          <wd-icon name="user" color="#1c6b4f" size="40px"/>
        </view>
        <view class="text">
          亲爱的用户,为了给你提供完整的服务,需要您授权手机号进行数据绑定,我们承诺严格保护您的个人信息,严格遵守
          <text @click="gotoPage('/pages/userService/userService')">《用户协议》</text><text @click="gotoPage('/pages/userPrivacy/userPrivacy')">《隐私协议》</text></view>
        <button open-type="getPhoneNumber" @getphonenumber="getPhoneNumber">立即授权</button>
      </view>
    </view>
    <wd-toast />
  </scroll-view>
</template>

<script setup>
  import {
    reactive,
    ref
  } from 'vue';

  import httpUtil from "../../common/httpUtil.js"
  import {
    useToast
  } from '@/uni_modules/wot-design-uni'

  const toast = useToast()

  // 返回
  function handleClickLeft() {
    uni.navigateBack()
  }
  // 跳转
  function gotoPage(url) {
    uni.navigateTo({
      url: url
    })
  }

  // 获取手机号
  function getPhoneNumber(e) {
    // 如果拿到动态令牌
    if (e.detail.code) {
      // 进行微信开发接口的登录操作,拿到js_code
      wx.login({
        success(res) {
          if (res.code) {
            // 发起登录
            toast.loading('授权中...')
            httpUtil('/login', 'POST', {
              'jsCode': res.code,
              'code': e.detail.code
            }, (res) => {
              if (res.code == 200) {
                uni.setStorageSync('token', res.data.token)
                toast.success('登录成功')
                uni.navigateTo({
                  url: '/pages/index/index'
                })
              } else {
                toast.error(res.msg)
              }
            }, (err) => {
              toast.error('登录异常')
            })
          } else {
            toast.error('登录异常')
          }
        }
      })
    }
  }
</script>

<style lang="scss">
  .org-container {
    height: 100vh;
    overflow: auto;
  }

  .container {
    background: url('https://tombstone.oss-cn-chengdu.aliyuncs.com/app/common_bg_1.png');
    width: 100%;
    min-height: 100vh;
    background-repeat: no-repeat;
    background-size: cover;
    background-attachment: fixed;
    box-sizing: border-box;
    padding: 20rpx 32rpx 88rpx 32rpx;
  }

  // 导航
  .wd-navbar {
    background: url('https://tombstone.oss-cn-chengdu.aliyuncs.com/app/common_bg_1.png');
    background-repeat: no-repeat;
    background-size: cover;
    background-attachment: fixed;
    // border-bottom: 1px solid #B5925E;
  }

  .login-box {
    padding-top: 128rpx;
    
    .icon {
      text-align: center;
      margin-bottom: 80rpx;
    }

    .text {
      font-size: 30rpx;
      text-indent: 2em;
      line-height: 2;
      margin-bottom: 128rpx;

      text {
        color: blue;
      }
    }

    button {
      background-color: #4d80f0;
      color: #fff;
      font-size: 28rpx;
    }
  }
</style>

附用户协议

userService.vue

<!-- 用户协议 -->
<template>
  <scroll-view class="org-container" scroll-y enhanced :show-scrollbar="false">
    <!-- 如果样式设置在全局,不需要背景的不使用class="com-container" -->
    <view class="com-container">
      <!-- placeholder:固定在顶部时,是否生成一个等高元素,以防止塌陷 -->
      <up-navbar title="用户协议" :placeholder="true" autoBack>
      </up-navbar>

      <view class="container">
        <view>尊敬的用户,欢迎使用我们的小程序。为了保护您的个人信息,我们制定了本隐私政策。</view>

        <view class="title">一、信息收集和使用</view>
        <view>1. 我们可能会收集您提供的个人信息,用于向您提供小程序的相关功能和服务。</view>
        <view>2. 我们承诺在未经您同意的情况下,不会将您的个人信息提供给任何第三方。</view>

        <view class="title">二、信息保护</view>
        <view>1.我们采取合理的安全措施保护您的个人信息,防止数据的丢失、泄漏、被盗用等风险。</view>
        <view>2. 在合理的范围内,我们会采取措施保证您的个人信息的准确性和完整性。</view>
        <view>3. 您应当保证提供的个人信息真实、准确,并及时更新。</view>

        <view class="title">三、信息存储和处理</view>
        <view>1. 我们可能会将收集的个人信息存储在本小程序所在国家/地区或者其他相关法律允许的地方。</view>
        <view>2. 我们仅在为实现本隐私政策中描述的目的所必需的期间内保留您的个人信息。</view>

        <view class="title">四、未成年人信息保护</view>
        <view>我们非常重视对未成年人个人信息的保护。如果您是未满18周岁的未成年人,请在监护人的陪同下使用小程序,并请不要向我们提供任何个人信息。</view>

        <view class="title">五、协议修改和通知</view>
        <view>1. 我们有权根据需要修改本隐私政策内容,并通过小程序公告或其他方式通知您。</view>
        <view>2. 如您不同意修改后的隐私政策内容,您可以停止使用小程序。如果您继续使用小程序,则视为您接受修改后的隐私政策。</view>

        <view style="margin-top: 24rpx;">感谢您的阅读,祝您使用愉快!</view>
      </view>

    </view>
  </scroll-view>
</template>

<script setup>

</script>

<style lang="scss">
  // 背景图片
  $container-bg-url: 'https://xs-face.oss-cn-shanghai.aliyuncs.com/202601/2008440539537674241_61252.png';

  .com-container {
    background: url($container-bg-url);
    width: 100%;
    min-height: 100vh;
    background-repeat: no-repeat;
    background-size: cover;
    background-attachment: fixed;
    box-sizing: border-box;
    padding: 16rpx 32rpx 24rpx 32rpx;

    // 导航栏默认背景替换
    .u-navbar--fixed {
      background: url($container-bg-url);
      background-repeat: no-repeat;
      background-size: cover;
      background-attachment: fixed;

      .u-navbar__content,
      .u-status-bar {
        background-color: transparent !important;
      }
    }
  }


  .container {
    padding: 12rpx 0 88rpx 0;

    view {
      font-size: 28rpx;
      color: #555;
      line-height: 1.7;
    }

    .title {
      font-weight: bold;
      margin: 16rpx 0 12rpx 0;
    }
  }
</style>

附隐私政策

userPrivacy.vue

<!-- 隐私政策 -->
<template>
  <scroll-view class="org-container" scroll-y enhanced :show-scrollbar="false">
    <!-- 如果样式设置在全局,不需要背景的不使用class="com-container" -->
    <view class="com-container">
      <!-- placeholder:固定在顶部时,是否生成一个等高元素,以防止塌陷 -->
      <up-navbar title="隐私政策" :placeholder="true" autoBack>
      </up-navbar>

      <view class="container">
        <view>尊敬的用户,欢迎使用我们的小程序。为了保护您的个人信息,我们制定了本隐私政策。</view>

        <view class="title">一、信息收集和使用</view>
        <view>1. 我们可能会收集您提供的个人信息,用于向您提供小程序的相关功能和服务。</view>
        <view>2. 我们承诺在未经您同意的情况下,不会将您的个人信息提供给任何第三方。</view>

        <view class="title">二、信息保护</view>
        <view>1.我们采取合理的安全措施保护您的个人信息,防止数据的丢失、泄漏、被盗用等风险。</view>
        <view>2. 在合理的范围内,我们会采取措施保证您的个人信息的准确性和完整性。</view>
        <view>3. 您应当保证提供的个人信息真实、准确,并及时更新。</view>

        <view class="title">三、信息存储和处理</view>
        <view>1. 我们可能会将收集的个人信息存储在本小程序所在国家/地区或者其他相关法律允许的地方。</view>
        <view>2. 我们仅在为实现本隐私政策中描述的目的所必需的期间内保留您的个人信息。</view>

        <view class="title">四、未成年人信息保护</view>
        <view>我们非常重视对未成年人个人信息的保护。如果您是未满18周岁的未成年人,请在监护人的陪同下使用小程序,并请不要向我们提供任何个人信息。</view>

        <view class="title">五、协议修改和通知</view>
        <view>1. 我们有权根据需要修改本隐私政策内容,并通过小程序公告或其他方式通知您。</view>
        <view>2. 如您不同意修改后的隐私政策内容,您可以停止使用小程序。如果您继续使用小程序,则视为您接受修改后的隐私政策。</view>

        <view class="title">六、联系我们</view>
        <view>如您对本隐私政策有任何疑问或意见,请通过以下方式与我们联系:</view>
        <view>邮箱:979398409@qq.com</view>


        <view style="margin-top: 24rpx;">感谢您的阅读,祝您使用愉快!</view>
      </view>

    </view>
  </scroll-view>
</template>

<script setup>

</script>

<style lang="scss">
  // 背景图片
  $container-bg-url: 'https://xs-face.oss-cn-shanghai.aliyuncs.com/202601/2008440539537674241_61252.png';

  .com-container {
    background: url($container-bg-url);
    width: 100%;
    min-height: 100vh;
    background-repeat: no-repeat;
    background-size: cover;
    background-attachment: fixed;
    box-sizing: border-box;
    padding: 16rpx 32rpx 24rpx 32rpx;

    // 导航栏默认背景替换
    .u-navbar--fixed {
      background: url($container-bg-url);
      background-repeat: no-repeat;
      background-size: cover;
      background-attachment: fixed;

      .u-navbar__content,
      .u-status-bar {
        background-color: transparent !important;
      }
    }
  }


  .container {
    padding: 12rpx 0 88rpx 0;

    view {
      font-size: 28rpx;
      color: #555;
      line-height: 1.7;
    }

    .title {
      font-weight: bold;
      margin: 16rpx 0 12rpx 0;
    }
  }
</style>

附拦截器

package com.fan.config;

import com.fan.entity.vo.ComResult;
import com.fan.enumEntity.RedisKeyEnum;
import com.fan.model.login.vo.CacheUserVo;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.common.lang.NonNullApi;
import jakarta.annotation.Resource;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

/**
 * 请求拦截
 */
@NonNullApi
@Slf4j
public class ReqInterceptor implements HandlerInterceptor {

    @Resource
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        // 获取请求头的token
        String token = request.getHeader("token");
        if (Objects.isNull(token)) {
            deal(request, response, "未携带Token", ComResult.CODE_FAIL);
            return false;
        }
        // 从缓存获取用户数据
        String userData = redisTemplate.opsForValue().get(RedisKeyEnum.KEY_APP_USER.value() + token);
        if (Objects.isNull(userData)) {
            deal(request, response, "未登录", ComResult.CODE_UNAUTHORIZED);
            return false;
        }
        CacheUserVo user = null;
        try {
            user = new ObjectMapper().readValue(userData, CacheUserVo.class);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        if (Objects.isNull(user)) {
            deal(request, response, "账号错误", ComResult.CODE_UNAUTHORIZED);
            return false;
        }
        // 在请求中放入用户信息
        request.setAttribute("appUserId", user.getId());
        request.setAttribute("appUserPhone", user.getPhone());
        request.setAttribute("appUserOpenId", user.getOpenId());
        return true;
    }

    /**
     * 无Token或未授权响应处理
     */
    private void deal(HttpServletRequest request, HttpServletResponse response, String text, Integer code) throws IOException {
        // 没有权限不放行并设置一个状态码给前端做判断
        response.setStatus(200);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json;charset=UTF-8");
        ComResult<String> result = new ComResult<>();
        result.setCode(code);
        result.setMsg(text);
        log.info("请求未达响应:{} --- {}", request.getRequestURI(), result);
        // 使用字节流输出
        try (ServletOutputStream out = response.getOutputStream()) {
            byte[] jsonBytes = new ObjectMapper().writeValueAsString(result).getBytes(StandardCharsets.UTF_8);
            out.write(jsonBytes);
        }
    }

}

附拦截器注册

package com.fan.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 拦截器注册
 */
@Configuration
public class Interceptor implements WebMvcConfigurer {

    @Bean
    ReqInterceptor reqInterceptor() {
        return new ReqInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(reqInterceptor()).addPathPatterns("/**")
                .excludePathPatterns("/login");

    }

}

数据准备
注意
请求VO
响应Vo
缓存Vo
接口
前端uniapp写法
附用户协议
附隐私政策
附拦截器
附拦截器注册