个人运动管理平台系统开发

个人运动管理平台

1.项目分析

致力于做一款前后分离的个人健康平台,易在配合热爱运动运动和健身的朋友们开发一款能够完成卡路里统计,运动计划编写,科学食物搭配,以及方便与其他朋友交流心得的平台。项目模块:用户登录模块、运动商品模块、运动健身方案模块、食物推荐模块、用户信息管理平台、平台维护平台。

2.痛点分析

健康平台中有需要许多第三方数据,比如:运动卡路里的消耗、食物卡路里的提供、以及各种健身运动的卡路里消耗。需要调取大量的api接口,数据库设计考虑用户分级还有管理员权限设置。比如:vip用户和普通用户查看的内容区分以及功能区分。

3.项目架构

4.项目搭建

1.前后端交互(跨域处理)

config.WebConfig.java

package com.jihu.javasport.config;

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

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**") //允许访问的路径
                .allowedOriginPatterns("*")
//                .allowedOrigins("http://localhost:9000") //配置请求来源
                .allowedMethods("GET","HEAD","POST","PUT","DELETE","OPTIONS") //允许跨域访问的方法
                .allowCredentials(true) //允许存在请求头
                .maxAge(3600) //最大响应时间
                .allowedHeaders("*");
    }
}

2.后端统一结果返回

com.jihu.javasport.util.Result.java

package com.jihu.javasport.util;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result implements Serializable {
    //响应给前端是否成功的标识
    private boolean flag;
    //响应信息
    private String message;

    //响应数据
    private Object data;

    public Result(boolean flag, String message) {
        this.flag = flag;
        this.message = message;
    }

    //响应成功的结果
    public static Result success(String message,Object data){
        return new  Result(true,message,data);
    }
    //响应失败的结果
    public  static Result fail(String message){
        return  new Result(false,message);
    }

}

3.分页查询

//com.jihu.javasport.util.QueryInfo
@Data
public class QueryInfo {
    //第几页
    private Integer pageNumber;
    //一页多少条数据
    private Integer pageSize;
    //查询的内容
    private String queryString;
}

4.分页返回

//com.jihu.javasport.util.PageResult
package com.jihu.javasport.util;

import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;
import java.util.List;
//EqualsAndHashCode用来排除继承过来的data注解
@EqualsAndHashCode(callSuper = true)
@Data
public class PageResult<T> extends Result implements Serializable {
    //总记录数
    private long total;
    //分页的数据
    private List<T> rows;

    public PageResult(long total,List<T> list){
        this.setFlag(true);
        this.setMessage("分页查询成功");
        this.total =total;
        this.rows = list;
    }
}

5.axios的挂载

方法一:

utils/ajax.js
//把axios抽离出来,写到一个js文件中
import Vue from 'vue'
import  axios from 'axios'

const ajax =axios.create({
    baseURL:'http://localhost:9000'
})

Vue.prototype.$ajax=ajax;

在main.js中引用
import '@/utils/ajax'

使用方法
//后端地址为 http://localhost:9000/test
this.$ajax.get('/test').then(res=>{
        console.log(res)
      }).catch(err=>{
        console.log(err);
      })

方法二:

//在main中直接挂载

//挂载axios
Vue.prototype.$http = axios
//设置访问根路径
axios.defaults.baseURL = "http://localhost:9000"

//使用方法
this.$http.get('/test').then(res=>{...})

5.模块开发

5.1登录模块

5.1.1登录页面的编写

<template>
<div>
    <div class="form-class">
        <img class="logo" src="../assets/logo.png"/>
        <el-card>
            <el-form :model="form"  :rules="rules" ref="form" label-width="50px">
                <el-form-item label="账号" prop="username">
                    <el-input type="username" v-model="form.username" ></el-input>
                </el-form-item>
                <el-form-item label="密码" prop="password">
                    <el-input type="password" v-model="form.password" ></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submit">提交</el-button>
                    <el-button @click="reset">重置</el-button>
                </el-form-item>
            </el-form>
        </el-card>
    </div>
  </div>
</template>

<script>
    export default {
        //页面加载函数
        created(){

        },
        data(){
            return{
                //表单对象
                form:{
                    username: '',
                    password: '',
                },
                //表单效验规则
                rules:{
                    username: [
                        { required: true, message: '请输入用户名', trigger: 'blur' },
                        { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入密码', trigger: 'blur' },
                        { min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
                    ],
                    
                }
            }
        },
        methods:{
            submit(){
                this.$refs.form.validate((valid)=>{
                    if(!valid) return this.$message.error('数据校验失败,请检查后提交')
                    //如果数据效验成功,则向后端发送请求登录
                    
                })
            },
            reset(){
                //将整个表单进行重置
                this.$refs.form.resetFields();    
            }
        }
    }
</script>

<style scoped>
    .form-class{
        width:30%;
        margin: 200px 500px auto;
    }
    .logo{
        width: 100px;
        height: 100px;
    }
</style><template>
<div>
    <div class="form-class">
        <img class="logo" src="../assets/logo.png"/>
        <el-card>
            <el-form :model="form"  :rules="rules" ref="form" label-width="50px">
                <el-form-item label="账号" prop="username">
                    <el-input type="username" v-model="form.username" ></el-input>
                </el-form-item>
                <el-form-item label="密码" prop="password">
                    <el-input type="password" v-model="form.password" ></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submit">提交</el-button>
                    <el-button @click="reset">重置</el-button>
                </el-form-item>
            </el-form>
        </el-card>
    </div>
  </div>
</template>

<script>
    export default {
        //页面加载函数
        created(){

        },
        data(){
            return{
                //表单对象
                form:{
                    username: '',
                    password: '',
                },
                //表单效验规则
                rules:{
                    username: [
                        { required: true, message: '请输入用户名', trigger: 'blur' },
                        { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入密码', trigger: 'blur' },
                        { min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
                    ],
                    
                }
            }
        },
        methods:{
            submit(){
                this.$refs.form.validate((valid)=>{
                    if(!valid) return this.$message.error('数据校验失败,请检查后提交')
                    //如果数据效验成功,则向后端发送请求登录
                    
                })
            },
            reset(){
                //将整个表单进行重置
                this.$refs.form.resetFields();    
            }
        }
    }
</script>

<style scoped>
    .form-class{
        width:30%;
        margin: 200px 500px auto;
    }
    .logo{
        width: 100px;
        height: 100px;
    }
</style>

5.1.2连接数据库配置

application.yml

server:
  port: 9000

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/personalsport?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai
    username: root
    password: 123456

#配置mybatis-plus
mybatis-plus:
  #mapper接口找的xml文件
  mapper-locations: classpath:mapper/*.xml
    #扫描的实体类
  type-aliases-package: com.jihu.javasport.entity
  configuration:
    #sql日志打印
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    #开启驼峰命名
    map-underscore-to-camel-case: true

5.1.3引入SpringSecurity+登录接口编写

pom.xml
<!--        引入springsecurity-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

5.1.3.1 config.security.SecurityConfig

@Configuration
public class SecurityConfig {
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5.1.4vo.LoginVo

@Data
public class LoginVo {
    private String username;
    private String password;
}

5.1.5entity.SysUser

package com.jihu.javasport.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.Collection;

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * <p>
 * 
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Data
  @EqualsAndHashCode(callSuper = false)
    public class SysUser implements Serializable, UserDetails {

    private static final long serialVersionUID=1L;

      /**
     * 主键
     */
        @TableId(value = "id", type = IdType.AUTO)
      private Long id;

      /**
     * 登录名
     */
      private String userName;

      /**
     * 密码
     */
      private String password;

      /**
     * 昵称
     */
      private String nickName;

      /**
     * 性别(0男,1女,2未知)
     */
      private Integer sex;

      /**
     * 用户头像
     */
      private String avatar;

      /**
     * 地址
     */
      private String address;

      /**
     * 微信小程序openid,每个用户对应一个,且唯一
     */
      private String openId;

      /**
     * 状态,是否禁用,1是禁用,0不禁用
     */
      private Boolean status;

      /**
     * 是否是管理员
     */
      private Boolean admin;

      /**
     * 电话号码
     */
      private String phoneNumber;

      /**
     * 用户邮箱
     */
      private String email;

      //权限数据
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return null;
  }
//用户名
  @Override
  public String getUsername() {
    return userName;
  }
//账号是否过期
  @Override
  public boolean isAccountNonExpired() {
    return false;
  }
//账号是否被锁定
  @Override
  public boolean isAccountNonLocked() {
    return false;
  }
//当前账号证书(密码)是否过期
  @Override
  public boolean isCredentialsNonExpired() {
    return false;
  }
//是否被禁用
  @Override
  public boolean isEnabled() {
    return status;
  }
}

5.1.6 controller.LoginController

package com.jihu.javasport.controller;

import com.jihu.javasport.service.SysUserService;
import com.jihu.javasport.util.Result;
import com.jihu.javasport.vo.LoginVo;
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.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/*
 登录 退出
 获取当前登录用户的基本信息
 */
@RestController
@RequestMapping("/user")
public class LoginController {
    @Autowired
    private  SysUserService sysUserService;

    @PostMapping("/login")
    public Result  login( LoginVo loginVo){
        return sysUserService.login(loginVo);
    }
}

5.1.7 SysUserService

package com.jihu.javasport.service;

import com.jihu.javasport.entity.SysUser;
import com.baomidou.mybatisplus.extension.service.IService;
import com.jihu.javasport.util.Result;
import com.jihu.javasport.vo.LoginVo;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysUserService extends IService<SysUser> {

    //获取所有的用户信息
    Result findAll();

    /**
     * 登录接口
     * @param loginVo 登录参数: 账号和密码
     * @return 返回token,用token去获取资源
     */
    Result login(LoginVo loginVo);
}

5.1.8 SysUserServiceImpl

package com.jihu.javasport.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihu.javasport.config.security.service.UserDetailServicelmpl;
import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.mapper.SysUserMapper;
import com.jihu.javasport.service.SysUserService;
import com.jihu.javasport.util.Result;
import com.jihu.javasport.util.TokenUtil;
import com.jihu.javasport.vo.LoginVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Service
@Slf4j //打印日志
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private UserDetailServicelmpl userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TokenUtil tokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    public Result findAll() {
        log.info("获取用户信息");
        return Result.success("获取用户信息成功",sysUserMapper.findAll());
    }

    @Override
    public Result login(LoginVo loginVo) {
        log.info("1.开始登录");
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginVo.getUsername());
        log.info("2.判断用户是否存在,密码是否正确");
        if (userDetails == null || !passwordEncoder.matches(loginVo.getPassword(),userDetails.getPassword())){
            return Result.fail("账号或密码错误,请重新输入!");
        }
        if (!userDetails.isEnabled()){
            return Result.fail("该账号已禁用,请联系管理员!");
        }
        log.info("登录成功,在security对象中存入登陆的信息");
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        log.info("根据登录信息获取token");
        //需要借助jwt来生成token
        String token = tokenUtil.generateToken(userDetails);
        Map<String,String> map = new HashMap<>(2);
        map.put("tokenHead",tokenHead);
        map.put("token",token);
        return Result.success("登录成功",map);
    }

    @Override
    public SysUser findByUsername(String username) {
        return sysUserMapper.findByUsername(username);
    }
}

5.1.9 token工具类编写

com.jihu.javasport.util.TokenUtil

package com.jihu.javasport.util;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * Token 工具类,用于生成token
 * 用户登录拿到Token然后然后访问我们的系统资源
 */
@Component
public class TokenUtil {

    @Value("${jwt.secret}")
    private String secret;

    @Value("${jwt.expiration}")
    private long expiration;

    /**
     * 收入用户登录信息,生成token
     * @param details
     * @return
     */
    public String generateToken(UserDetails  details){
        Map<String,Object> map = new HashMap<>(2);
        map.put("username",details.getUsername());
        map.put("created",new Date());
        return generateJwt(map);
    }

    /**
     * 根据荷载信息去生成token
     * @param map
     * @return
     */
    private String generateJwt(Map<String,Object> map){
       return Jwts.builder()
                .setClaims(map)
                .signWith(SignatureAlgorithm.HS512,secret)
                .setExpiration(new Date(System.currentTimeMillis()+expiration*1000))
                .compact();
    }

    /**
     * 根据token获取载荷信息
     * @param token
     * @return
     */
    public Claims getTokenBody(String token){
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        }catch (Exception e){
            return null;
        }
    }

    /**
     * 根据token得到用户名
     * @param token
     * @return
     */
    public String getUsernameByToken(String token){
        return (String) getTokenBody(token).get("username");
    }

    /**
     * 根据token判断当前时间内,该token是否过期
     * @param token
     * @return
     */
    public boolean isExpiration(String token){
        return getTokenBody(token).getExpiration().before(new Date());
    }

    /**
     * 刷新token令牌
     * @param token
     * @return
     */
    public String refreshToken(String token){
        Claims claims = getTokenBody(token);
        claims.setExpiration(new Date());
        return generateJwt(claims);
    }

}

5.1.10 自定义登录逻辑编写

com.jihu.javasport.config.security.SecurityConfig

/**
 * 权限基本配置
 */
@Configuration
@EnableWebSecurity
//为了让全局能使用其方法
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SysUserService sysUserService;
    //重写userDetailsService方法
    @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        return username -> {
            SysUser user = sysUserService.findByUsername(username);
            if (user != null){
                return user;
            }
            throw new UsernameNotFoundException("用户名或密码错误!");
        };
    }

    //一般用来配置白名单
    //白名单:可以没有权限也可以访问的资源
    @Override
    public void configure(WebSecurity web) throws Exception {
        super.configure(web);
    }
    //Security的核心配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5.1.11 SysUserMapper

public interface SysUserMapper extends BaseMapper<SysUser> {

    List<SysUser> findAll();

    /**
     * 根据用户名获取用户对象
     * @param username
     * @return
     */
    SysUser findByUsername(String username);
}

5.1.12 SysUserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysUserMapper">
<!--用户信息结果-->
    <resultMap id="mainMap" type="com.jihu.javasport.entity.SysUser">
    <id column="id" property="id"/>
    <result column="user_name" property="userName" />
    <result column="password" property="password" />
    <result column="nick_name" property="nickName" />
    <result column="sex" property="sex" />
    <result column="avatar" property="avatar" />
    <result column="user_name" property="userName" />
    <result column="address" property="address" />
    <result column="open_id" property="openId" />
    <result column="status" property="status" />
    <result column="phone_number" property="phoneNumber" />
    <result column="admin" property="admin" />
    <result column="email" property="email" />
    <collection property="roles" ofType="com.jihu.javasport.entity.SysRole"  select="findRoles" column="id"/>
    </resultMap>
<!--    角色结果-->
    <resultMap id="roleMap" type="com.jihu.javasport.entity.SysRole">
    <id column="id" property="id"/>
    <result column="label" property="label"/>
     <result column="code" property="code"/>
     <result column="status" property="status"/>
      <collection  property="menus" ofType="com.jihu.javasport.entity.SysMenu"  select="findMenu" column="id" />
      <collection  property="permissions" ofType="com.jihu.javasport.entity.SysPermission"  select="findpermission" column="id"  />
    </resultMap>
<!--    菜单结果-->
    <resultMap id="menuMap" type="com.jihu.javasport.entity.SysMenu">
    <id column="id" property="id"/>
    <result column="path" property="path"/>
     <result column="icon" property="icon"/>
     <result column="title" property="title"/>
     <result column="component" property="component"/>
      <result column="status" property="status"/>
      <collection  property="children" ofType="com.jihu.javasport.entity.SysMenu"  select="findChildrenMenu" column="id" />
    </resultMap>
<!--    权限结果-->
     <resultMap id="permissionMap" type="com.jihu.javasport.entity.SysPermission">
    <id column="id" property="id"/>
    <result column="label" property="label"/>
     <result column="code" property="code"/>
     <result column="status" property="status"/>
    </resultMap>

    <select id="findAll" resultType="com.jihu.javasport.entity.SysUser">
    select  * from sys_user
    </select>
<!-- 获取用户信息-->
    <select id="findByUsername" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.SysUser">
     select  * from sys_user where user_name = #{username}
    </select>
<!-- 根据用户id查询角色信息   -->
    <select id="findRoles" parameterType="int" resultMap="roleMap">
        select * from sys_role where id in (select role_id from user_roles where user_id=#{id})
    </select>
<!--  根据角色id查询数据权限信息  -->
    <select id="findpermission" parameterType="int" resultMap="permissionMap" >
        select * from sys_permission where id in(select permission_id from roles_permissions where role_id =#{id})
    </select>
<!--  根据角色的id查询菜单信息  -->
    <select id="findMenu" parameterType="int" resultMap="menuMap" >
        select  * from sys_menu where id in(select menu_id from roles_menus where role_id = #{id}) and parent_id is null
    </select>
<!-- 获取子菜单信息   -->
    <select id="findChildrenMenu" parameterType="int" resultType="com.jihu.javasport.entity.SysMenu">
        select * from sys_menu where id = #{id}
    </select>
</mapper>

5.1.13 security核心配置

com.jihu.javasport.config.security.SecurityConfig

  //Security的核心配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.使用jwt,首先关闭跨域攻击
        http.csrf().disable();
        //2.关闭session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //3.请求都需要进行认证之后才能访问,除白名单以外的资源
        http.authorizeRequests().anyRequest().authenticated();
        //4.关闭缓存
        http.headers().cacheControl();
    }
 /**
     * 自定义登录逻辑的配置
     * 也即是配置到security中进行认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());
    }

    //@Bean是将其放到spring容器中
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

5.1.14 security核心配置(token过滤器)

   //Security的核心配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.使用jwt,首先关闭跨域攻击
        http.csrf().disable();
        //2.关闭session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //3.请求都需要进行认证之后才能访问,除白名单以外的资源
        http.authorizeRequests().anyRequest().authenticated();
        //4.关闭缓存
        http.headers().cacheControl();

        //5. token过滤器,效验token
        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //6.没有登录、没有权限访问资源自定义返回结果
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
    }
5.1.14.1 security.handler.JwtAccessDeniedHandler
/**
 * 没有访问权限时返回结果
 */
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        response.setStatus(403);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(Result.fail("权限不足,请联系管理员!")));
        writer.flush();
        writer.close();
    }
}

5.1.14.2 security.handler.JwtAuthenticationEntryPoint
/**
 * 当用户未登录和token过期的情况下访问资源
 */
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setStatus(401);
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json");
        PrintWriter writer = response.getWriter();
        writer.write(new ObjectMapper().writeValueAsString(Result.fail("您尚未登录,请登录后操作!")));
        writer.flush();
        writer.close();
    }
}
5.1.14.3 security.handler.JwtAuthenticationFilter
/**
 * token认证
 * 在接口访问前进行过滤
 */
@Component
public class JwtAuthenticationFilter  extends OncePerRequestFilter {

    @Autowired
    private TokenUtil tokenUtil;

    @Autowired
    private UserDetailServicelmpl userDetailsService;

    @Value("${jwt.tokenHeader}")
    private String tokenHeader;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    /**
     * 请求前获取请求头信息token
     * @param request
     * @param response
     * @param chain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
        //1.获取token
        String header = request.getHeader(tokenHeader);
        //2.判断token是否存在
        if (header != null && header.startsWith(tokenHead)){
            //1.拿到token主体
            String token = header.substring(tokenHead.length());
            //2.根据token获取用户名
            String username = tokenUtil.getUsernameByToken(token);
            //3.token存在,但是没有登录信息
            if (username != null && SecurityContextHolder.getContext().getAuthentication() == null){
                //没有登录信息,直接登录
                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                //判断token是否有效
                if (!tokenUtil.isExpiration(token) && username.equals(userDetails.getUsername())){
                    //刷新security中的用户信息
                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
                    authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);
                }
            }
        }
        //过滤器放行
        chain.doFilter(request,response);
    }
}

5.1.14.4 security.service.UserDetailServicelmpl
/**
 * 实现UserDetailsService接口,实现自定义登录逻辑
 * 重写loadUserByUsername方法
 */
@Slf4j
@Service
public class UserDetailServicelmpl  implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //在mapper中自定义登录,根据用户名获取用户信息
        log.info("loadUserByUsername启用");
        System.out.println("username: "+username);
        SysUser user = sysUserMapper.findByUsername(username);
        System.out.println("user: "+user);
        if (user == null){
            throw new RuntimeException("用户名或密码错误!");
        }
        return user;
    }
}
5.1.14.5 最终版config.SecurityConfig
/**
 * 权限基本配置
 */
@Configuration
@EnableWebSecurity
//为了让全局能使用其方法
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private UserDetailServicelmpl userDetailServicelmpl;

    @Autowired
    private JwtAuthenticationEntryPoint authenticationEntryPoint;
    @Autowired
    private JwtAccessDeniedHandler accessDeniedHandler;
    @Autowired
    private JwtAuthenticationFilter authenticationFilter;


    //重写WebSecurityConfigurerAdapter下面的userDetailsService方法
    //实现自定义登录逻辑
 /*   @Override
    @Bean
    protected UserDetailsService userDetailsService() {
        return username -> {
            SysUser user = sysUserService.findByUsername(username);
            if (user != null){
                return user;
            }
            throw new UsernameNotFoundException("用户名或密码错误!");
        };
    }*/     //要不然会报错

    //一般用来配置白名单
    //白名单:可以没有权限也可以访问的资源
    @Override
    public void configure(WebSecurity web) throws Exception {
       web.ignoring()
               .mvcMatchers("/user/login");

    }
    //Security的核心配置
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //1.使用jwt,首先关闭跨域攻击
        http.csrf().disable();
        //2.关闭session
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        //3.请求都需要进行认证之后才能访问,除白名单以外的资源
        http.authorizeRequests().anyRequest().authenticated();
        //4.关闭缓存
        http.headers().cacheControl();

        //5. token过滤器,效验token
        http.addFilterBefore(authenticationFilter, UsernamePasswordAuthenticationFilter.class);
        //6.没有登录、没有权限访问资源自定义返回结果
        http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);
    }

    /**
     * 自定义登录逻辑的配置
     * 也即是配置到security中进行认证
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailServicelmpl).passwordEncoder(passwordEncoder());
    }

    //@Bean是将其放到spring容器中
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

5.1.15 全局异常处理

com.jihu.javasport.config.exception.GlobleException

@Slf4j
@RestControllerAdvice
public class GlobleException {

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = RuntimeException.class)
    public Result  exception(RuntimeException e){
        log.info("系统运行异常--->{}",e.getMessage());
        return Result.fail(e.getMessage());
    }
}

5.1.16登录接口测试

5.1.17 权限不足情况测试

com.jihu.javasport.config.exception.GlobleException

@Slf4j
@RestControllerAdvice
public class GlobleException {

//    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    @ExceptionHandler(value = RuntimeException.class)
    public Result  exception(RuntimeException e){
        e.printStackTrace(); //打印出错的异常
        log.error("系统运行异常--->{}",e.getMessage());
        return Result.fail(e.getMessage());
    }

    @ResponseStatus(HttpStatus.FORBIDDEN)
    @ExceptionHandler(value = AccessDeniedException.class)
    public Result exception(AccessDeniedException e){
        log.error("权限不足--->{}",e.getMessage());
        return Result.fail("权限不足,请联系管理员!");
    }
}

com.jihu.javasport.entity.SysUser

//权限数据
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> list = new ArrayList<>();
    roles.forEach(item ->list.add(new SimpleGrantedAuthority("ROLE_" + item.getCode())));
//    list.add(new SimpleGrantedAuthority("ROLE_admin"));
    return list;
  }

5.1.18 整合swagger2接口文档

需要的依赖

   <!--  swagger2      -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--  swagger ui      -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
    <!--  swagger可视化文档      -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>2.0.4</version>
        </dependency>

com.jihu.javasport.config.SwaggerConfig

package com.jihu.javasport.config;

import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.ArrayList;
import java.util.List;

/**
 * 接口文档配置类
 */
@Configuration
@EnableSwagger2
@EnableKnife4j //开启swagger可视化
public class SwaggerConfig {
    /**
     * 创建接口文档
     * @return
     */
    @Bean
    public Docket createApi(){
        return  new Docket(DocumentationType.SWAGGER_2)
                    .useDefaultResponseMessages(false)
                    .apiInfo(apiInfo())
                    .select()
                    .apis(RequestHandlerSelectors.basePackage("com.jihu.javasport.controller"))
                    .paths(PathSelectors.any())
                    .build()
                    .securitySchemes(securitySchemes())
                    .securityContexts(securityContexts());
    }

    /**
     * 设置文档信息
     * @return
     */
    private ApiInfo apiInfo(){
        return  new ApiInfoBuilder()
                .title("个人运动管理平台")
                .version("1.0.0")
                .contact(new Contact("yin","http://localhost:9000/doc.html","[email protected]"))
                .description("个人运动管理平台接口文档")
                .build();
    }

    /**
     * 设置请求的信息
     * @return
     */
    private List<ApiKey> securitySchemes(){
        List<ApiKey> list = new ArrayList<>();
        ApiKey apiKey = new ApiKey("Authorization","Authorization","Header");
        list.add(apiKey);
        return list;
    }

    /**
     * 配置security对swagger测试的权限
     * @return
     */
    private List<SecurityContext> securityContexts(){
        List<SecurityContext> list = new ArrayList<>();
        list.add(getSecurityContext());
        return list;
    }

    /**
     * 得到授权路径
     * @return
     */
    private SecurityContext getSecurityContext(){
        return  SecurityContext
                .builder()
                .securityReferences(securityReferences())
                .forPaths(PathSelectors.regex("hello/.*"))
                .build();
    }

    /**
     * 给授权 swagger,可以进行接口测试
     * @return
     */
    private List<SecurityReference> securityReferences(){
        List<SecurityReference> list = new ArrayList<>();
        //授权范围  全局
        AuthorizationScope scope = new AuthorizationScope("globle","accessEverything");
        AuthorizationScope[] scopes = new AuthorizationScope[1];
        scopes[0]=scope;
        list.add(new SecurityReference("Authorization",scopes));
        return  list;
    }
}

5.1.19 白名单

com.jihu.javasport.config.security.contents.SecurityContents

package com.jihu.javasport.config.security.contents;

/**
 * 白名单
 */
public class SecurityContents {
    public static final String[] WHITE_LIST ={
         //后端登录接口
        "/user/login",

         //swagger相关
        "/swagger-ui.html",
        "/doc.html",
        "/webjars/**",
        "swagger-resources/**",
        "v2/**",
        "configuration/ui",
        "configuration/security",
    };
}

com.jihu.javasport.config.security.SecurityConfig

    //一般用来配置白名单
    //白名单:可以没有权限也可以访问的资源
    @Override
    public void configure(WebSecurity web) throws Exception {
       web.ignoring()
                 .mvcMatchers(SecurityContents.WHITE_LIST);
//               .mvcMatchers("/user/login");

    }

com.jihu.javasport.entity.SysUser

      //权限数据
  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> list = new ArrayList<>();
    roles.forEach(item ->{
      list.add(new SimpleGrantedAuthority("ROLE_"+ item.getCode()));
    });

//    list.add(new SimpleGrantedAuthority("ROLE_admin"));
    return list;

5.1.20 获取用户基本信息 + 退出登录接口

com.jihu.javasport.util.SecurityUtil

package com.jihu.javasport.util;

import com.jihu.javasport.entity.SysUser;
import org.springframework.security.core.context.SecurityContextHolder;

/**
 * 用于获取当前登录用户的基本信息
 */
public class SecurityUtil {
    /**
     * 从Security主体信息中获取用户信息
     * @return
     */
    public static SysUser getUser(){
        return (SysUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
    }

    /**
     * 从Security主体信息中获取用户名
     * @return
     */
    public static String getUserName(){
        return getUser().getUsername();
    }

    /**
     * 从Security主体信息中获取用户id
     * @return
     */
    public static Long getUserId(){
        return getUser().getId();
    }

}

com.jihu.javasport.controller.LoginController

   @ApiOperation(value = "获取用户基本信息")
    @GetMapping("/getInfo")
    public Result getUserInfo(Principal principal){
        if (principal == null){
            return  Result.fail("请登录!");
        }
        return Result.success("获取用户信息成功", SecurityUtil.getUser());
    }

@ApiOperation(value = "用户退出登录")
    @GetMapping("/logout")
    public Result logout(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null){
            SecurityContextHolder.getContext().setAuthentication(null);
        }
        return  Result.success("退出成功");
    }

5.1.21 前端登录-页面跳转+响应拦截

vue-sport\src\utils\ajax.js

import Vue from 'vue'
import  axios from 'axios'
import {Message} from 'element-ui'

const ajax =axios.create({
    baseURL:'http://localhost:9000'
});

ajax.interceptors.response.use((res)=>{
    // console.log(res);
    if(!res.data.flag){
        Message.error(res.data.message)
    }
    return res;
},(err)=>{
    console.log("异常",err.response);
    //三个等号:  绝对等于 0=0 0!='0'  两个等: 0=0 0='0'
    if(err.response.status === 400){
        Message.error(err.response.data.message);
    }else if(err.response.status === 401){
        Message.error("您未登录,请登录后操作!");
    }else if(err.response.status === 403){
        Message.error(err.response.data.message);
    }else if(err.response.status === 404){
        Message.error("后端接口未找到!");
    }else if(err.response.status === 500){
        Message.error("后端异常-->"+err.response.data.message);
    }else{
        Message.error("未知错误!");
    }
    

})

Vue.prototype.$ajax=ajax;

vue-sport\src\views\login.vue

<template>
<div>
    <div class="form-class">
        <img class="logo" src="../assets/logo.png"/>
        <el-card>
            <el-form :model="form"  :rules="rules" ref="form" label-width="50px">
                <el-form-item label="账号" prop="username">
                    <el-input type="username" v-model="form.username" ></el-input>
                </el-form-item>
                <el-form-item label="密码" prop="password">
                    <el-input type="password" v-model="form.password" ></el-input>
                </el-form-item>
                <el-form-item>
                    <el-button type="primary" @click="submit">提交</el-button>
                    <el-button @click="reset">重置</el-button>
                </el-form-item>
            </el-form>
        </el-card>
    </div>
  </div>
</template>

<script>
    export default {
        //页面加载函数
        created(){

        },
        data(){
            return{
                //表单对象
                form:{
                    username: '',
                    password: '',
                },
                //表单效验规则
                rules:{
                    username: [
                        { required: true, message: '请输入用户名', trigger: 'blur' },
                        { min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入密码', trigger: 'blur' },
                        { min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
                    ],
                    
                }
            }
        },
        methods:{
            // 登录的方法
            submit(){
                this.$refs.form.validate((valid)=>{
                    if(!valid) return this.$message.error('数据校验失败,请检查后提交')
                    //如果数据效验成功,则向后端发送请求登录
                    this.$ajax.post('/user/login',this.form).then(res=>{
                        console.log(res);
                        const tokenBody = res.data.data;
                        let tokenHead = tokenBody.tokenHead;
                        let token = tokenBody.token;
                        //将token存储到store中
                        this.$store.commit('setToken', tokenHead+token);
                        this.$router.push("/home")
                    })
                })
            },
            reset(){
                //将整个表单进行重置
                this.$refs.form.resetFields();    
            }
        }
    }
</script>

<style scoped>
    .form-class{
        width:30%;
        margin: 200px 500px auto;
    }
    .logo{
        width: 100px;
        height: 100px;
    }
</style>

vue-sport\src\views\Home.vue

<template>
    <div>
        主页面
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style  scoped>

</style>

5.1.22 主页面(home)编写

<template>
    <!-- 主容器 -->
    <el-container class="main-class">
        <!-- 头部信息 -->
        <el-header >
            <el-row style="height: 100%;">
                <!-- 头像 -->
                <el-col :span="2" style="height: 100%;">
                    <el-avatar :size="60" src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"></el-avatar>
                </el-col>
                <el-col :span="15" class="title">
                    个人运动管理平台<span>--管理员({{ name }})</span>
                </el-col>
                <!-- 退出按钮 -->
                <el-col :span="7" class="logout">
                   <el-button type="info">推出登录 </el-button>
                </el-col>
            </el-row>
        </el-header>
        <el-container>
            <!-- 侧边栏 -->
            <el-aside :width="menuActive ? '200px':'60px'">
                <!-- 折叠按钮 -->
                <div class="menu-button" @click="menuActive=!menuActive">
                    <i class="el-icon-s-fold"></i>
                </div>
                <!-- 侧边栏 -->
                <el-menu
                default-active="2"
                class="el-menu-vertical-demo"
                @open="handleOpen"
                @close="handleClose"
                background-color="#545c64"
                text-color="#fff"
                :collapse="!menuActive"
                active-text-color="#ffd04b">
                <el-submenu index="1">
                    <template slot="title">
                    <i class="el-icon-location"></i>
                    <span>导航一</span>
                    </template>
                    <el-menu-item index="1-1">选项1</el-menu-item>
                    <el-menu-item index="1-2">选项2</el-menu-item>
                    <el-menu-item index="1-3">选项3</el-menu-item>
                </el-submenu>
                <el-menu-item index="2">
                    <i class="el-icon-menu"></i>
                    <span slot="title">导航二</span>
                </el-menu-item>
                <el-menu-item index="3" >
                    <i class="el-icon-document"></i>
                    <span slot="title">导航三</span>
                </el-menu-item>
                <el-menu-item index="4">
                    <i class="el-icon-setting"></i>
                    <span slot="title">导航四</span>
                </el-menu-item>
                </el-menu>
            </el-aside>
            <!-- 主体 -->
            <el-main>
                <el-breadcrumb separator="/">
                    <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
                    <el-breadcrumb-item><a href="/">活动管理</a></el-breadcrumb-item>
                    <el-breadcrumb-item>活动列表</el-breadcrumb-item>
                    <el-breadcrumb-item>活动详情</el-breadcrumb-item>
                </el-breadcrumb>
                <span class="main-title">欢迎来到个人运动管理平台!</span>
            </el-main>
        </el-container>
    </el-container>

</template>

<script>
    export default {
        data(){
            return{
                name:'admin',
                menuActive:true,
            }
        },
        method:{
            //  menuActive()=()=>{

            // }
        }
    }
</script>

<style  scoped>
/* scoped受保护的样式,当前style标签下的样式只在当前组件生效,其他组件无法使用,避免样式污染 */
  .el-header {
    background-color: #B3C0D1;
    color: #333;
    text-align: center;
    line-height: 60px;
  }
  
  .el-aside {
    background-color: #D3DCE6;
    color: #333;
    text-align: center;
    /* line-height: 200px; */
  }
  
  .el-main {
    background-color: #E9EEF3;
    color: #333;
    text-align: center;
    /* line-height: 160px; */
  }

  /* 主容器样式编写 */
.main-class{
    height: 100%;
    
  }
.title{
    text-align:left;
    font-size: 25px;
    font-family: 华文行楷;
}
.logout{
    text-align: right;
}
.menu-button{
    font-size: 20px;
    color: #fff; 
    cursor: pointer;
    background-color:#2e363f;
}

.main-title{
    display: block;
    margin-top: 100px;
    font-size: 50px;
    font-family:华文行楷;
    color: #fcc400;
}

</style>

5.1.23 前端路由守卫

vue-sport\src\router\index.js

/**
 * 路由守卫
 * to:将要去到哪个页面
 * from:从哪个页面过来
 * next:放行到哪个页面
 */
router.beforeEach((to,from,next)=>{
  //判断用户是否登录
  const token= sessionStorage.getItem('token')
  if(!token){
    if(to.path === '/login'){
      next();
    }else{
      // next("/login");
      next(`/login?redirect=${to.fullPath}`)
    }
  }else{
    //判断vuex中是否存在用户基本信息
    // console.log(store);
    if(!store.state.roles || store.state.roles.length < 1){
      //向后端发送请求,获取用户的基本信息
      ajax.get('/user/getInfo').then((res)=>{
        // console.log('用户基本信息 ',res);
        //得到用户信息
        const user = res.data.data;
        store.commit('setName',user.username);
        store.commit('setAvatar',user.avatar);
        if(user.roles.length >0){
          //添加角色、菜单、权限等信息
          store.commit('setRoles',user.roles)
          store.commit('setMenus',user.menus)
          store.commit('setPermissions',user.permissions)
        }

      });
    }

    //已经登录
    // next();
    if(to.path === '/login'){
      next('/');
    }else{
      next();
    }
  }
})

5.1.24 获取用户基本信息(前端代理+请求拦截)

1. vue-sport\vue.config.js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,
  lintOnSave:false,
  //可以用来修改前端 端口号
  devServer:{
//自己设置一个代理服务器
    port:8888,
// proxy: {
//   //代理的根路径
//   // [process.env.VUE_APP_BASE_URL] : {
//   //     //代理的后端路径
//   //     target: 'http://localhost:9000',
//   //     //是否开启根路径转换  123
//   //     changeOrigin: true,
//   //     //代理路径更改
//   //     pathRewrite: {
//   //         ['^' + process.env.VUE_APP_BASE_URL]: '/'
//   //     }
//   // }
  
// }

 //设置代理
//  proxy:{
//   '/':{
//     // target用于配置你允许访问数据的计算机名称,即是你的api接口的服务器地址
//     target: 'http://localhost:9000',
//     ws:true,//启用webSocket
//     changeOrigin:true,//开启代理跨域
//     pathRewrite:{
//       '^/':'/'
//     }
//   }
// },

    // // port:8888
    //设置是否启动时打开浏览器
    // open:true,
    proxy:{
      //代理的跟路径
      '/': {
        //代理的后端路径http://localhost:9000/
        target:`http://localhost:9000`,
        // target:'http://localhost:9000',
        //是否开启根路径转换
        changeOrigin:true,
        //代理websocked
        ws:false,
        //代理路径更改
        pathReWrite:{
          '^/':'/'
        }
      }
    }



  }
})

2. vue-sport\src\utils\ajax.js
/**
 * 请求拦截器
 */
ajax.interceptors.request.use((config)=>{
    console.log('请求:',config);
    const token = sessionStorage.getItem('token');
    if(token){
        //给请求头加上token
        config.headers['Authorization'] = token;
    }

    return config;
},(err)=>{
    console.log("请求异常",err);
})

3. vue-sport\src\views\Home.vue
<script>
import {mapState} from 'vuex'
    export default {
        //vue计算属性
        computed:{
            ...mapState(['name','avatar','menus'])
        },
        created(){
            console.log(this.name)
        },
        data(){
            return{
                // name:'admin',
                menuActive:true,
            }
        },
    }
</script>

5. vue-sport\src\store\index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: sessionStorage.getItem('token') || '',
    name:sessionStorage.getItem('name') || '',
    avatar:sessionStorage.getItem('avatar') || '',
    roles:JSON.parse(sessionStorage.getItem('roles') || '[]'),
    menus:JSON.parse(sessionStorage.getItem('menus') || '[]'),
    permissions:JSON.parse(sessionStorage.getItem('permissions') || '[]'),
  },
  mutations: {
    setToken(state,data){
      state.token = data;
      // console.log('token:'+data);
      sessionStorage.setItem('token',data);
      
    },
    setName(state,data){
      state.name = data;
      sessionStorage.setItem('name',data);
      
    },
    setAvatar(state,data){
      state.avatar = data;
      // console.log('token:'+data);
      sessionStorage.setItem('avatar',data);
      
    },
    //向vuex中存入角色信息
    setRoles(state,data){
      state.roles = data;
      // console.log('token:'+data);
      sessionStorage.setItem('roles',JSON.stringify(data));
      
    },
    ////向vuex中存入菜单信息
    setMenus(state,data){
      state.menus = data;
      // console.log('token:'+data);
      sessionStorage.setItem('menus',JSON.stringify(data));
      
    },
    //向vuex中存入权限数据,限制用户操作
    setPermissions(state,data){
      state.permissions = data;
      // console.log('token:'+data);
      sessionStorage.setItem('permissions',JSON.stringify(data));
      
    },
  },
  actions: {
  },
  modules: {
  }
})

6 .vue-sport\src\utils\ajax.js
import Vue from 'vue'
import  axios from 'axios'
import {Message} from 'element-ui'
import router from '@/router';

/**
 * 创建ajax实例
 * 并设置请求超时时间
 */
const ajax =axios.create({
    // baseURL:'http://localhost:9000',
    // baseURL: process.env.VUE_APP_BASE_URL,
    // baseURL:'/',
    timeout:100000
});

/**
 * 请求拦截器
 */
ajax.interceptors.request.use((config)=>{
    console.log('请求:',config);
    const token = sessionStorage.getItem('token');
    if(token){
        //给请求头加上token
        config.headers['Authorization'] = token;
    }

    return config;
},(err)=>{
    console.log("请求异常",err);
})

/**
 * 响应拦截器
 * 即后端响应的信息
 */
ajax.interceptors.response.use((res)=>{
    // console.log(res);
    if(!res.data.flag){
        Message.error(res.data.message)
    }
    return res;
},(err)=>{
    console.log("异常",err.response);
    //三个等号:  绝对等于 0=0 0!='0'  两个等: 0=0 0='0'
    if(err.response.status === 400){
        Message.error(err.response.data.message);
    }else if(err.response.status === 401){
        Message.error("您未登录,请登录后操作!");
        //清空本地缓存
        sessionStorage.clear();
        //跳转到登录页面
        router.replace("/login")
    }else if(err.response.status === 403){
        Message.error(err.response.data.message);
    }else if(err.response.status === 404){
        Message.error("后端接口未找到!");
    }else if(err.response.status === 500){
        Message.error("后端异常-->"+err.response.data.message);
    }else{
        Message.error("未知错误!");
    }
    

})

Vue.prototype.$ajax=ajax;

//暴露ajax, 要不然别的js中访问不到
export default ajax
7. vue-sport\src\store\index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: sessionStorage.getItem('token') || '',
    name:sessionStorage.getItem('name') || '',
    avatar:sessionStorage.getItem('avatar') || '',
    roles:JSON.parse(sessionStorage.getItem('roles') || '[]'),
    menus:JSON.parse(sessionStorage.getItem('menus') || '[]'),
    permissions:JSON.parse(sessionStorage.getItem('permissions') || '[]'),
  },
  mutations: {
    setToken(state,data){
      state.token = data;
      // console.log('token:'+data);
      sessionStorage.setItem('token',data);
      
    },
    setName(state,data){
      state.name = data;
      sessionStorage.setItem('name',data);
      
    },
    setAvatar(state,data){
      state.avatar = data;
      // console.log('token:'+data);
      sessionStorage.setItem('avatar',data);
      
    },
    //向vuex中存入角色信息
    setRoles(state,data){
      state.roles = data;
      // console.log('token:'+data);
      sessionStorage.setItem('roles',JSON.stringify(data));
      
    },
    ////向vuex中存入菜单信息
    setMenus(state,data){
      state.menus = data;
      // console.log('token:'+data);
      sessionStorage.setItem('menus',JSON.stringify(data));
      
    },
    //向vuex中存入权限数据,限制用户操作
    setPermissions(state,data){
      state.permissions = data;
      // console.log('token:'+data);
      sessionStorage.setItem('permissions',JSON.stringify(data));
      
    },
  },
  actions: {
  },
  modules: {
  }
})
vue-sport\src\store\index.js
8.修改service.UserDetailServicelmpl
package com.jihu.javasport.config.security.service;

import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.mapper.SysUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 实现UserDetailsService接口,实现自定义登录逻辑
 * 重写loadUserByUsername方法
 */
@Slf4j
@Service
public class UserDetailServicelmpl  implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //在mapper中自定义登录,根据用户名获取用户信息
        log.info("loadUserByUsername启用");
        System.out.println("username: "+username);
        SysUser user = sysUserMapper.findByUsername(username);

        if (user == null){
            throw new UsernameNotFoundException("用户名或密码错误!");
        }
        if (user.isAdmin()){
            List<SysRole> list = new ArrayList<>();
            SysRole role = new SysRole();
            role.setCode("admin");
            list.add(role);
            user.setRoles(list);
            user.setMenus(sysUserMapper.findMenus(null));
        }else{
            //非管理员需要查询角色信息
            user.setRoles(sysUserMapper.findRoles(user.getId()));
            user.setMenus(sysUserMapper.findMenus(user.getId()));
            user.setPermissions(sysUserMapper.findPermissions(user.getId()));
        }

        return user;
    }
}

9.修改mapper.SysUserMapper
    /**
     * 根据用户ID查询权限信息
     * @param userId
     * @return
     */
    List<SysRole> findRoles(@Param("userId") Long userId);

    /**
     * 根据用户ID查询菜单信息
     * @param userId
     * @return
     */
    List<SysMenu> findMenus(@Param("userId") Long userId);

    /**
     * 根据用户ID查询权限数据
     * @param userId
     * @return
     */
    List<SysPermission> findPermissions(@Param("userId") Long userId);
10.修改SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysUserMapper">
<!--用户信息结果-->
    <resultMap id="mainMap" type="com.jihu.javasport.entity.SysUser">
    <id column="id" property="id"/>
    <result column="user_name" property="userName" />
    <result column="password" property="password" />
    <result column="nick_name" property="nickName" />
    <result column="sex" property="sex" />
    <result column="avatar" property="avatar" />
    <result column="user_name" property="userName" />
    <result column="address" property="address" />
    <result column="open_id" property="openId" />
    <result column="status" property="status" />
    <result column="phone_number" property="phoneNumber" />
    <result column="admin" property="admin" />
    <result column="email" property="email" />
    </resultMap>
<!--    角色结果-->
    <resultMap id="roleMap" type="com.jihu.javasport.entity.SysRole">
    <id column="id" property="id"/>
    <result column="label" property="label"/>
     <result column="code" property="code"/>
     <result column="status" property="status"/>

    </resultMap>
<!--    菜单结果-->
    <resultMap id="menuMap" type="com.jihu.javasport.entity.SysMenu">
    <id column="id" property="id"/>
    <result column="path" property="path"/>
     <result column="icon" property="icon"/>
     <result column="title" property="title"/>
     <result column="component" property="component"/>
      <result column="status" property="status"/>
      <collection  property="children" ofType="com.jihu.javasport.entity.SysMenu"  select="findChildrenMenu" column="id" />
    </resultMap>
<!--    权限结果-->
     <resultMap id="permissionMap" type="com.jihu.javasport.entity.SysPermission">
    <id column="id" property="id"/>
    <result column="label" property="label"/>
     <result column="code" property="code"/>
     <result column="status" property="status"/>
    </resultMap>

    <select id="findAll" resultType="com.jihu.javasport.entity.SysUser">
    select  * from sys_user
    </select>
<!-- 获取用户信息-->
    <select id="findByUsername" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.SysUser">
     select  * from sys_user where user_name = #{username}
    </select>
<!-- 根据用户id查询角色信息   -->
    <select id="findRoles" parameterType="int" resultMap="roleMap">
        select * from sys_role where id in (select role_id from user_roles where user_id=#{userId})
    </select>
<!--  根据角色id查询数据权限信息  -->
    <select id="findPermissions" parameterType="int" resultMap="permissionMap" >
     select * from sys_permission
     <if test="userId != null">
      where id in
        (SELECT permission_id from roles_permissions WHERE role_id in
         (select role_id from user_roles where user_id =#{userId}) )
    </if>

<!--        select * from sys_permission where id in(select permission_id from roles_permissions where role_id =#{userId})-->
    </select>
<!--  根据角色的id查询菜单信息  -->
    <select id="findMenus" parameterType="int" resultMap="menuMap" >
    select * from sys_menu where parent_id is NULL
    <if test="userId != null ">
     and id in
        (SELECT menu_id from roles_menus WHERE role_id in
            (select role_id from user_roles where user_id =#{userId}) )
    </if>

<!--        select  * from sys_menu where id in(select menu_id from roles_menus where role_id = #{userId}) and parent_id is null-->
    </select>
<!-- 获取子菜单信息   -->
    <select id="findChildrenMenu" parameterType="int" resultType="com.jihu.javasport.entity.SysMenu">
        select * from sys_menu where parent_id = #{id}
    </select>
</mapper>

5.1.25 个人信息完结-后端(角色、菜单、权限设置)

1.修改UserDetailServicelmpl
package com.jihu.javasport.config.security.service;

import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.mapper.SysUserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 实现UserDetailsService接口,实现自定义登录逻辑
 * 重写loadUserByUsername方法
 */
@Slf4j
@Service
public class UserDetailServicelmpl  implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        //在mapper中自定义登录,根据用户名获取用户信息
        log.info("loadUserByUsername启用");
        System.out.println("username: "+username);
        SysUser user = sysUserMapper.findByUsername(username);

        if (user == null){
            throw new UsernameNotFoundException("用户名或密码错误!");
        }
        if (user.isAdmin()){
            //管理员 设置的查询角色信息
            user.setRoles(sysUserMapper.findRoles(null));
            user.setPermissions(sysUserMapper.findPermissions(null));
            //获取父级菜单
            List<SysMenu> menus =  sysUserMapper.findMenus(null);
            //获取子级菜单
            menus.forEach(item ->{
                item.setChildren(sysUserMapper.findChildrenMenu(item.getId(),null));
            });
            user.setMenus(menus);

           /* List<SysRole> list = new ArrayList<>();
            SysRole role = new SysRole();
            role.setCode("admin");
            list.add(role);
            user.setRoles(list);
            user.setMenus(sysUserMapper.findMenus(null));*/
        }else{
            //非管理员需要查询角色信息
            user.setRoles(sysUserMapper.findRoles(user.getId()));
            user.setPermissions(sysUserMapper.findPermissions(user.getId()));
            //获取父级菜单
            List<SysMenu> menus =  sysUserMapper.findMenus(user.getId());
            //获取子级菜单
            menus.forEach(item ->{
                item.setChildren(sysUserMapper.findChildrenMenu(item.getId(),user.getId()));
            });
            user.setMenus(menus);
        }

        return user;
    }
}

2.修改SysUserMapper
/**
     * 根据用户ID查询菜单信息
     * @param userId
     * @return
     */
    List<SysMenu> findMenus(@Param("userId") Long userId);

    /**
     * 根据父级ID和用户ID查询子级菜单
     * @param id 父级ID
     * @param userId 用户ID
     * @return
     */
    List<SysMenu> findChildrenMenu(@Param("id") Long id,@Param("userId") Long userId);
3.修改SysUserMapper.xml
<!--  根据角色的id查询菜单信息  -->
    <select id="findMenus" parameterType="int" resultMap="menuMap" >
    select * from sys_menu where parent_id is NULL
    <if test="userId != null ">
     and id in
        (select menu_id from roles_menus where role_id in
            (select role_id from user_roles where user_id = #{userId}) )
    </if>

<!--        select  * from sys_menu where id in(select menu_id from roles_menus where role_id = #{userId}) and parent_id is null-->
    </select>
<!-- 获取子菜单信息   -->
    <select id="findChildrenMenu" parameterType="int" resultType="com.jihu.javasport.entity.SysMenu">
        select * from sys_menu where parent_id = #{id}
         <if test="userId != null ">
             and id in (select menu_id from roles_menus where role_id in
                (select role_id from user_roles where user_id = #{userId}))
         </if>
    </select>

5.2 前端退出功能

vue-sport\src\views\Home.vue

  <!-- 退出按钮 -->
                <el-col :span="7" class="logout">
                   <el-button type="info" @click="logOut">退出登录 </el-button>
                </el-col>

methods:{
            logOut(){
                console.log("退出登录");
                this.$confirm('您将退出登录,是否继续?', '提示', {
                        confirmButtonText: '确定',
                        cancelButtonText: '取消',
                        type: 'warning'
                        }).then(() => {
                            //掉后端的退出接口
                            this.$ajax.get('/user/logout').then((res)=>{
                                //清空本地缓存
                                sessionStorage.clear();
                                //跳转到登录页面
                                this.$router.push('/login');
                                this.$message.success(res.data.message);
                            })
                        }).catch(() => { });

            }
        }

5.3菜单编写

5.3.1菜单展示

vue-sport\src\views\Home.vue

<template>
    <!-- 主容器 -->
    <el-container class="main-class">
        <!-- 头部信息 -->
        <el-header >
            <el-row style="height: 100%;">
                <!-- 头像 -->
                <el-col :span="2" style="height: 100%;">
                    <el-avatar :size="60" :src="avatar"></el-avatar>
                </el-col>
                <el-col :span="15" class="title">
                    个人运动管理平台<span>--管理员({{ name }})</span>
                </el-col>
                <!-- 退出按钮 -->
                <el-col :span="7" class="logout">
                   <el-button type="info" @click="logOut">退出登录 </el-button>
                </el-col>
            </el-row>
        </el-header>
        <el-container>
            <!-- 侧边栏 -->
            <el-aside :width="menuActive ? '200px':'60px'">
                <!-- 折叠按钮 -->
                <div class="menu-button" @click="menuActive=!menuActive">
                    <i class="el-icon-s-fold"></i>
                </div>
                <!-- 侧边栏 -->
                <!--unique-opened:是否只保持一个子菜单的展开 ,router:让菜单为路由模式,会让菜单index属性为 path 进行跳转 -->
                <el-menu
                default-active="2"
                class="el-menu-vertical-demo"
                unique-opened
                router
                background-color="#545c64"
                text-color="#fff"
                :collapse="!menuActive"
                active-text-color="#ffd04b">
                <!-- 一级菜单 -->
                <el-submenu  :index="index +''" v-for="(parentMenu,index) in menus" :key="index">
                    <template slot="title">
                        <i :class="parentMenu.icon"></i>
                        <span>{{parentMenu.title}}</span>
                    </template>
                    <el-menu-item :index="childrenMenu.path" 
                    v-for="(childrenMenu,i) in parentMenu.children" :key="i" >
                        <template slot="title">
                            <i :class="childrenMenu.icon"></i>
                            <span>{{childrenMenu.title}}</span>
                        </template>                       
                    </el-menu-item>                   
                </el-submenu>

                </el-menu>
            </el-aside>
            <!-- 主体 -->
            <el-main>
                <el-breadcrumb separator="/">
                    <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
                    <el-breadcrumb-item v-for="(item,index) in $router.currentRoute.matched" :key="index">{{item.meta.title}}</el-breadcrumb-item>
                </el-breadcrumb>
                <span v-show="this.$router.currentRoute.path === '/'" class="main-title">欢迎来到个人运动管理平台!</span>
                <!-- 作为主体的子路由 -->
                <router-view/>

            </el-main>
        </el-container>
    </el-container>

</template>

<script>
import {mapState} from 'vuex'
    export default {
        //vue计算属性
        computed:{
            ...mapState(['name','avatar','menus'])
        },
        created(){
            console.log("菜单:",this.menus)
        },
        data(){
            return{
                // name:'admin',
                menuActive:true,
            }
        },
        methods:{
            logOut(){
                console.log("退出登录");
                this.$confirm('您将退出登录,是否继续?', '提示', {
                        confirmButtonText: '确定',
                        cancelButtonText: '取消',
                        type: 'warning'
                        }).then(() => {
                            //掉后端的退出接口
                            this.$ajax.get('/user/logout').then((res)=>{
                                //清空本地缓存
                                sessionStorage.clear();
                                //跳转到登录页面
                                this.$router.push('/login');
                                this.$message.success(res.data.message);
                            })
                        }).catch(() => { });

            }
        }
    }
</script>

<style  scoped>
/* scoped受保护的样式,当前style标签下的样式只在当前组件生效,其他组件无法使用,避免样式污染 */
  .el-header {
    background-color: #B3C0D1;
    color: #333;
    text-align: center;
    line-height: 60px;
  }
  
  .el-aside {
    background-color: #D3DCE6;
    color: #333;
    text-align: center;
    /* line-height: 200px; */
  }
  
  .el-main {
    background-color: #E9EEF3;
    color: #333;
    text-align: center;
    /* line-height: 160px; */
  }

  /* 主容器样式编写 */
.main-class{
    height: 100%;
    
  }
.title{
    text-align:left;
    font-size: 25px;
    font-family: 华文行楷;
}
.logout{
    text-align: right;
}
.menu-button{
    font-size: 20px;
    color: #fff; 
    cursor: pointer;
    background-color:#2e363f;
}

.main-title{
    display: block;
    margin-top: 100px;
    font-size: 50px;
    font-family:华文行楷;
    color: #fcc400;
}
.el-submenu .el-menu-item{
    padding: 0;
}



</style>

5.3.2 菜单组件化

vue-sport\src\utils\initMenus.js

import Home from '@/views/Home';


/**
 * 格式化菜单,将菜单转换为组件
 * @param {Array} menus 传入菜单信息
 */
export const formatMenu = (menus)=>{
    //迭代菜单,并对菜单进行改造,返回格式化后的菜单
    //filter 创建一个新的数组,新数组中的元素通过指定数组符合的条件进行返回
    const route = menus.filter(item=>{
        item.component = (item.component === 'home') ? Home:loadView(item.component);
        item.meta={
            title:item.title
        }
        //处理子菜单
        if(item.children && item.children.length > 0){
            formatMenu(item.children);
        }
        return true;
    })
    //将格式化后的菜单进行返回
    return route;
}

/**
 * 路由懒加载
 * @param {String} views 组件路径
 * @returns 返回路由组件
 */
const loadView = (views)=>{
    return (resolve)=>require([`@/views/${views}.vue`],resolve);
}


5.3.3 动态面包屑

Home.vue

 <el-main>
                <el-breadcrumb separator="/">
                    <el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
                    <el-breadcrumb-item v-for="(item,index) in $router.currentRoute.matched" :key="index">{{item.meta.title}}</el-breadcrumb-item>
                </el-breadcrumb>
                <span v-show="this.$router.currentRoute.path === '/'" class="main-title">欢迎来到个人运动管理平台!</span>
                <!-- 作为主体的子路由 -->
                <router-view/>

 </el-main>

修改vue-sport\src\utils\initMenus.js

/**
 * 格式化菜单,将菜单转换为组件
 * @param {Array} menus 传入菜单信息
 */
export const formatMenu = (menus)=>{
    //迭代菜单,并对菜单进行改造,返回格式化后的菜单
    //filter 创建一个新的数组,新数组中的元素通过指定数组符合的条件进行返回
    const route = menus.filter(item=>{
        item.component = (item.component === 'home') ? Home:loadView(item.component);
        ///////修改地方,让title存入meta中,然后在导航栏中调取
        item.meta={
            title:item.title
        }
        //处理子菜单
        if(item.children && item.children.length > 0){
            formatMenu(item.children);
        }
        return true;
    })
    //将格式化后的菜单进行返回
    return route;
}

5.3.4前端自定义指令(角色、权限)

vue-sport\src\utils\permission.js

import Vue from 'vue'
import store from '@/store'

/**
 * directive自定义角色指令
 * 第一个参数是指令
 * 第二个参数是实现方法
 */
Vue.directive('hasRole',{
    inserted(el,binding){
        console.log("111",binding)
        //获取指令传递过来的数据
        const {value} = binding;
        //获取vuex中的角色信息
        const  roles =store.state.roles;
        //定义超级管理员可以查看所有
        const admin = 'SUPER_ADMIN';
        //判断指令是否存在,传递的值是否是一个数组,数组是否大于0
        if(value && value instanceof Array && value.length >0){
            /**
             * some:用于检测数组中的元素是否满足指定的条件,并不会改变原来的数组  返回true/false
             */
            const hasRole = roles.some(item=>{
                //includes: 用于判断字符串是否包涵有个指定的子字符串
                return item.code  === admin || value.includes(item.code)
            });
            //如果没有该角色
            if(!hasRole){
                //把对应的元素给删掉
                el.parentNode.removeChild(el);
            }
        }else{
            throw new Error(`请设置${value}对应的角色标签`);
        }

    }
})
/**
 * 自定义权限指令
 */
Vue.directive('hasPermi',{
    inserted(el,binding){
        const {value} = binding;
        //获取权限数据
        const permissions = store.state.permissions;
        //获取vuex中的角色信息
        const  roles =store.state.roles;
        //定义超级管理员可以查看所有
        const admin = 'SUPER_ADMIN';
        //判断值是否存在  是否属于数组 是否大于0
        if(value && value instanceof Array && value.length > 0){
            const hasPermi = permissions.some(item=>{
                return value.includes(item.code);
            });
            const hasRole = roles.some(item=>{
                return item.code === admin;
            })
            if(!hasPermi && !hasRole){
                el.parentNode.removeChild(el);
            }
        }else{
            throw new Error(`请设置${value}对应的权限标签`)
        }

    }
})

vue-sport\src\views\system\user\index.vue

<template>
    <div>用户管理
        <!-- 做角色判断 -->
        <el-button v-hasRole="['ADMIN']">删除用户(角色)</el-button>
        <!-- 做权限判断 -->
        <el-button v-hasPermi="['USER_DELETE']">删除用户(权限)</el-button>
    </div>
</template>

<script>
import {mapState} from 'vuex';
export default {
        computed:{
            ...mapState(['roles'])
        },
        created(){
            // console.log("-----",this.roles)
        }

    }
</script>

<style  scoped>

</style>

5.3.5菜单图片-引入阿里矢量图标(Font class版)

vue-sport\src\main.js 引入阿里矢量库

//引入阿里矢量库
import '@/assets/iconfont/iconfont.css'

将上面iconfont的文件放入到 assets\iconfont\iconfont.css中

在想使用的地方加上如下代码

 <i :class="iconfont icon-xxx" style="margin-right: 5px;"></i>

使用手册:https://www.iconfont.cn/help/detail?spm=a313x.7781069.1998910419.28&helptype=code

5.4安装redis + 后端集成redis

引入redis依赖

  <!--   引入redis依赖     -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置application.yml

spring:
  #redis相关配置
  redis:
    #配置主机
    host: 127.0.0.1
    #配置使用的数据库
    database: 0
    #指定端口号
    port: 6379
1.自定义RedisTemplate序列化配置

com.jihu.javasport.config.RedisConfig

package com.jihu.javasport.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * redis配置类
 */
@Configuration
public class RedisConfig {
    /**
     * 配置redis的模板类
     * @param redisConnectionFactory redis的连接信息
     * @return redis模板
     */
    @Bean
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<String, Object> template = new RedisTemplate<String,Object>();
        template.setConnectionFactory(redisConnectionFactory);

        // Json序列化配置
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
                Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        // String 的序列化
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        template.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        template.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();

        return template;

    }
}

2.自定义RedisUtil配置

com.jihu.javasport.util.RedisUtil

package com.jihu.javasport.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
 * redis的工具类
 */
@Component
@Slf4j
public class RedisUtil {

    @Autowired
    private RedisTemplate<String,Object> redisTemplate;

    /**
     * 根据key获取redis中的值
     * @param key 键
     * @return 值
     */
    public Object getValue(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 向redis中存值
     * @param key   键
     * @param value 值
     * @return true成功 false失败
     */

    public boolean setValue(String key, Object value) {
        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
//            e.printStackTrace();
            log.error("向redis中存入值时异常-->{}",e.getMessage());
            return false;
        }
    }
    /**
     * 向redis中存值并指定过期时间  (应用业务场景:比如 手机的验证码时间)
     * SECONDS:秒
     * MINUTES:分
     * @param key   键
     * @param value 值
     * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */

    public boolean setValueTime(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.MINUTES);
            } else {
                setValue(key, value);
            }
            return true;
        } catch (Exception e) {
            log.error("设置缓存并制定过期时间异常--->{}",e.getMessage());
            return false;
        }
    }

    /**
     * 根据key删除redis中的缓存
     * @param keys  多个key
     * @return
     */
    public void delKey(String... keys){
        if (keys != null && keys.length >0){
            if (keys.length == 1){
                redisTemplate.delete(keys[0]);
            }else {
                for (String key: keys){
                    redisTemplate.delete(key);
                }
            }
        }
    }


    /**
     * 判断值是否存在
     * @param key
     * @return
     */
    public boolean haskey(String key){
        try {
            return redisTemplate.hasKey(key);
        }catch (Exception e){
            log.error("redis值不存在--->{}",e.getMessage());
            return false;
        }
    }

    /**
     * 获取redis键的过期时间
     * 0代表永久有效
     * 大于0就剩多少分钟失效
     * @param key
     * @return
     */
    public Long isExpire(String key){
        return  redisTemplate.getExpire(key, TimeUnit.MINUTES);
    }

    /**
     * 给key加过期时间
     * @param key
     * @param time
     * @return
     */
    public boolean expire(String key,long time){
        try {
            if (time >0){
                redisTemplate.expire(key,time, TimeUnit.MINUTES);
            }
            return  true;
        }catch (Exception e){
            log.error("给旧的缓存设置新的过期时间异常--->{}",e.getMessage());
            return false;
        }
   }
}

3.获取用户信息优化--存入redis中,设置缓存
1.修改security.service.UserDetailServicelmpl
package com.jihu.javasport.config.security.service;

import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.mapper.SysUserMapper;
import com.jihu.javasport.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

/**
 * 实现UserDetailsService接口,实现自定义登录逻辑
 * 重写loadUserByUsername方法
 */
@Slf4j
@Service
public class UserDetailServicelmpl  implements UserDetailsService {
    @Autowired
    private SysUserMapper sysUserMapper;
    @Autowired
    private RedisUtil redisUtil;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

//        log.info("loadUserByUsername启用");
        //判断缓存中是否存在用户信息  存在则直接从缓存中取,不存在则查询数据库并把数据存入缓存
        SysUser user;
        //redis中有值的话,在redis中查  要不然从数据库中查
        if (redisUtil.haskey("userInfo_"+username)){
            //缓存中存在用户信息,直接从redis中取
            user = (SysUser)redisUtil.getValue("userInfo_" + username);
            //为了不让其5分钟后过期,所以再设置一个
            redisUtil.expire("userInfo_"+username,5);
        }else {
            //在mapper中自定义登录,根据用户名获取用户信息
            user = sysUserMapper.findByUsername(username);

            if (user == null){
                throw new UsernameNotFoundException("用户名或密码错误!");
            }
            if (user.isAdmin()){
                //管理员 设置的查询角色信息
                user.setRoles(sysUserMapper.findRoles(null));
                user.setPermissions(sysUserMapper.findPermissions(null));
                //获取父级菜单
                List<SysMenu> menus =  sysUserMapper.findMenus(null);
                //获取子级菜单
                menus.forEach(item ->{
                    item.setChildren(sysUserMapper.findChildrenMenu(item.getId(),null));
                });
                user.setMenus(menus);

           /* List<SysRole> list = new ArrayList<>();
            SysRole role = new SysRole();
            role.setCode("admin");
            list.add(role);
            user.setRoles(list);
            user.setMenus(sysUserMapper.findMenus(null));*/
            }else{
                //非管理员需要查询角色信息
                user.setRoles(sysUserMapper.findRoles(user.getId()));
                user.setPermissions(sysUserMapper.findPermissions(user.getId()));
                //获取父级菜单
                List<SysMenu> menus =  sysUserMapper.findMenus(user.getId());
                //获取子级菜单
                menus.forEach(item ->{
                    item.setChildren(sysUserMapper.findChildrenMenu(item.getId(),user.getId()));
                });
                user.setMenus(menus);
            }
            redisUtil.setValueTime("userInfo_"+username,user,5);
        }

        return user;
    }
}
2.修改.entity.SysUser
package com.jihu.javasport.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

/**
 * <p>
 * 
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Data
  @EqualsAndHashCode(callSuper = false)
    public class SysUser implements Serializable, UserDetails {

    private static final long serialVersionUID=1L;

      /**
     * 主键
     */
      @ApiModelProperty(value = "主键")
      @TableId(value = "id", type = IdType.AUTO)
      private Long id;

      /**
     * 登录名
     */
      @ApiModelProperty(value = "用户名")
      private String userName;

      @ApiModelProperty(value = "前端展示的用户名")
      private String name;

      /**
     * 密码
     */
      @ApiModelProperty(value = "密码")
      private String password;

      /**
     * 昵称
     */
      @ApiModelProperty(value = "昵称")
      private String nickName;

      /**
     * 性别(0男,1女,2未知)
     */
      @ApiModelProperty(value = "性别")
      private Integer sex;

      /**
     * 用户头像
     */
      @ApiModelProperty(value = "用户头像")
      private String avatar;

      /**
     * 地址
     */
      @ApiModelProperty(value = "地址")
      private String address;

      /**
     * 微信小程序openid,每个用户对应一个,且唯一
     */
      @ApiModelProperty(value = "微信唯一ID")
      private String openId;

      /**
     * 状态,是否禁用,1是禁用,0不禁用
     */
      @ApiModelProperty(value = "当前状态")
      private Boolean status;

      /**
     * 是否是管理员
     */
      @ApiModelProperty(value = "是否是管理员")
      private Boolean admin;

      /**
     * 电话号码
     */
      @ApiModelProperty(value = "电话号码")
      private String phoneNumber;

      /**
     * 用户邮箱
     */
      @ApiModelProperty(value = "邮箱")
      private String email;
  /**
   * 角色信息
   */
  @ApiModelProperty(value = "角色信息")
      private List<SysRole> roles;

  /**
   * 角色对应的菜单列表
   */
  @ApiModelProperty(value = "用户对应的菜单列表")
  private  List<SysMenu> menus;

  /**
   * 数据权限
   */
  @ApiModelProperty(value = "用户对应的权限数据")
  private List<SysPermission> permissions;

      //权限数据
  @Override
  //不加@JsonIgnore会报如下错
//  Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
  @JsonIgnore
  public Collection<? extends GrantedAuthority> getAuthorities() {
    List<GrantedAuthority> list = new ArrayList<>();
    if (roles != null && roles.size() > 0){
      roles.forEach(item ->{
        list.add(new SimpleGrantedAuthority("ROLE_"+ item.getCode()));
      });
    }
    if(permissions != null && permissions.size() >0){
      permissions.forEach(item->{list.add(new SimpleGrantedAuthority(item.getCode()));});
    }

//    list.add(new SimpleGrantedAuthority("ROLE_admin"));
    return list;
  }
//用户名
  @Override
  @JsonIgnore
  public String getUsername() {
    return userName;
  }
//账号是否过期
  @Override
  @JsonIgnore
  public boolean isAccountNonExpired() {
    return false;
  }
//账号是否被锁定
  @Override
  @JsonIgnore
  public boolean isAccountNonLocked() {
    return false;
  }
//当前账号证书(密码)是否过期
  @Override
  @JsonIgnore
  public boolean isCredentialsNonExpired() {
    return false;
  }
//是否被禁用
  @Override
  @JsonIgnore
  public boolean isEnabled() {
    return status;
  }

  public boolean isAdmin() {
    return admin;
  }
}

3.修改.util.SecurityUtil
public static SysUser getUser(){
       SysUser user =  (SysUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
       user.setPassword(null);
       user.setName(user.getUsername());
       return user;
    }
4.修改vue-sport\src\router\index.js
/**
 * 路由守卫
 * to:将要去到哪个页面
 * from:从哪个页面过来
 * next:放行到哪个页面
 */
router.beforeEach((to,from,next)=>{
  console.log("roter11 ===",router);
  //判断用户是否登录
  const token= sessionStorage.getItem('token')
  if(!token){
    if(to.path === '/login'){
      next();
    }else{
      // next("/login");
      next(`/login?redirect=${to.fullPath}`)
    }
  }else{
    //判断vuex中是否存在用户基本信息
    // console.log(store);
    // if(!store.state.roles || store.state.roles.length < 1){
      //向后端发送请求,获取用户的基本信息
      ajax.get('/user/getInfo').then((res)=>{
        // console.log('用户基本信息 ',res);
        //得到用户信息
        const user = res.data.data;
        // store.commit('setName',user.username);
        store.commit('setName',user.name);
        store.commit('setAvatar',user.avatar);
        if(user.roles.length >0){
          //添加角色、菜单、权限等信息
          store.commit('setRoles',user.roles);
          //格式化菜单
          const menuList = formatMenu(user.menus);
          for(var i=0;i<menuList.length;i++){
            router.addRoute(menuList[i]);
            router.options.routes.push(menuList[i]);
          }
          console.log("roter==>",router.getRoutes());
          // for(let item of menuList){
          //   router.addRoute(item);
          //   console.log("item-->",item);
          // }
          // router.addRoutes(menuList); //已经废弃这个方法了
          store.commit('setMenus',menuList)
          store.commit('setPermissions',user.permissions)
        }

      });
    // }

    //已经登录
    // next();
    if(to.path === '/login'){
      next('/');
    }else{
      next();
    }
  }
})
5.修改vue-sport\src\store\index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: sessionStorage.getItem('token') || '',
    name:sessionStorage.getItem('name') || '',
    avatar:sessionStorage.getItem('avatar') || '',
    // roles:JSON.parse(sessionStorage.getItem('roles') || '[]'),
    roles:[],
    // menus:JSON.parse(sessionStorage.getItem('menus') || '[]'),
    menus:[],
    permissions:[]
  },
  mutations: {
    setToken(state,data){
      state.token = data;
      // console.log('token:'+data);
      sessionStorage.setItem('token',data);
      
    },
    setName(state,data){
      state.name = data;
      // sessionStorage.setItem('name',data);
      
    },
    setAvatar(state,data){
      state.avatar = data;
      // sessionStorage.setItem('avatar',data);
      
    },
    //向vuex中存入角色信息
    setRoles(state,data){
      state.roles = data;
      // sessionStorage.setItem('roles',JSON.stringify(data));
      
    },
    ////向vuex中存入菜单信息
    setMenus(state,data){
      state.menus = data;
      // sessionStorage.setItem('menus',JSON.stringify(data));
      
    },
    //向vuex中存入权限数据,限制用户操作
    setPermissions(state,data){
      state.permissions = data;
      // sessionStorage.setItem('permissions',JSON.stringify(data));
      
    },
  },
  actions: {
  },
  modules: {
  }
})

5.5 实现权限功能

5.3.7.1权限功能-分页查询、添加、修改等(后端编写)
1..SysPermissionController
package com.jihu.javasport.controller;


import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.service.SysPermissionService;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 权限数据控制层
 *
 * @author yin
 * @since 2023-01-14
 */
@RestController
@RequestMapping("/sysPermission")
@Api(tags = "权限数据")
public class SysPermissionController {
    @Autowired
    private SysPermissionService permissionService;

    @ApiOperation(value = "分页查询")
    @PostMapping("/findPage")
    public Result findPage(@RequestBody QueryInfo queryInfo){
        return permissionService.findPage(queryInfo);
    }

    @PostMapping("/insert")
    @ApiOperation(value = "添加权限")
    public Result insert(@RequestBody SysPermission sysPermission){
        return permissionService.insert(sysPermission);
    }

    @PostMapping("/update")
    @ApiOperation(value = "修改权限")
    public Result update(@RequestBody SysPermission sysPermission){
        return permissionService.update(sysPermission);
    }

    @DeleteMapping("/delete/{id}")
    @ApiOperation(value = "删除权限")
    public Result delete(@PathVariable("id") Long id){
        return permissionService.delete(id);
    }

}

2.service.SysPermissionService
package com.jihu.javasport.service;

import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysPermissionService {
    /**
     * 分页查询
     * @param queryInfo:包含 页码、页数大小、查询内容
     * @return
     */
    Result findPage(QueryInfo queryInfo);

    /**
     * 添加权限信息
     * @param permission
     * @return
     */
    Result insert(SysPermission permission);

    /**
     * 删除权限数据
     * @param id
     * @return
     */
    Result delete(Long id);

    /**
     * 修改权限数据
     * @param permission
     * @return
     */
    Result update(SysPermission permission);

}

3.service.impl.SysRoleServiceImpl
package com.jihu.javasport.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.mapper.SysPermissionMapper;
import com.jihu.javasport.service.SysPermissionService;
import com.jihu.javasport.util.PageResult;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Service
@Slf4j
public class SysPermissionServiceImpl implements SysPermissionService {
    @Autowired
    private SysPermissionMapper permissionMapper;

    @Override
    public Result findPage(QueryInfo queryInfo) {
        log.info("开始权限数据分页--->页码{},-->{}页数--->查询内容{}",queryInfo.getPageNumber(),queryInfo.getPageSize(),queryInfo.getQueryString());
        PageHelper.startPage(queryInfo.getPageNumber(),queryInfo.getPageSize());
        Page<SysPermission> page = permissionMapper.findPage(queryInfo.getQueryString());
        long total = page.getTotal();
        List<SysPermission> result = page.getResult();
        return  PageResult.pageResult(total,result);
    }

    @Override
    public Result insert(SysPermission permission) {
        permissionMapper.insertpermis(permission);
        return Result.success("添加权限数据成功");
    }

    @Override
    public Result delete(Long id) {
        permissionMapper.delete(id);
        return Result.success("删除权限数据成功");
    }

    @Override
    public Result update(SysPermission permission) {
        permissionMapper.update(permission);
        return Result.success("修改权限数据成功");
    }
}

4..mapper.SysPermissionMapper
package com.jihu.javasport.mapper;

import com.github.pagehelper.Page;
import com.jihu.javasport.entity.SysPermission;

/**
 * 权限数据的增删改查
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysPermissionMapper  {
    /**
     * 添加权限信息
     * @param permission
     */
    void  insertpermis(SysPermission permission);

    /**
     * 更新权限信息
     * @param permission
     */
    void update(SysPermission permission);

    /**
     * 删除权限数据
     */
    void delete(Long id);

    /**
     * 分页查询
     * @param queryString
     * @return
     */
    Page<SysPermission> findPage(String queryString);
}

5.SysPermissionMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysPermissionMapper">
    <select id="findPage" resultType="com.jihu.javasport.entity.SysPermission">
<!-- 分页查询权限数据   -->
    select * from sys_permission
    <if test="queryString != null and queryString.length>0">
        where label like concat('%'+#{queryString}+'%') or code like concat('%',#{queryString},'%)
    </if>
    </select>
<!-- 添加权限数据   -->
    <insert id="insertpermis" parameterType="com.jihu.javasport.entity.SysPermission">
    insert into sys_permission (label,`code`) values (#{label},#{code})
    </insert>

<!--  删除权限数据  -->
    <delete id="delete" parameterType="java.lang.Long" >
    delete from sys_permission where id=#{id}
    </delete>
<!-- 修改权限数据   -->
    <update id="update" parameterType="com.jihu.javasport.entity.SysPermission">
    update sys_permission
    <set>
        <if test="label != null">
            label = #{label},
        </if>
        <if test="code != null">
            `code` = #{code}
        </if>
    </set>
    where  id = #{id}
    </update>

 </mapper>

5.3.7.2 权限管理--前端页面编写+分页查询等(前端编写)

新建 \src\views\system\permission.vue

<template>
    <div>
        <el-card >
            <el-row>
                <el-col :span="8">
                    <!-- @clear="findPage":清除事件, 在输入框清楚后会调用findpage方法 -->
                    <el-input placeholder="请输入内容" clearable v-model="queryInfo.queryString" @clear="findPage" class="input-with-select">
                        <!--@click="findPage":点击搜索图标时,会进行搜索,因为与queryInfo.queryString属性绑定着呢,会将queryString值传给后端  -->
                        <el-button slot="append" icon="el-icon-search" @click="findPage"></el-button>
                    </el-input>
                </el-col>
                <el-col :span="2">
                        <el-button @click="insert" type="primary" style="margin-left: 15px;">添加信息</el-button>
                </el-col>
            </el-row>
            <el-table :data="tableList" >
                <el-table-column type="index" label="序号"  />
                <el-table-column prop="label" label="权限标签"  />
                <el-table-column prop="code" label="权限标签值"  />
                <el-table-column prop="status" label="状态"  />  
                <el-table-column label="是否启用">   
                    <template slot-scope = "scope">
                        <el-switch v-model="scope.row.status" @change="updateStatus(scope.row)" />
                    </template>
                </el-table-column>
                <el-table-column fixed="right" label="操作" width="180">
                    <template slot-scope="scope" >
                        <el-button type="primary" @click="update(scope.row)"  size="small" icon="el-icon-edit" circle />
                        <el-button type="danger" @click="deleteById(scope.row.id)" size="small"  icon="el-icon-delete" circle />
                    </template>
                </el-table-column>      
            </el-table>
            <el-pagination
                @size-change="handlePageSize"
                @current-change="handlePageNumber"
                :current-page="queryInfo.pageNumber"
                :page-sizes="[6, 10, 20, 50,100]"
                :page-size="queryInfo.pageSize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="total">
            </el-pagination>
        </el-card>
        <!-- 表单添加和修改 -->
        <el-dialog :title="title"  :visible.sync="open" width="35%" @close="dialogClose">
            <el-form :model="form" ref="form" :rules="rules">
            <el-form-item label="权限标签" prop="label" >
                <el-input v-model="form.label" />
            </el-form-item>
            <el-form-item label="权限标签值" prop="code" >
                <el-input v-model="form.code" />
            </el-form-item>
            <el-form-item label="是否启用" prop="status" >
                <el-radio-group v-model="form.status">
                    <el-radio :label="true">是</el-radio>
                    <el-radio :label="false">否</el-radio>
                </el-radio-group>
            </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button type="primary" @click="clickOk">确 定</el-button>
                <el-button @click="clickCancel">取 消</el-button>
            </div>
        </el-dialog>
    </div>
</template>

<script>
    export default {
        data(){
            return{
                //分页查询条件
                queryInfo:{
                    pageNumber:1, //页码
                    pageSize:6, //页数
                    queryString :null, //关键字
                },
                //表格数据
                tableList:[],
                //总记录数
                total:0,
                //表单的标题
                title:null,
                //是否打开对话框
                open:false,
                //表单数据
                form:{},
                //表单效验
                rules:{
                    label:[
                        { required: true, message: '请输入权限标签', trigger: 'blur' },
                        { min: 1, max: 50, message: '权限标签的长度为1~50之间', trigger: 'blur' }
                    ],
                    code:[
                        { required: true, message: '请输入权限标签值', trigger: 'blur' },
                        { min: 1, max: 20, message: '权限标签的长度为1~20之间', trigger: 'blur' }
                    ],
                    status:[
                        { required: true, message: '请选择状态', trigger: 'blur' },
                    ],
                    
                }
            }
        },
        //页面初始化调用方法
        created(){
            this.findPage();
        },
        methods:{
            insert(){
                // console.log("添加按钮")
                this.title = "新增权限数据";
                this.open = true;
            },    
            update(row){
                //row 就是这一行的数据
                console.log(row);
                this.form = row;
                this.title = "更新权限数据";
                this.open = true;
            },    
            // 分页查询
            findPage(){
                this.$ajax.post('/sysPermission/findPage',this.queryInfo).then((res)=>{
                    // console.log(res)
                    this.tableList = res.data.rows;
                    this.total = res.data.total;
                })
            },
            //页码改变事件
            handlePageNumber(newPageNumber){
                //将分页的新数据赋值给分页参数
                this.queryInfo.pageNumber = newPageNumber;
                //掉分页的方法
                this.findPage();
            },
            //页数改变事件
            handlePageSize(nwePageSize){
                this.queryInfo.pageSize = nwePageSize;
                this.findPage();
            },
            //删除权限信息
            deleteById(id){
                this.$confirm('您将永久删除编号为{'+ id +'} 的数据, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
                }).then(() => {
                    this.$ajax.delete(`/sysPermission/delete/${id}`).then(res=>{
                        this.$message.success(res.data.message);
                        this.queryInfo.pageNumber = 1;
                        //重新查询
                        this.findPage();
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });          
                });
            },
            //关闭对话框的事件
            dialogClose(){
                //将整个表单进行重置
                this.$refs.form.resetFields();    
            },
            //点击取消
            clickCancel(){
                // this.$refs.form.resetFields();
                this.open = false;
                this.findPage();
            },
            // 点击确定,添加权限数据
            clickOk(){
                //进行表单效验
                this.$refs.form.validate((valid)=>{
                    //效验不通过
                    if(!valid) return this.$message.error('表单效验不通过,请检查后提交!');
                    //效验通过 判断是否是新增
                    if(this.form.id === undefined || this.form.id === null){
                        this.$ajax.post('/sysPermission/insert',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }else{
                        this.$ajax.post('/sysPermission/update',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }


                })

            },
            //改变权限数据的状态
            updateStatus(row){
                // console.log(row);
                this.$ajax.post('/sysPermission/update',row).then((res)=>{
                            this.$message.success('状态更新成功!');
                            
                        });
            }
            
        }
    }
</script>

<style  scoped>

</style>

修改vue-sport\src\assets\global.css

/* 全局样式编写 */

/* 主页面样式 */
html,body,#app{
    margin: 0;
    padding: 0;
    height: 100%;
}

/* 表格样式 */
.el-table{
    width:100%;
    margin-top: 20px;
}

/* 面包屑导航的全局样式 */
.el-breadcrumb{
    font-size: 16px;
    margin-bottom: 15px;
}
/* 分页样式 */
.el-pagination{
    margin-top: 10px;
    text-align: right;
}

示例:

5.6 菜单管理编写CRUD

5.6.1 菜单管理CRUD(后端编写)

1.新建SysMenuController
package com.jihu.javasport.controller;


import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.service.SysMenuService;
import com.jihu.javasport.service.SysPermissionService;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import org.springframework.stereotype.Controller;

/**
 * <p>
 *  前端控制器
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@RestController
@RequestMapping("/sysMenu")
@Api(tags = "菜单数据")
public class SysMenuController {
    @Autowired
    private SysMenuService sysMenuService;

    @ApiOperation(value = "分页查询")
    @PostMapping("/findPage")
    public Result findPage(@RequestBody QueryInfo queryInfo){
        return sysMenuService.findPage(queryInfo);
    }

    @PostMapping("/insert")
    @ApiOperation(value = "添加权限")
    public Result insert(@RequestBody SysMenu sysMenu){
        return sysMenuService.insert(sysMenu);
    }

    @PostMapping("/update")
    @ApiOperation(value = "修改权限")
    public Result update(@RequestBody SysMenu sysMenu){
        return sysMenuService.update(sysMenu);
    }

    @DeleteMapping("/delete/{id}")
    @ApiOperation(value = "删除权限")
    public Result delete(@PathVariable("id") Long id){
        return sysMenuService.delete(id);
    }

    @GetMapping("/findParent")
    @ApiOperation(value = "查询所有父级菜单")
    public  Result findParent(){
        return  sysMenuService.findParent();
    }
}
2.新建SysMenuService
package com.jihu.javasport.service;

import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysMenuService  {
    /**
     * 分页查询
     * @param queryInfo:包含 页码、页数大小、查询内容
     * @return
     */
    Result findPage(QueryInfo queryInfo);

    /**
     * 添加菜单信息
     * @param sysMenu
     * @return
     */
    Result insert(SysMenu sysMenu);

    /**
     * 删除菜单数据
     * @param id
     * @return
     */
    Result delete(Long id);

    /**
     * 修改菜单数据
     * @param sysMenu
     * @return
     */
    Result update(SysMenu sysMenu);

    /**
     * 查询所有父级菜单
     * @return
     */
    Result findParent();
}

3.新建SysMenuServiceImpl
package com.jihu.javasport.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.mapper.SysMenuMapper;
import com.jihu.javasport.mapper.SysPermissionMapper;
import com.jihu.javasport.service.SysMenuService;
import com.jihu.javasport.util.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Service
@Slf4j
public class SysMenuServiceImpl  implements SysMenuService {
    @Autowired
    private SysMenuMapper sysMenuMapper;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public Result findPage(QueryInfo queryInfo) {
        log.info("开始权限数据分页--->页码{},-->{}页数--->查询内容{}",queryInfo.getPageNumber(),queryInfo.getPageSize(),queryInfo.getQueryString());
        PageHelper.startPage(queryInfo.getPageNumber(),queryInfo.getPageSize());
        Page<SysMenu> page = sysMenuMapper.findPage(queryInfo.getQueryString());
        long total = page.getTotal();
        List<SysMenu> result = page.getResult();
        return  PageResult.pageResult(total,result);
    }

    @Override
    public Result insert(SysMenu sysMenu) {
        sysMenuMapper.insertpermis(sysMenu);
        redisUtil.delKey("userInfo_"+ SecurityUtil.getUserName());
        return Result.success("添加权限数据成功");
    }

    @Override
    public Result delete(Long id) {
        sysMenuMapper.delete(id);
        redisUtil.delKey("userInfo_"+ SecurityUtil.getUserName());
        return Result.success("删除权限数据成功");
    }

    @Override
    public Result update(SysMenu sysMenu) {
        sysMenuMapper.update(sysMenu);
        redisUtil.delKey("userInfo_"+ SecurityUtil.getUserName());
        return Result.success("修改权限数据成功");
    }

    @Override
    public Result findParent() {
        return  Result.success("查询父级菜单成功",sysMenuMapper.findParent());
    }
}

4.新建SysMenuMapper
package com.jihu.javasport.mapper;

import com.github.pagehelper.Page;
import com.jihu.javasport.entity.SysMenu;

import java.util.List;


/**
 * 菜单数据的增删改查
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysMenuMapper  {
    /**
     * 添加菜单信息
     * @param sysMenu
     */
    void  insertpermis(SysMenu sysMenu);

    /**
     * 更新菜单信息
     * @param sysMenu
     */
    void update(SysMenu sysMenu);

    /**
     * 删除菜单数据
     */
    void delete(Long id);

    /**
     * 分页查询
     * @param queryString
     * @return
     */
    Page<SysMenu> findPage(String queryString);

    /**
     * 查询所有父级菜单
     * @return
     */
    List<SysMenu> findParent();
}
5.新建SysMenuMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysMenuMapper">

<!--    菜单结果-->
    <resultMap id="menuMap" type="com.jihu.javasport.entity.SysMenu">
    <id column="id" property="id"/>
    <result column="path" property="path"/>
     <result column="icon" property="icon"/>
     <result column="title" property="title"/>
     <result column="component" property="component"/>
     <result column="parent_id" property="parentId"/>
      <result column="status" property="status"/>
      <collection  property="children" ofType="com.jihu.javasport.entity.SysMenu"  select="findChildren" column="id" />
    </resultMap>

   <select id="findParent" resultType="com.jihu.javasport.entity.SysMenu">
      select * from sys_menu where parent_id is null
    </select>

<!--分页查询菜单数据-->
    <select id="findPage" resultMap="menuMap">
    select * from sys_menu where parent_id is null
    <if test="queryString != null and queryString.length>0">
        and title like concat('%',#{queryString},'%')
    </if>
    </select>

    <!-- 获取子菜单信息   -->
    <select id="findChildren" parameterType="int" resultType="com.jihu.javasport.entity.SysMenu">
        select * from sys_menu where parent_id = #{id}
    </select>

<!-- 添加权限数据   -->
    <insert id="insertpermis" parameterType="com.jihu.javasport.entity.SysMenu">
        insert into sys_menu (path,icon,title,component,parent_id,status)
            values (#{path},#{icon},#{title},#{component},#{parentId},#{status})
    </insert>

<!--  删除权限数据  -->
    <delete id="delete" parameterType="java.lang.Long" >
    delete from sys_menu where id=#{id}
    </delete>

<!-- 修改权限数据   -->
    <update id="update" parameterType="com.jihu.javasport.entity.SysMenu">
    update sys_menu
    <set>
        <if test="path != null">
            path = #{path},
        </if>
        <if test="icon != null">
            icon = #{icon},
        </if>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="component != null">
            component = #{component},
        </if>
        <if test="parentId != null">
            parent_id = #{parentId},
        </if>
        <if test="status != null">
            status = #{status}
        </if>
    </set>
    where  id = #{id}
    </update>
</mapper>

5.6.2菜单管理(前端页面编写)

新建vue-sport\src\views\system\menu.vue

<template>
    <div>
        <el-card >
            <el-row>
                <el-col :span="8">
                    <!-- @clear="findPage":清除事件, 在输入框清楚后会调用findpage方法 -->
                    <el-input placeholder="请输入内容" clearable v-model="queryInfo.queryString" @clear="findPage" class="input-with-select">
                        <!--@click="findPage":点击搜索图标时,会进行搜索,因为与queryInfo.queryString属性绑定着呢,会将queryString值传给后端  -->
                        <el-button slot="append" icon="el-icon-search" @click="findPage"></el-button>
                    </el-input>
                </el-col>
                <el-col :span="2">
                        <el-button @click="insert" type="primary" v-hasPermi="['MENU_INSERT']" style="margin-left: 15px;">添加信息</el-button>
                </el-col>
            </el-row>
            <el-table :data="tableList"  row-key="id"
            :tree-props="{children: 'children', hasChildren: 'hasChildren'}" >
                <el-table-column prop="path" label="前端路由"  />
                <el-table-column prop="icon" label="菜单图标"  />
                <el-table-column prop="title" label="菜单标题"  />
                <el-table-column prop="component" label="前端组件"  />
                <el-table-column label="是否启用" v-hasPermi="['MENU_UPDATE']">   
                    <template slot-scope = "scope">
                        <el-switch v-model="scope.row.status" @change="updateStatus(scope.row)" />
                    </template>
                </el-table-column>
                <el-table-column fixed="right" label="操作" width="180">
                    <template slot-scope="scope" >
                        <el-button type="primary" @click="update(scope.row)" v-hasPermi="['MENU_UPDATE']"  size="small" icon="el-icon-edit" circle />
                        <el-button type="danger" @click="deleteById(scope.row.id)" v-hasPermi="['MENU_DELETE']" size="small"  icon="el-icon-delete" circle />
                    </template>
                </el-table-column>      
            </el-table>
            <el-pagination
                @size-change="handlePageSize"
                @current-change="handlePageNumber"
                :current-page="queryInfo.pageNumber"
                :page-sizes="[6, 10, 20, 50,100]"
                :page-size="queryInfo.pageSize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="total">
            </el-pagination>
        </el-card >
        <!-- 菜单对话框 -->
        <el-dialog :title="title"  :visible.sync="open" width="35%" @close="dialogClose">
            <el-form :model="form" ref="form" :rules="rules" label-position="right" label-width="120px">
            <el-form-item label="是否是父级菜单" v-if="isChildrenMenu"  >
                <el-checkbox v-model="isChildrenMenu" style="margin-left:-90%" :disabled="disMenuSelect"  />
            </el-form-item>
            <el-form-item label="父级菜单" prop="parentId" v-if="isChildrenMenu">
                <el-select v-model="form.parentId" placeholder="请选择" style="width:90%">
                    <el-option
                        v-for="(item,index) in parentList"
                        :key="index"
                        :label="item.title"
                        :value="item.id"
                        :disabled="!item.status">
                    </el-option>
                </el-select>
            </el-form-item>
            <el-form-item label="前端路径" prop="path" >
                <el-input v-model="form.path" />
            </el-form-item>
            <el-form-item label="图标" prop="icon" >
                <e-icon-picker v-model="form.icon" />
            </el-form-item>
            <el-form-item label="标题" prop="title" >
                <el-input v-model="form.title" />
            </el-form-item>
            <el-form-item label="前端组件" prop="component" >
                <el-input v-model="form.component" />
            </el-form-item>
            <el-form-item label="是否启用" prop="status"  >
                <el-radio-group v-model="form.status" class="status-class">
                    <el-radio :label="true">是</el-radio>
                    <el-radio :label="false">否</el-radio>
                </el-radio-group>
            </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button type="primary" @click="clickOk">确 定</el-button>
                <el-button @click="clickCancel">取 消</el-button>
            </div>
        </el-dialog>
    </div>
</template>
    
<script>
export default {
    data(){
        return{
                //分页查询条件
                queryInfo:{
                    pageNumber:1, //页码
                    pageSize:6, //页数
                    queryString :null, //关键字
                },
                //表格数据
                tableList:[],
                //总记录数
                total:0,
                //表单的标题
                title:null,
                //是否打开对话框
                open:false,
                //表单数据
                form:{},
                //表单效验
                rules:{
                    path:[
                        { required: true, message: '请输入菜单路径', trigger: 'blur' },
                        { min: 1, max: 100, message: '菜单路径的长度为1~100之间', trigger: 'blur' }
                    ],
                    icon:[
                        { required: true, message: '请选择菜单图标', trigger: 'change' },
                    ],
                    title:[
                        { required: true, message: '请输入菜单标题', trigger: 'blur' },
                        { min: 1, max: 50, message: '菜单标题的长度为1~50之间', trigger: 'blur' }
                    ],
                    component:[
                        { required: true, message: '请输入菜单组件路径', trigger: 'blur' },
                        { min: 1, max: 50, message: '菜单组件的长度为1~50之间', trigger: 'blur' }
                    ],
                    parentId:[
                        { required: true, 
                        //自定义表单效验
                        validator:(rule,value,callback)=>{
                            if(this.form.parentId == null || this.form.parentId == undefined){
                                callback(new Error('请选择父级菜单'));
                            }else{
                                callback();
                            }
                        }    
                        , trigger: 'change' },
                    ],  
                    status:[
                        { required: true, message: '请选择是否启用', trigger: 'blur' },
                    ],
                    
                },
                //父级菜单列表
                parentList:[],
                isChildrenMenu:true,
                iconList:[],
                disMenuSelect:false,

            }
        },
        //页面初始化调用方法
        created(){
            this.findPage();
            this.findParent();
        },
        methods:{
            insert(){
                // console.log("添加按钮")
                this.title = "新增菜单";
                this.open = true;
            },
            update(row){
                this.form = row;
                this.title = "修改菜单";
                this.open = true;
                if(row.parentId === null){
                    //隐藏父级菜单下拉列表
                    this.isChildrenMenu = false;
                }else{
                    //禁用菜单级别选择
                    this.disMenuSelect = true;

                }
                this.findPage();
            },
            findParent(){
                this.$ajax.get('/sysMenu/findParent').then((res)=>{
                    console.log("1111",res);
                    this.parentList = res.data.data;
                });
            },  
             // 分页查询
            findPage(){
                this.$ajax.post('/sysMenu/findPage',this.queryInfo).then((res)=>{
                    // console.log(res)
                    this.tableList = res.data.rows;
                    this.total = res.data.total;
                })
            },
             //页码改变事件
            handlePageNumber(newPageNumber){
                //将分页的新数据赋值给分页参数
                this.queryInfo.pageNumber = newPageNumber;
                //掉分页的方法
                this.findPage();
            },
            //页数改变事件
            handlePageSize(nwePageSize){
                this.queryInfo.pageSize = nwePageSize;
                this.findPage();
            },
            //改变菜单数据的状态
            updateStatus(row){
                // console.log(row);
                this.$ajax.post('/sysMenu/update',row).then((res)=>{
                this.$message.success('状态更新成功!');
            });
            },
            //关闭对话框的事件
            dialogClose(){
                //将整个表单进行重置
                this.$refs.form.resetFields();    
                this.isChildrenMenu=true;
                this.disMenuSelect = false;
            },
            //点击取消
            clickCancel(){
                // this.$refs.form.resetFields();
                this.open = false;
                this.isChildrenMenu=true;
                this.disMenuSelect = false;
                this.findPage();
            },
            // 点击确定,添加权限数据
            clickOk(){
                //进行表单效验
                this.$refs.form.validate((valid)=>{
                    //效验不通过
                    if(!valid) return this.$message.error('表单效验不通过,请检查后提交!');
                    //效验通过 判断是否是新增
                    if(this.form.id === undefined || this.form.id === null){
                        this.$ajax.post('/sysMenu/insert',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }else{
                        this.$ajax.post('/sysMenu/update',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }
                })
            },
            //删除权限信息
            deleteById(id){
                this.$confirm('您将永久删除编号为{'+ id +'} 的数据, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
                }).then(() => {
                    this.$ajax.delete(`/sysMenu/delete/${id}`).then(res=>{
                        this.$message.success(res.data.message);
                        this.queryInfo.pageNumber = 1;
                        //重新查询
                        this.findPage();
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });          
                });
            },

        }
}
</script>

<style  scoped>
.status-class{
    margin-left:-67%
}
</style>

示例:

5.7引入(e-icon-picker)图标选择器组件

网站:https://e-icon-picker-ts.cnovel.club/

//安装
npm install e-icon-picker -S

新建vue-sport\src\utils\icons.js

import Vue from 'vue';

import eIconPicker from 'e-icon-picker';
import "e-icon-picker/lib/symbol.js"; //基本彩色图标库
import 'e-icon-picker/lib/index.css'; // 基本样式,包含基本图标
import 'font-awesome/css/font-awesome.min.css'; //font-awesome 图标库
import 'element-ui/lib/theme-chalk/icon.css'; //element-ui 图标库


Vue.use(eIconPicker,{
    FontAwesome: true,
    ElementUI: true,
    eIcon: true,//自带的图标,来自阿里妈妈
    eIconSymbol: true,//是否开启彩色图标
    
});

在vue-sport\src\main.js中引用

//引入图标选择器组件
import '@/utils/icons'

使用方法

<el-form-item label="图标" prop="icon" >
      <e-icon-picker v-model="form.icon" />
</el-form-item>

5.8 角色管理编写

5.8.1角色管理(后端编写)

1.新建SysRoleController
package com.jihu.javasport.controller;


import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.service.SysMenuService;
import com.jihu.javasport.service.SysRoleService;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import org.springframework.stereotype.Controller;

/**
 * 角色管理控制器
 *
 * @author yin
 * @since 2023-01-14
 */
@RestController
@RequestMapping("/sysRole")
@Api(tags = "角色管理")
public class SysRoleController {
    @Autowired
    private SysRoleService sysRoleService;

    @ApiOperation(value = "分页查询")
    @PostMapping("/findPage")
    public Result findPage(@RequestBody QueryInfo queryInfo){
        return sysRoleService.findPage(queryInfo);
    }

    @PostMapping("/insert")
    @ApiOperation(value = "添加角色信息")
    public Result insert(@RequestBody SysRole sysRole){
        return sysRoleService.insert(sysRole);
    }

    @PostMapping("/update")
    @ApiOperation(value = "修改角色信息")
    public Result update(@RequestBody SysRole sysRole){
        return sysRoleService.update(sysRole);
    }

    @DeleteMapping("/delete/{id}")
    @ApiOperation(value = "删除角色信息")
    public Result delete(@PathVariable("id") Long id){
        return sysRoleService.delete(id);
    }
}
2.新建SysRoleService
package com.jihu.javasport.service;

import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysRoleService  {
    /**
     * 分页查询
     * @param queryInfo:包含 页码、页数大小、查询内容
     * @return
     */
    Result findPage(QueryInfo queryInfo);

    /**
     * 添加角色信息
     * @param sysRole
     * @return
     */
    Result insert(SysRole sysRole);

    /**
     * 删除角色数据
     * @param id
     * @return
     */
    Result delete(Long id);

    /**
     * 修改角色数据
     * @param sysRole
     * @return
     */
    Result update(SysRole sysRole);
}

3.新建SysRoleServiceImpl
package com.jihu.javasport.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.mapper.SysMenuMapper;
import com.jihu.javasport.mapper.SysPermissionMapper;
import com.jihu.javasport.mapper.SysRoleMapper;
import com.jihu.javasport.service.SysRoleService;
import com.jihu.javasport.util.*;
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.util.ArrayList;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Service
@Slf4j
public class SysRoleServiceImpl implements SysRoleService {
    @Autowired
    private SysRoleMapper sysRoleMapper;

    @Autowired
    private SysMenuMapper sysMenuMapper;

    @Autowired
    private SysPermissionMapper permissionMapper;

    @Autowired
    private RedisUtil redisUtil;

    @Override
    public Result findPage(QueryInfo queryInfo) {
        log.info("开始权限数据分页--->页码{},-->{}页数--->查询内容{}",queryInfo.getPageNumber(),queryInfo.getPageSize(),queryInfo.getQueryString());
        PageHelper.startPage(queryInfo.getPageNumber(),queryInfo.getPageSize());
        Page<SysRole> page = sysRoleMapper.findPage(queryInfo.getQueryString());
        long total = page.getTotal();
        List<SysRole> result = page.getResult();
        result.forEach(item->{
            //查询角色下的菜单信息
           List<SysMenu> menus =  sysMenuMapper.findByRoleId(item.getId());
           menus.forEach(menu->{
               List<SysMenu> children = sysMenuMapper.findByRoleIdAndParentId(menu.getParentId(), menu.getId());
               menu.setChildren(children);
           });
           item.setSysMenus(menus);

           //查询该角色下的权限信息
            List<SysPermission> permissions = permissionMapper.findByRoleId(item.getId());
            item.setPermissions(permissions);

        });

        return  PageResult.pageResult(total,result);
    }

    @Override
    @Transactional //事务
    public Result insert(SysRole sysRole) {
        //查询角色信息是否存在
        SysRole role = sysRoleMapper.findByLabel(sysRole.getLabel());
        if (role != null){
            return Result.fail("该角色已经存在");
        }
        //插入角色信息
        sysRoleMapper.insert(sysRole);
        if(sysRole.getPermissions().size()>0){
//            log.info("先删除对应权限数据");
//            permissionMapper.deletePermissionById(sysRole.getId());
            log.info("再添加对应的权限数据");
            sysRole.getPermissions().forEach(item->{sysRoleMapper.insertPermissions(sysRole.getId(),item.getId());});
        }

        if (sysRole.getSysMenus().size()>0){
//            log.info("添加菜单信息");
//            sysMenuMapper.deleteMenuById(sysRole.getId());
            sysRole.getSysMenus().forEach(item->{sysRoleMapper.insertMenus(sysRole.getId(),item.getId());});
        }


        redisUtil.delKey("userInfo_"+ SecurityUtil.getUserName());
        return Result.success("添加角色信息成功");
    }

    @Override
    public Result delete(Long id) {
        //查询该角色信息下是否有菜单权限
        List<SysMenu> menus = sysMenuMapper.findByRoleId(id);
        List<SysMenu> childrens = new ArrayList<>();
        menus.forEach(item->{
            childrens.addAll(sysMenuMapper.findByRoleIdAndParentId(item.getId(),id));
        });
        if (menus.size() > 0  || childrens.size() > 0){
            return  Result.fail("删除失败,该角色下拥有菜单信息,请先删除对应的菜单信息!");
        }
        if (permissionMapper.findByRoleId(id).size() > 0){
            return  Result.fail("删除失败,该角色下拥有权限信息,请先删除对应的权限信息!");
        }

        sysRoleMapper.delete(id);
        redisUtil.delKey("userInfo_"+ SecurityUtil.getUserName());
        return Result.success("删除成功");
    }

    @Override
    @Transactional
    public Result update(SysRole sysRole) {
        sysRoleMapper.update(sysRole);
        if(sysRole.getPermissions().size()>0){
            log.info("先删除对应权限数据");
            permissionMapper.deletePermissionById(sysRole.getId());
            log.info("再添加对应的权限数据");
            sysRole.getPermissions().forEach(item->{sysRoleMapper.insertPermissions(sysRole.getId(),item.getId());});
        }

        if (sysRole.getSysMenus().size()>0){
//            log.info("添加菜单信息");
            sysMenuMapper.deleteMenuById(sysRole.getId());
            sysRole.getSysMenus().forEach(item->{sysRoleMapper.insertMenus(sysRole.getId(),item.getId());});
        }

        redisUtil.delKey("userInfo_"+ SecurityUtil.getUserName());
        return Result.success("修改角色信息成功");
    }

}

4.新建SysRoleMapper
package com.jihu.javasport.mapper;

import com.github.pagehelper.Page;
import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.entity.SysRole;
import org.apache.ibatis.annotations.Param;


/**
 * 角色数据的增删改查
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysRoleMapper  {
    /**
     * 添加角色信息
     * @param sysRole
     */
    void  insert(SysRole sysRole);

    /**
     * 更新角色信息
     * @param sysRole
     */
    void update(SysRole sysRole);

    /**
     * 删除角色数据
     */
    void delete(Long id);

    /**
     * 分页查询
     * @param queryString
     * @return
     */
    Page<SysRole> findPage(String queryString);

    /**
     * 根据角色ID查询出角色信息
     * @param id
     * @return
     */
    SysRole findById(Long id);

    /**
     * 添加角色权限信息
     * @param roleId 角色ID
     * @param permissionId 权限ID
     */
    void insertPermissions(@Param("roleId") Long roleId,@Param("permissionId") Long permissionId);

    /**
     * 添加角色对应的菜单信息
     * @param roleId 角色ID
     * @param menuId 菜单ID
     */
    void insertMenus(@Param("roleId") Long roleId,@Param("menuId") Long menuId);

    /**
     * 根据角色名称查询是否存在角色信息
     * @param label
     * @return
     */
    SysRole findByLabel(String label);
}

5.新建SysRoleMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysRoleMapper">

<!--    角色结果-->
    <resultMap id="roleMap" type="com.jihu.javasport.entity.SysRole">
    <id column="id" property="id"/>
    <result column="label" property="label"/>
     <result column="code" property="code"/>
      <result column="status" property="status"/>
    <!--  集合:property对应实体里的属性名,ofType:属于什么类型   column:将要传递给查询的标签的值    -->
<!--      <collection  property="sysMenus" ofType="com.jihu.javasport.entity.SysMenu"  select="findChildren" column="id" />-->
<!--      <collection  property="permissions" ofType="com.jihu.javasport.entity.SysPermission"  select="findChildren" column="id" />-->
    </resultMap>


<!--分页查询菜单数据-->
    <select id="findPage" resultMap="roleMap">
        select * from sys_role
        <if test="queryString != null and queryString.length>0">
            where label like concat('%',#{queryString},'%') or code like concat('%',#{queryString},'%')
        </if>
    </select>



<!-- 添加角色数据  keyProperty:指定返回自增的属性名  useGeneratedKeys:是否使用自增ID  -->
    <insert id="insert" parameterType="com.jihu.javasport.entity.SysRole" keyProperty="id" useGeneratedKeys="true">
        insert into sys_role (label,code,status)
            values (#{label},#{code},#{status})
    </insert>

<!--  删除角色数据  -->
    <delete id="delete" parameterType="java.lang.Long" >
    delete from sys_role where id=#{id}
    </delete>

<!-- 修改角色数据   -->
    <update id="update" parameterType="com.jihu.javasport.entity.SysRole">
    update sys_role
    <set>
        <if test="label != null">
            label = #{label},
        </if>
        <if test="code != null">
            code = #{code},
        </if>
        <if test="status != null">
            status = #{status}
        </if>
    </set>
    where  id = #{id}
    </update>

<!-- 根据角色ID查询角色信息-->
    <select id="findById" resultType="com.jihu.javasport.entity.SysRole">
        select * from sys_role where  id = #{id}
    </select>

<!-- 添加角色对应的权限信息   -->
    <insert id="insertPermissions">
     insert into roles_permissions  values (#{roleId},#{permissionId});
    </insert>

    <!-- 添加角色对应的菜单信息   -->
    <insert id="insertMenus">
     insert into roles_menus  values (#{roleId},#{menuId});
    </insert>

<!-- 根据角色名称查询角色信息    -->
    <select id="findByLabel" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.SysRole">
        select  * from sys_role where label = #{code}
    </select>
</mapper>

6.修改SysMenuMapper
package com.jihu.javasport.mapper;

import com.github.pagehelper.Page;
import com.jihu.javasport.entity.SysMenu;
import org.apache.ibatis.annotations.Param;

import java.util.List;


/**
 * 菜单数据的增删改查
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysMenuMapper  {
    /**
     * 添加菜单信息
     * @param sysMenu
     */
    void  insert(SysMenu sysMenu);

    /**
     * 更新菜单信息
     * @param sysMenu
     */
    void update(SysMenu sysMenu);

    /**
     * 删除菜单数据
     */
    void delete(Long id);

    /**
     * 分页查询
     * @param queryString
     * @return
     */
    Page<SysMenu> findPage(String queryString);

    /**
     * 查询所有父级菜单
     * @return
     */
    List<SysMenu> findParent();
    /**
     * 根据角色ID查询菜单信息
     * @param roleId
     * @return
     */
    List<SysMenu> findByRoleId(@Param("roleId") Long roleId);

    /**
     * 根据角色ID和父级ID查询所有的子级菜单
     * @param parentId
     * @param roleId
     * @return
     */
    List<SysMenu> findByRoleIdAndParentId(@Param("parentId") Long parentId,@Param("roleId") Long roleId);

    /**
     * 根据角色ID删除对应的菜单信息
     * @param roleId
     */
    void deleteMenuById( @Param("roleId") Long roleId);
}

7.修改SysMenuMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysMenuMapper">

<!--    角色结果-->
    <resultMap id="menuMap" type="com.jihu.javasport.entity.SysMenu">
    <id column="id" property="id"/>
    <result column="path" property="path"/>
     <result column="icon" property="icon"/>
     <result column="title" property="title"/>
     <result column="component" property="component"/>
     <result column="parent_id" property="parentId"/>
      <result column="status" property="status"/>
      <collection  property="children" ofType="com.jihu.javasport.entity.SysMenu"  select="findChildren" column="id" />
    </resultMap>

   <select id="findParent" resultMap="menuMap">
      select * from sys_menu where parent_id is null
    </select>

<!--分页查询角色数据-->
    <select id="findPage" resultMap="menuMap">
    select * from sys_menu where parent_id is null
    <if test="queryString != null and queryString.length>0">
        and title like concat('%',#{queryString},'%')
    </if>
    </select>

    <!-- 获取子角色信息   -->
    <select id="findChildren" parameterType="int" resultType="com.jihu.javasport.entity.SysMenu">
        select * from sys_menu where parent_id = #{id}
    </select>

<!-- 添加菜单数据   -->
    <insert id="insert" parameterType="com.jihu.javasport.entity.SysMenu">
        insert into sys_menu (path,icon,title,component,parent_id,status)
            values (#{path},#{icon},#{title},#{component},#{parentId},#{status})
    </insert>

<!--  删除菜单数据  -->
    <delete id="delete" parameterType="java.lang.Long" >
    delete from sys_menu where id=#{id}
    </delete>

<!-- 修改菜单数据   -->
    <update id="update" parameterType="com.jihu.javasport.entity.SysMenu">
    update sys_menu
    <set>
        <if test="path != null">
            path = #{path},
        </if>
        <if test="icon != null">
            icon = #{icon},
        </if>
        <if test="title != null">
            title = #{title},
        </if>
        <if test="component != null">
            component = #{component},
        </if>
        <if test="parentId != null">
            parent_id = #{parentId},
        </if>
        <if test="status != null">
            status = #{status}
        </if>
    </set>
    where  id = #{id}
    </update>

<!-- 根据角色ID查询对应的父级菜单  -->
    <select id="findByRoleId" resultType="com.jihu.javasport.entity.SysMenu">
        select  * from sys_menu where parent_id is null and status = 1
        and  id in (select menu_id from roles_menus where role_id = #{roleId} )
    </select>

<!-- 根据角色ID查询对应的子菜单   -->
      <select id="findByRoleIdAndParentId" resultType="com.jihu.javasport.entity.SysMenu">
        select  * from sys_menu where parent_id = #{parentId} and status = 1
        and  id in (select menu_id from roles_menus where role_id = #{roleId} )
    </select>

<!--  删除角色拥有的菜单信息  -->
    <delete id="deleteMenuById" parameterType="java.lang.Long">
        delete  from roles_menus where role_id = #{roleId}
    </delete>
</mapper>

8.修改SysPermissionMapper
package com.jihu.javasport.mapper;

import com.github.pagehelper.Page;
import com.jihu.javasport.entity.SysPermission;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * 权限数据的增删改查
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysPermissionMapper  {
    /**
     * 添加权限信息
     * @param permission
     */
    void  insertpermis(SysPermission permission);

    /**
     * 更新权限信息
     * @param permission
     */
    void update(SysPermission permission);

    /**
     * 删除权限数据
     */
    void delete(Long id);

    /**
     * 分页查询
     * @param queryString
     * @return
     */
    Page<SysPermission> findPage(String queryString);

    /**
     * 根据角色ID查询该角色下的权限信息
     * @param roleId
     * @return
     */
    List<SysPermission> findByRoleId(@Param("roleId") Long roleId);

    /**
     * 根据角色ID删除对应的权限信息
     * @param roleId
     */
    void deletePermissionById(@Param("roleId")Long roleId);

    /**
     * 查询所有权限数据
     * @return
     */
    List<SysPermission> findAll();
}

9.修改SysPermissionMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysPermissionMapper">
    <select id="findPage" resultType="com.jihu.javasport.entity.SysPermission">
<!-- 分页查询权限数据   -->
    select * from sys_permission
    <if test="queryString != null and queryString.length>0">
        where label like concat('%',#{queryString},'%') or code like concat('%',#{queryString},'%')
    </if>
    </select>
<!-- 添加权限数据   -->
    <insert id="insertpermis" parameterType="com.jihu.javasport.entity.SysPermission">
    insert into sys_permission (label,`code`,status) values (#{label},#{code},#{status})
    </insert>

<!--  删除权限数据  -->
    <delete id="delete" parameterType="java.lang.Long" >
    delete from sys_permission where id=#{id}
    </delete>
<!-- 修改权限数据   -->
    <update id="update" parameterType="com.jihu.javasport.entity.SysPermission">
    update sys_permission
    <set>
        <if test="label != null">
            label = #{label},
        </if>
        <if test="code != null">
            `code` = #{code},
        </if>
        <if test="status != null">
            status = #{status}
        </if>
    </set>
    where  id = #{id}
    </update>

<!-- 根据角色ID查询权限信息   -->
    <select id="findByRoleId" resultType="com.jihu.javasport.entity.SysPermission">
        select  * from sys_permission where id in
        (select permission_id from roles_permissions where role_id = #{roleId}) and status = 1
    </select>

    <delete id="deletePermissionById" parameterType="java.lang.Long">
        delete from roles_permissions where role_id = #{roleId}
    </delete>

    <select id="findAll" resultType="com.jihu.javasport.entity.SysPermission">
        select  * from sys_permission
    </select>



 </mapper>

5.8.2 角色管理(前端页面编写)

vue-sport\src\views\system\role.vue

<template>
    <div>
        <el-card >
            <el-row>
                <el-col :span="8">
                    <!-- @clear="findPage":清除事件, 在输入框清楚后会调用findpage方法 -->
                    <el-input placeholder="请输入内容" clearable v-model="queryInfo.queryString" @clear="findPage" class="input-with-select">
                        <!--@click="findPage":点击搜索图标时,会进行搜索,因为与queryInfo.queryString属性绑定着呢,会将queryString值传给后端  -->
                        <el-button slot="append" icon="el-icon-search" @click="findPage"></el-button>
                    </el-input>
                </el-col>
                <el-col :span="2">
                        <el-button @click="insert" type="primary" v-hasPermi="['PRE_ROLE_INSERT']" style="margin-left: 15px;">添加信息</el-button>
                </el-col>
            </el-row>
            <el-table :data="tableList"
            v-loading="loading"
            element-loading-text="拼命加载中"
            element-loading-spinner="el-icon-loading" >
                <el-table-column type="index" label="序号"  />
                <el-table-column prop="label" label="权限标签"  />
                <el-table-column prop="code" label="权限标签值"  />
                <!-- <el-table-column prop="status" label="状态"  />   -->
                <el-table-column label="是否启用" v-hasPermi="['PRE_USER_UPDATE']">   
                    <template slot-scope = "scope">
                        <el-switch v-model="scope.row.status" @change="updateStatus(scope.row)" />
                    </template>
                </el-table-column>
                <el-table-column fixed="right" label="操作" width="180">
                    <template slot-scope="scope" >
                        <el-button type="primary" @click="update(scope.row)"  v-hasPermi="['PRE_USER_UPDATE']" size="small" icon="el-icon-edit" circle />
                        <el-button type="danger" @click="deleteById(scope.row.id)" v-hasPermi="['PRE_USER_DELETE']" size="small"  icon="el-icon-delete" circle />
                    </template>
                </el-table-column>      
            </el-table>
            <el-pagination
                v-if="total > 0"
                @size-change="handlePageSize"
                @current-change="handlePageNumber"
                :current-page="queryInfo.pageNumber"
                :page-sizes="[6, 10, 20, 50,100]"
                :page-size="queryInfo.pageSize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="total">
            </el-pagination>
        </el-card>
        <!-- 表单添加和修改 -->
        <el-dialog :title="title"  :visible.sync="open" class="dialog" width="35%" @close="dialogClose">
            <el-form :model="form" ref="form" :rules="rules">
                <el-row>
                    <el-col :span ="11">
                        <el-form-item label="权限标签" prop="label" >
                            <el-input v-model="form.label" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="11">
                        <el-form-item label="权限标签值" prop="code" >
                            <el-input v-model="form.code" />
                        </el-form-item>
                    </el-col>
                </el-row> 
                <el-row>
                    <el-col :span ="23">
                            <el-card>
                                <div slot="header" class="clearfix">
                                    <span>菜单列表</span>
                                </div>
                                <el-tree
                                ref="tree"
                                check-strictly
                                highlight-current
                                :data="menusList"
                                show-checkbox
                                node-key="id"
                                :props="{label:'title',children:'children'}" 
                                @check-change="treeChange"	/>
                            </el-card>
                    </el-col>
                </el-row> 
                <el-row>
                    <el-col :span ="23">
                            <el-card>
                                <div slot="header" class="clearfix">
                                    <span>权限列表</span>
                                </div>
                                <!-- <el-checkbox :indeterminate="isIndeterminate" v-model="checkAll" @change="handleCheckAllChange">全选</el-checkbox> -->
                                <el-checkbox-group v-model="checkPermissions">
                                    <el-checkbox v-for="(item,index) in permissionsList" 
                                    :key="index" 
                                    border size="small"
                                    :label="item.label" 
                                    :disabled="!item.status" 
                                    @change="permissionChange(item)"/>
                                    
                                </el-checkbox-group>
                            </el-card>
                    </el-col>
                   
                </el-row> 
            <el-form-item label="是否启用" prop="status" >
                <el-radio-group v-model="form.status">
                    <el-radio :label="true">是</el-radio>
                    <el-radio :label="false">否</el-radio>
                </el-radio-group>
            </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button type="primary" @click="clickOk">确 定</el-button>
                <el-button @click="clickCancel">取 消</el-button>
            </div>
        </el-dialog>
    </div>
</template>

<script>
export default {
    data(){
        return{
                //分页查询条件
                queryInfo:{
                    pageNumber:1, //页码
                    pageSize:6, //页数
                    queryString :null, //关键字
                },
                //表格数据
                tableList:[],
                //表格加载
                loading:false,
                //总记录数
                total:0,
                //表单的标题
                title:null,
                //是否打开对话框
                open:false,
                //表单数据
                form:{
                    status: true
                },
                //表单效验
                rules:{
                    label:[
                        { required: true, message: '请输入权限标签', trigger: 'blur' },
                        { min: 1, max: 50, message: '权限标签的长度为1~50之间', trigger: 'blur' }
                    ],
                    code:[
                        { required: true, message: '请输入权限标签值', trigger: 'blur' },
                        { min: 1, max: 20, message: '权限标签的长度为1~20之间', trigger: 'blur' }
                    ],
                    status:[
                        { required: true, message: '请选择状态', trigger: 'blur' },
                    ],
                    
                },
                //菜单列表
                menusList:[],
                //选中的菜单信息
                checkMenus:[],
                //权限列表
                permissionsList:[],
                //选择的权限
                checkPermissions:[],
                //真正提交到后台的,选中的权限
                checkPermis:[],
        }
    },
    //页面初始化调用方法
    created(){
            this.findPage();
            this.findMenusandPermissions();
        },
    methods:{
        //查询所有的菜单和权限信息
        findMenusandPermissions(){
            this.$ajax.get('/sysMenu/findParent').then(res=>{
                this.menusList = res.data.data;
                //对菜单列表进行改造
                this.menusList.filter(item=>{
                    item.disabled = !item.status;
                    if(item.children && item.children.length > 0){
                        item.children.filter(i=>{
                            i.disabled = !i.status;
                        });
                    }
                    return true;
                });
            });
            this.$ajax.get('/sysPermission/findAll').then(res=>{
                this.permissionsList = res.data.data;
            });
        },
        // 打开新增对话框
        insert(){
                // console.log("添加按钮")
                this.title = "新增角色";
                this.open = true;
            },  
        //打开修改对话框
        update(row){
                //row 就是这一行的数据
                console.log("row---",row);
                this.checkMenus=[];
                this.checkPermissions = [];
                this.form = row;
                this.title = "更新角色";
                row.sysMenus.forEach(item=>{
                    this.checkMenus.push(item);
                    if(item.children && item.children.length > 0){
                        item.children.forEach(i=>{
                            this.checkMenus.push(i);
                        })
                    }
                });
                row.permissions.forEach(item=>{
                    this.checkPermissions.push(item.label);
                    this.checkPermis.push(item);
                })

                //添加异步方法
                this.$nextTick(()=>{
                    this.$refs.tree.setCheckedNodes(this.checkMenus);
                })

                this.open = true;
            },  
        // 分页查询
        findPage(){
            //loading 显示加载效果
            this.loading = true;
            this.$ajax.post('/sysRole/findPage',this.queryInfo).then((res)=>{
                    // console.log(res)
                    this.loading = false;
                    this.tableList = res.data.rows;
                    this.total = res.data.total;
                });
            },
         //页码改变事件
        handlePageNumber(newPageNumber){
                //将分页的新数据赋值给分页参数
                this.queryInfo.pageNumber = newPageNumber;
                //掉分页的方法
                this.findPage();
            },
        //页数改变事件
        handlePageSize(nwePageSize){
                this.queryInfo.pageSize = nwePageSize;
                this.findPage();
            },    
        //删除角色信息
        deleteById(id){
                this.$confirm('您将永久删除编号为{'+ id +'} 的数据, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
                }).then(() => {
                    this.$ajax.delete(`/sysRole/delete/${id}`).then(res=>{
                        this.$message.success(res.data.message);
                        this.queryInfo.pageNumber = 1;
                        //重新查询
                        this.findPage();
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });          
                });
            },
            //关闭对话框的事件
            dialogClose(){
                //将整个表单进行重置
                this.$refs.form.resetFields();  
                //将选中的多选框重置  
                this.checkPermissions= [];
                //将选中的树型下拉列表重置  
                this.$refs.tree.setCheckedKeys([]);
                //将选中的菜单设置为空
                this.checkMenus= [];
                this.findPage();
            },
            //点击取消
            clickCancel(){
                // this.$refs.form.resetFields();
                this.checkPermissions= [];
                this.$refs.tree.setCheckedKeys([]);
                this.checkMenus= [];
                this.open = false;
                this.findPage();
            },
            // 点击确定,添加权限数据
            clickOk(){
                //进行表单效验
                this.$refs.form.validate((valid)=>{
                    //效验不通过
                    if(!valid) return this.$message.error('表单效验不通过,请检查后提交!');
                        this.form.menusList = this.checkMenus;
                        this.form.permissionsList = this.checkPermis;
                        // console.log(this.form)
                    // //效验通过 判断是否是新增
                    if(this.form.id === undefined || this.form.id === null){
                        this.$ajax.post('/sysRole/insert',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }else{
                        this.$ajax.post('/sysRole/update',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }


                })

            },
            //改变角色数据的状态
            updateStatus(row){
                // console.log(row);
                this.$ajax.post('/sysRole/update',row).then((res)=>{
                    this.$message.success('状态更新成功!');
                }).catch(()=>{
                    this.$message.error('状态更新失败!');
                });
            },
            //菜单树形控件  结点值被选中
            treeChange(){
                this.checkMenus= [];
                /**
                 * getCheckedNodes:获取tree控件选中的值的数据
                 * 第一个参数:是否获取父节点的数据
                 * 第二个参数:当check-strictly为false的情况下是否包含父级结点
                */
                const checks =  this.$refs.tree.getCheckedNodes(false,true);
            //    console.log(checks);
                checks.forEach(item=>{
                    this.checkMenus.push(item)
                })

            },
            //勾选权限的事件
            permissionChange(item){
                this.checkPermis.push(item);
                // console.log(item)
            }
        

    },
    }
</script>

<style  scoped>
.el-col{
    margin-right:15px;
}
.el-checkbox{
    
    margin-right: 10px;
    margin-bottom: 10px;
}
.dialog{
    text-align: left;
}
</style>

5.9 用户管理编写

5.9.1用户管理(后端编写)

1.新建SysUserController

2.新建SysUserService
package com.jihu.javasport.service;

import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import com.jihu.javasport.vo.LoginVo;

/**
 * <p>
 *  服务类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysUserService  {

    //获取所有的用户信息
    Result findAll();

    /**
     * 登录接口
     * @param loginVo 登录参数: 账号和密码
     * @return 返回token,用token去获取资源
     */
    Result login(LoginVo loginVo);

    //根据用户名获取用户信息
    SysUser findByUsername(String username);

    /**
     * 分页查询
     * @param queryInfo
     * @return
     */
    Result findPage(QueryInfo queryInfo);

    /**
     * 添加用户信息
     * @param user
     * @return
     */
    Result insert(SysUser user);
    /**
     * 更新用户信息
     * @param user
     * @return
     */
    Result update(SysUser user);

    /**
     * 删除用户信息
     * @param id
     * @return
     */
    Result delete(Long id);

}

3.新建SysUserServiceImpl
package com.jihu.javasport.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.jihu.javasport.config.security.service.UserDetailServicelmpl;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.mapper.SysUserMapper;
import com.jihu.javasport.service.SysUserService;
import com.jihu.javasport.util.PageResult;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import com.jihu.javasport.util.TokenUtil;
import com.jihu.javasport.vo.LoginVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Service
@Slf4j //打印日志
public class SysUserServiceImpl  implements SysUserService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private UserDetailServicelmpl userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TokenUtil tokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    public Result findAll() {
        log.info("获取用户信息");
        return Result.success("获取用户信息成功",sysUserMapper.findAll());
    }

    @Override
    public Result login(LoginVo loginVo) {
        log.info("1.开始登录");
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginVo.getUsername());
        log.info("2.判断用户是否存在,密码是否正确");
        if (userDetails == null || !passwordEncoder.matches(loginVo.getPassword(),userDetails.getPassword())){
            return Result.fail("账号或密码错误,请重新输入!");
        }
        if (!userDetails.isEnabled()){
            return Result.fail("该账号已禁用,请联系管理员!");
        }
        log.info("3.登录成功,在security对象中存入登陆的信息");
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        log.info("4.根据登录信息获取token");
        //需要借助jwt来生成token
        String token = tokenUtil.generateToken(userDetails);
        Map<String,String> map = new HashMap<>(2);
        map.put("tokenHead",tokenHead);
        map.put("token",token);
        return Result.success("登录成功",map);
    }

    @Override
    public SysUser findByUsername(String username) {
        return sysUserMapper.findByUsername(username);
    }


    @Override
    public Result findPage(QueryInfo queryInfo) {
        PageHelper.startPage(queryInfo.getPageNumber(),queryInfo.getPageSize());
        Page<SysUser> page = sysUserMapper.findPage(queryInfo.getQueryString());
        long total = page.getTotal();
        List<SysUser> result = page.getResult();
        result.forEach(item->{
            item.setRoles(sysUserMapper.findRoles(item.getId()));
            item.setName(item.getUsername());
        });
        return PageResult.pageResult(total,result);
    }

    @Override
    @Transactional
    public Result insert(SysUser user) {
        //根据用户名查询用户信息
        SysUser username = sysUserMapper.findByUsername(user.getUsername());
        if (username != null) {
            return Result.fail("用户名已经存在!");
        }
        //给密码加密
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        //添加用户信息
        sysUserMapper.insert(user);
        List<SysRole> roles = user.getRoles();
        if (roles.size()>0) {
            roles.forEach(item->{
                sysUserMapper.insertRolesByUserId(user.getId(),item.getId());
            });
        }

        return Result.success("用户添加成功!");
    }

    @Override
    @Transactional
    public Result update(SysUser user) {
        //先将用户角色信息给删除
        sysUserMapper.deleteRolesByUserId(user.getId());
        //添加用户角色信息
        List<SysRole> roles = user.getRoles();
        if (roles.size()>0) {
            roles.forEach(item->{
                sysUserMapper.insertRolesByUserId(user.getId(),item.getId());
            });
        }
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        //修改用户信息
        sysUserMapper.update(user);

        return Result.success("用户信息修改成功!");
    }

    @Override
    public Result delete(Long id) {
        SysUser user = sysUserMapper.findById(id);
        if (user == null) {
            return Result.fail("用户ID不存在!");
        }
        sysUserMapper.deleteRolesByUserId(id);
        sysUserMapper.delete(id);
        return Result.success("用户信息删除成功!");
    }
}

4.新建SysUserMapper
package com.jihu.javasport.mapper;

import com.github.pagehelper.Page;
import com.jihu.javasport.entity.SysMenu;
import com.jihu.javasport.entity.SysPermission;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.util.QueryInfo;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
public interface SysUserMapper  {

    List<SysUser> findAll();

    /**
     * 根据用户名获取用户对象
     * @param username
     * @return
     */
    SysUser findByUsername(String username);

    /**
     * 根据用户ID查询权限信息
     * @param userId
     * @return
     */
    List<SysRole> findRoles(@Param("userId") Long userId);

    /**
     * 根据用户ID查询菜单信息
     * @param userId
     * @return
     */
    List<SysMenu> findMenus(@Param("userId") Long userId);

    /**
     * 根据父级ID和用户ID查询子级菜单
     * @param id 父级ID
     * @param userId 用户ID
     * @return
     */
    List<SysMenu> findChildrenMenu(@Param("id") Long id,@Param("userId") Long userId);

    /**
     * 根据用户ID查询权限数据
     * @param userId
     * @return
     */
    List<SysPermission> findPermissions(@Param("userId") Long userId);

    /**
     * 分页查询用户信息
     * @param queryString
     * @return
     */
    Page<SysUser> findPage(String queryString);

    /**
     * 添加用户信息
     * @param user
     */
    void insert(SysUser user);
    /**
     * 修改用户信息
     * @param user
     */
    void update(SysUser user);

    /**
     * 添删除用户信息
     * @param id 用户ID
     */
    void delete(Long id);

    /**
     * 根据用户信息中的角色列表,去添加用户的角色
     * @param userId
     * @param roleId
     */
    void insertRolesByUserId(@Param("userId") Long userId,@Param("roleId") Long roleId);

    /**
     * 根据用户ID去删除用户的角色列表
     * @param userId
     */
    void deleteRolesByUserId(@Param("userId") Long userId);

    /**
     * 根据用户ID查询用户的基本信息
     * @param id
     * @return
     */
    SysUser findById(Long id);
}

5.新建SysUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SysUserMapper">
<!--用户信息结果-->
    <resultMap id="mainMap" type="com.jihu.javasport.entity.SysUser">
    <id column="id" property="id"/>
    <result column="user_name" property="userName" />
    <result column="password" property="password" />
    <result column="nick_name" property="nickName" />
    <result column="sex" property="sex" />
    <result column="avatar" property="avatar" />
    <result column="address" property="address" />
    <result column="open_id" property="openId" />
    <result column="status" property="status" />
    <result column="phone_number" property="phoneNumber" />
    <result column="admin" property="admin" />
    <result column="email" property="email" />
    </resultMap>
<!--    角色结果-->
    <resultMap id="roleMap" type="com.jihu.javasport.entity.SysRole">
    <id column="id" property="id"/>
    <result column="label" property="label"/>
     <result column="code" property="code"/>
     <result column="status" property="status"/>

    </resultMap>
<!--    菜单结果-->
    <resultMap id="menuMap" type="com.jihu.javasport.entity.SysMenu">
    <id column="id" property="id"/>
    <result column="path" property="path"/>
     <result column="icon" property="icon"/>
     <result column="title" property="title"/>
     <result column="component" property="component"/>
      <result column="status" property="status"/>
<!--      <collection  property="children" ofType="com.jihu.javasport.entity.SysMenu"  select="findChildrenMenu" column="id" />-->
    </resultMap>
<!--    权限结果-->
     <resultMap id="permissionMap" type="com.jihu.javasport.entity.SysPermission">
    <id column="id" property="id"/>
    <result column="label" property="label"/>
     <result column="code" property="code"/>
     <result column="status" property="status"/>
    </resultMap>

    <select id="findAll" resultType="com.jihu.javasport.entity.SysUser">
    select  * from sys_user
    </select>

<!-- 获取用户信息-->
    <select id="findByUsername" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.SysUser">
     select  * from sys_user where user_name = #{username}
    </select>

<!-- 根据用户id查询角色信息   -->
    <select id="findRoles" parameterType="int" resultMap="roleMap">
        select * from sys_role where status = 1
        <if test="userId != null ">
          and id in (select role_id from user_roles where user_id=#{userId})
        </if>

    </select>

<!--  根据角色id查询数据权限信息  -->
    <select id="findPermissions" parameterType="int" resultMap="permissionMap" >
     select * from sys_permission where status = 1
     <if test="userId != null">
      and id in
        (select permission_id from roles_permissions where role_id in
         (select role_id from user_roles where user_id =#{userId}) )
    </if>

<!--        select * from sys_permission where id in(select permission_id from roles_permissions where role_id =#{userId})-->
    </select>

<!--  根据角色的id查询菜单信息  -->
    <select id="findMenus" parameterType="int" resultMap="menuMap" >
    select * from sys_menu where parent_id is NULL and status = 1
    <if test="userId != null ">
     and id in
        (select menu_id from roles_menus where role_id in
            (select role_id from user_roles where user_id = #{userId}) )
    </if>

<!--        select  * from sys_menu where id in(select menu_id from roles_menus where role_id = #{userId}) and parent_id is null-->
    </select>

<!-- 获取子菜单信息   -->
    <select id="findChildrenMenu" parameterType="int" resultType="com.jihu.javasport.entity.SysMenu">
        select * from sys_menu where parent_id = #{id} and status = 1
         <if test="userId != null ">
             and id in (select menu_id from roles_menus where role_id in
                (select role_id from user_roles where user_id = #{userId}))
         </if>
    </select>
<!--分页查询-->
    <select id="findPage" resultMap="mainMap">
        select * from sys_user
        <if test="queryString !=  null  and queryString.length() > 0">
            where user_name like concat('%',#{queryString},'%')
            or nick_name like concat('%',#{queryString},'%')
            or phone_number like concat('%',#{queryString},'%')
        </if>
    </select>



<!--添加用户信息-->
    <insert id="insert" parameterType="com.jihu.javasport.entity.SysUser" >
        insert into
        sys_user(user_name,`password`,nick_name,sex,avatar,address,open_id,`status`,admin,phone_number,email)
        values (#{userName},#{password},#{nickName},#{sex},#{avatar},#{address},#{openId},#{status},#{admin},#{phoneNumber},#{email})
    </insert>

    <!--  修改用户信息  -->
    <update id="update" parameterType="com.jihu.javasport.entity.SysUser" >
        update sys_user
        <set>
             <if test="password !=  null ">
                password=#{password},
            </if>
             <if test="nickName !=  null ">
                nick_name=#{nickName},
            </if>
             <if test="sex !=  null ">
                sex=#{sex},
            </if>
             <if test="avatar !=  null ">
                avatar=#{avatar},
            </if>
             <if test="address !=  null ">
                address=#{address},
            </if>
             <if test="openId !=  null ">
                open_id=#{openId},
            </if>
            <if test="status !=  null ">
                status=#{status},
            </if>
            <if test="admin !=  null ">
                admin=#{admin},
            </if>
            <if test="phoneNumber !=  null ">
                phone_number=#{phoneNumber},
            </if>
            <if test="email !=  null ">
                email=#{email}
            </if>
        </set>
        where id = #{id}
    </update>

    <!--    删除用户信息-->
    <select id="delete" parameterType="java.lang.Long" >
        delete  from sys_user where id = #{id}
    </select>

    <!--  根据用户信息中的角色列表,去添加用户的角色  -->
    <insert id="insertRolesByUserId" >
        insert  into user_roles values (#{userId},#{roleId})
    </insert>

       <!--  根据用户ID去删除用户的角色列表  -->
    <delete id="deleteRolesByUserId" >
        delete  from user_roles where user_id = #{userId}
    </delete>

<!--根据用户ID查询用户的基本信息-->
    <select id="findById" parameterType="java.lang.Long" resultType="com.jihu.javasport.entity.SysUser">
        select * from sys_user where id = #{id}
    </select>

</mapper>

5.9.2用户管理(前端页面编写)

1.用户管理的页面编写

vue-sport\src\views\system\user\index.vue

<template>
    <div>
        <el-card class="main-card">
            <!-- v-if: 判断true/false 没有直接删除document 造成性能问题 读标签元素进行删除和展示
                 v-show: 判断true/false  不管是否存在  元素都存在,只是加了display属性
                 应用场景:经常对元素进行展示的,用v-show,反正用v-if
            -->
            <div v-show="!open">
                <el-row>
                <el-col :span="8">
                    <!-- @clear="findPage":清除事件, 在输入框清楚后会调用findpage方法 -->
                    <el-input placeholder="请输入内容" clearable v-model="queryInfo.queryString" @clear="findPage" class="input-with-select">
                        <!--@click="findPage":点击搜索图标时,会进行搜索,因为与queryInfo.queryString属性绑定着呢,会将queryString值传给后端  -->
                        <el-button slot="append" icon="el-icon-search" @click="findPage"></el-button>
                    </el-input>
                </el-col>
                <el-col :span="2">
                        <el-button @click="insert" type="primary" v-hasPermi="['PRE_ROLE_INSERT']" style="margin-left: 15px;">添加信息</el-button>
                </el-col>
            </el-row>
            <el-table :data="tableList" stripe  
            v-loading="loading"
            element-loading-text="拼命加载中"
            element-loading-spinner="el-icon-loading" >
                <el-table-column type="index" label="序号"  />
                <el-table-column prop="name" label="用户名"  />
                <el-table-column prop="nickName" label="昵称"  />
                <el-table-column prop="sex" label="性别" :formatter="formaterSex" />
                <el-table-column  label = "头像" >
                    <template slot-scope = "scope">
                        <el-image
                        style="width:100px;height=100px"
                        :src="scope.row.avatar"
                        :preview-src-list="preview(scope.row.avatar)">
                        </el-image>
                    </template>
                </el-table-column>
                <el-table-column label = "角色标签">
                    <template slot-scope = "scope">
                        <el-tag sin size="mini" v-for="(item,index) in scope.row.roles" :key="index" >
                            {{ item.label }}
                        </el-tag>
                    </template>
                </el-table-column>
                <el-table-column prop="phoneNumber" label="电话"  />
                <el-table-column prop="email" label="邮箱"  />
                <el-table-column prop="address" label="地址" show-overflow-tooltip />
                <!-- <el-table-column label="管理员" v-hasRole="['SUPER_ADMIN']">   
                    <template slot-scope = "scope">
                        <el-switch v-model="scope.row.admin" @change="updateStatus(scope.row)" />
                    </template>
                </el-table-column> -->
                <!-- <el-table-column prop="status" label="状态"  />   -->
                <el-table-column label="是否启用" v-hasPermi="['USER_UPDATE']">   
                    <template slot-scope = "scope">
                        <el-switch v-model="scope.row.status" @change="updateStatus(scope.row)" />
                    </template>
                </el-table-column>
                <el-table-column fixed="right" label="操作" width="180">
                    <template slot-scope="scope" >
                        <el-button type="primary" @click="update(scope.row)"  v-hasPermi="['USER_UPDATE']" size="small" icon="el-icon-edit" circle />
                        <el-button type="danger" @click="deleteById(scope.row)" v-hasPermi="['USER_DELETE']" size="small"  icon="el-icon-delete" circle />
                    </template>
                </el-table-column>      
            </el-table>
            <el-pagination
                v-if="total > 0"
                @size-change="handlePageSize"
                @current-change="handlePageNumber"
                :current-page="queryInfo.pageNumber"
                :page-sizes="[6, 10, 20, 50,100]"
                :page-size="queryInfo.pageSize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="total">
            </el-pagination>
            </div>
            //表单添加和修改的子组件
            <handle v-show="open" :title="title" @cancel='cancel' :form="form" />
        </el-card>
    </div>
</template>

<script>
import handle from './handle.vue'
export default {
    components:{
        handle,
    },
    data(){
        return{
                //分页查询条件
                queryInfo:{
                    pageNumber:1, //页码
                    pageSize:6, //页数
                    queryString :null, //关键字
                },
                //表格数据
                tableList:[],
                //表格加载
                loading:false,
                //总记录数
                total:0,
                //表单的标题
                title:null,
                //是否打开对话框
                open:false,
                //表单数据
                form:{
                    status: true
                },
                //表单效验
                rules:{
                    
                },
                //角色列表
                roles:[],
                
        }
    },
    //页面初始化调用方法
    created(){
            this.findPage();
        },
    methods:{
        //分页查询
        findPage(){
            //loading 显示加载效果
            this.loading = true;
            this.$ajax.post('/sysUser/findPage',this.queryInfo).then((res)=>{
                    // console.log(res)
                    this.loading= false;
                    this.tableList = res.data.rows;
                    this.total = res.data.total;
                });
        },
        //页码改变事件
        handlePageNumber(newPageNumber){
            //将分页的新数据赋值给分页参数
            this.queryInfo.pageNumber = newPageNumber;
            //掉分页的方法
            this.findPage();
            },
        //页数改变事件
        handlePageSize(nwePageSize){
                this.queryInfo.pageSize = nwePageSize;
                this.findPage();
            },
        insert(){
                this.open=true;
                this.title='添加用户'
            },
        update(row){
            this.title = '修改用户',
            this.open = true;
            this.form = row;
        },
        //删除用户信息
        deleteById(row){
            if(row.admin) return this.$message.warning("管理员角色不能操作!")
                this.$confirm('您将永久删除编号为{'+ row.id +'} 的数据, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
                }).then(() => {
                    this.$ajax.delete(`/sysUser/delete/${id}`).then(res=>{
                        this.$message.success(res.data.message);
                        this.queryInfo.pageNumber = 1;
                        //重新查询
                        this.findPage();
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });          
                });
            },
        //头像预览
        preview(src){
            let arr = [];
            arr.push(src);
            return arr;
        },
        //性别格式化
        formaterSex(row,){
            if(row.sex === 0){
                return '男';
            }else if(row.sex === 1){
                return '女';
            }else{
                return '未知';
            }
        },
        cancel(){
            // console.log(val);
            this.open = false;
        }


        },
    }
</script>

<style  scoped>

</style>
2.添加、修改信息的子组件

vue-sport\src\views\system\user\handle.vue

<template>
    <div>   
        <!-- 表单添加和修改 -->
        <el-card class="box-card">
            <div slot="header" class="clearfix">
                <span>{{title}}</span>
                <!-- $emit:子组件传递给父组件时,用来传值用的
                    @click="$emit('cancel','点击了返回')" : 这样的话可以返回信息值,在父组件可以接收  -->
                <el-button style="float: right;" size="small" @click="$emit('cancel')">返回</el-button>
            </div>
            <div>
                <el-form :model="form" ref="form" :rules="rules" label-width="80px" >
                <el-row>
                    <el-col :span ="11">
                        <el-form-item label="登录名" prop="userName" >
                            <el-input v-model="form.userName" placeholder="请输入登录名" style="width:80%" />
                        </el-form-item>
                    </el-col>
                    <el-col :span="11">
                        <el-form-item label="密码" prop="password" >
                            <el-input v-model="form.password" type="password" placeholder="请输入密码"  style="width:80%"/>
                        </el-form-item>
                    </el-col>
                </el-row> 
                <el-row>
                    <el-col :span ="11">
                        <el-form-item label="性别" prop="sex" >
                            <el-select v-model="form.sex" placeholder="请选择性别" style="width:80%" >
                                <el-option :value="0" label="男" />
                                <el-option :value="1" label="女" />
                            </el-select>
                        </el-form-item>
                    </el-col>
                    <el-col :span="11">
                        <el-form-item label="用户昵称" prop="nickName" >
                            <el-input v-model="form.nickName"   style="width:80%"  />
                        </el-form-item>
                    </el-col>
                </el-row> 
                <el-row>
                    <el-col :span ="11">
                        <el-form-item label="电话号码" prop="phoneNumber" >
                            <el-input v-model="form.phoneNumber" placeholder="请输入电话号码" style="width:80%"  />
                        </el-form-item>
                    </el-col>
                    <el-col :span ="11">
                        <el-form-item label="电子邮箱" prop="email" >
                            <el-input v-model="form.email" placeholder="请输入电子邮箱" style="width:80%"  />
                        </el-form-item>
                    </el-col>
                </el-row> 
                <el-row>
                    <el-col :span ="11">
                        <el-form-item label="管理员" prop="admin" >
                            <el-radio-group v-model="form.admin">
                                <el-radio :label="true">是</el-radio>
                                <el-radio :label="false">否</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                    <el-col :span ="11">
                        <el-form-item label="是否启用" prop="status" >
                            <el-radio-group v-model="form.status">
                                <el-radio :label="true">是</el-radio>
                                <el-radio :label="false">否</el-radio>
                            </el-radio-group>
                        </el-form-item>
                    </el-col>
                </el-row> 
                <el-row>
                    <el-col :span ="11">
                        <el-form-item label="用户地址" prop="address" >
                            <el-input v-model="form.address" placeholder="请输入用户地址" style="width:80%"  />
                        </el-form-item>
                    </el-col>
                    <el-col :span ="11">
                        <el-form-item label="用户头像" prop="avatar" >
                            <!-- 
                                action:用户上传地址
                                show-file-list:是否 展示列表
                             -->
                            <el-upload
                            class="avatar-uploader"
                            action="https://jsonplaceholder.typicode.com/posts/"
                            :show-file-list="false"
                            :on-success="handleSuccess"
                            :before-upload="beforeUpload">
                            <img v-if="imageUrl" :src="imageUrl" class="avatar">
                            <i v-else class="el-icon-plus avatar-uploader-icon"></i>
                            </el-upload>
                        </el-form-item>
                    </el-col>
                </el-row> 
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button type="primary" @click="clickOk">确 定</el-button>
                <el-button @click="$emit('cancel')">取 消</el-button>
            </div>

            </div>
          
        </el-card>
    </div>
</template>

<script>
export default {
    //接收父组件传递的值
    props:['title','form'],
    // 属性监听(监听父组件传递给子组件的值)
    watch:{
        'form':{
            handler(val,old){
                console.log('表单值',val);
            }
        }
    },
    data(){
        return{
            imageUrl:null,
            rules:{
                    userName:[
                        { required: true, message: '请输入用户名', trigger: 'blur' },
                        { min: 2, max: 50, message: '权限标签的长度为2~50之间', trigger: 'blur' }
                    ],
                    password:[
                        { required: true, message: '请输入用户密码', trigger: 'blur' },
                        { min: 8, max: 50, message: '权限标签的长度为8~50之间', trigger: 'blur' }
                    ],
                    sex:[
                        { required: true, message: '请选择性别', trigger: 'change' },
                        // { min: 1, max: 20, message: '权限标签的长度为1~20之间', trigger: 'blur' }
                    ],
                    nickName:[
                        { required: true, message: '请输入用户昵称', trigger: 'blur' },
                        { min: 2, max: 50, message: '用户昵称的长度为2~50之间', trigger: 'blur' }
                    ],
                    phoneNumber:[
                        { required: true, message: '请输入电话号码', trigger: 'blur' },
                        { min: 11, max: 11, message: '请输入正确的电话号码', trigger: 'blur' }
                    ],
                    email:[
                        { required: true, message: '请输入用户邮箱', trigger: 'blur' },
                        { min: 8, max: 50, message: '用户邮箱的长度为8~50之间', trigger: 'blur' }
                    ],
                    address:[
                        { required: true, message: '请输入用户地址', trigger: 'blur' },
                        { min: 8, max: 50, message: '用户地址的长度为8~50之间', trigger: 'blur' }
                    ],
                    admin:[
                        { required: true, message: '请选择是否是管理员', trigger: 'blur' },
                    ],
                    status:[
                        { required: true, message: '请选择状态', trigger: 'blur' },
                    ],
            }
        }
    },
    methods:{
        /**
         * 上传成功
         * res:返回的参数
         * file:上传的文件信息
         */
        handleSuccess(res, file){

        },
        //上传之前  file:可以判断文件类型、文件大小
        beforeUpload(file){
            console.log("上传之前",file);

            //判断文件大小,不能超过10MB
            // let isLt2M =  / 1024 / 1024 < 2;
            if (file.size > 10 * 1024 * 1024) {
                this.$message.warnging('上传头像图片大小不能超过 10MB!');
                return false;
            }
            let index = file.name.lastIndexOf('.');
            //获取上传图片的后缀名
            let suffix = file.name.substring(index+1);
            // console.log('suffix',suffix)
            //判断文件后缀名是否符合规范
            if(suffix !== 'jpg' && suffix!== 'png' && suffix !=='jpeg' ){
                this.$message.warning('请上传jpg、png、jpeg格式的图片!');
                return false;
            }
            
        },
        clickOk(row){
            console.log()
        },
    }
}
</script>

<style  scoped>
.avatar-uploader .el-upload {
    border: 1px dashed #d9d9d9;
    border-radius: 6px;
    cursor: pointer;
    position: relative;
    overflow: hidden;
  }
  .avatar-uploader .el-upload:hover {
    border-color: #409EFF;
  }
  .avatar-uploader-icon {
    font-size: 28px;
    color: #8c939d;
    width: 125px;
    height: 125px;
    line-height: 125px;
    text-align: center;
    border: 1px dashed black;
  }
  .avatar {
    width: 178px;
    height: 178px;
    display: block;
    
  }
</style>
3.修改global.css
/* 全局样式编写 */

/* 主页面样式 */
html,body,#app{
    margin: 0;
    padding: 0;
    height: 100%;
}

/* 表格样式 */
.el-table{
    width:100%;
    margin-top: 20px;
    min-height: 380px;
}

/* 面包屑导航的全局样式 */
.el-breadcrumb{
    font-size: 16px;
    margin-bottom: 15px;
}
/* 分页样式 */
.el-pagination{
    margin-top: 10px;
    text-align: right;
}
/* 对话框样式 */
.el-dialog{
    text-align: left;
}
.main-card{
    min-height: 500px;
}

5.10 MD5加密

com.jihu.javasport.util.MD5Utils

package com.jihu.javasport.util;

import lombok.extern.slf4j.Slf4j;

import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

/**
 * MD5加密工具类
 * MD5加密是单向加密,不可逆==>即就是不能解密
 * BCryptPasswordEncoder(强散哈希算法): 是双向加密,可逆的  即可解密
 */
@Slf4j
public class MD5Utils {
    /**
     * md5加密
     * @param password 要加密的内容
     * @return 32位的加密串
     */
    public static  String md5(String password){
        if (StringUtils.isNotEmpty(password)){
            byte[] bytes = null;
            try {
                bytes = MessageDigest.getInstance("md5").digest(password.getBytes());
            } catch (NoSuchAlgorithmException e) {
                log.error("没有MD5这个加密算法!");
            }
            //由md5加密算法得到的字节数组转换为16进制数字
            StringBuilder code = new StringBuilder(new BigInteger(1, bytes).toString(16));
            //保证md5加密后是32位
            for (int i = 0; i < 32 - code.length(); i++) {
                code.insert(0, "0"); //不够32位的用0来补位
            }
            return code.toString();
        }else {
            return null;
        }
    }

    public static void main(String[] args) {
        System.out.println(md5("123456"));
    }

}

修改com.jihu.javasport.service.impl.SysUserServiceImpl

package com.jihu.javasport.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.jihu.javasport.config.security.service.UserDetailServicelmpl;
import com.jihu.javasport.entity.SysRole;
import com.jihu.javasport.entity.SysUser;
import com.jihu.javasport.mapper.SysUserMapper;
import com.jihu.javasport.service.SysUserService;
import com.jihu.javasport.util.*;
import com.jihu.javasport.vo.LoginVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author yin
 * @since 2023-01-14
 */
@Service
@Slf4j //打印日志
public class SysUserServiceImpl  implements SysUserService {
    @Autowired
    private SysUserMapper sysUserMapper;

    @Autowired
    private UserDetailServicelmpl userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Autowired
    private TokenUtil tokenUtil;

    @Value("${jwt.tokenHead}")
    private String tokenHead;

    @Override
    public Result findAll() {
        log.info("获取用户信息");
        return Result.success("获取用户信息成功",sysUserMapper.findAll());
    }

    @Override
    public Result login(LoginVo loginVo) {
        log.info("1.开始登录");
        UserDetails userDetails = userDetailsService.loadUserByUsername(loginVo.getUsername());
        log.info("2.判断用户是否存在,密码是否正确");
        if (userDetails == null || !passwordEncoder.matches(MD5Utils.md5(loginVo.getPassword()),userDetails.getPassword())){
            return Result.fail("账号或密码错误,请重新输入!");
        }
        if (!userDetails.isEnabled()){
            return Result.fail("该账号已禁用,请联系管理员!");
        }
        log.info("3.登录成功,在security对象中存入登陆的信息");
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails,null,userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        log.info("4.根据登录信息获取token");
        //需要借助jwt来生成token
        String token = tokenUtil.generateToken(userDetails);
        Map<String,String> map = new HashMap<>(2);
        map.put("tokenHead",tokenHead);
        map.put("token",token);
        return Result.success("登录成功",map);
    }

    @Override
    public SysUser findByUsername(String username) {
        return sysUserMapper.findByUsername(username);
    }


    @Override
    public Result findPage(QueryInfo queryInfo) {
        PageHelper.startPage(queryInfo.getPageNumber(),queryInfo.getPageSize());
        Page<SysUser> page = sysUserMapper.findPage(queryInfo.getQueryString());
        long total = page.getTotal();
        List<SysUser> result = page.getResult();
        result.forEach(item->{
            item.setRoles(sysUserMapper.findRoles(item.getId()));
            item.setName(item.getUsername());
        });
        return PageResult.pageResult(total,result);
    }

    @Override
    @Transactional
    public Result insert(SysUser user) {
        //根据用户名查询用户信息
        SysUser username = sysUserMapper.findByUsername(user.getUsername());
        if (username != null) {
            return Result.fail("用户名已经存在!");
        }
        //给密码加密
        user.setPassword(passwordEncoder.encode(MD5Utils.md5(user.getPassword())));
        //添加用户信息
        sysUserMapper.insert(user);
        List<SysRole> roles = user.getRoles();
        if (roles.size()>0) {
            roles.forEach(item->{
                sysUserMapper.insertRolesByUserId(user.getId(),item.getId());
            });
        }

        return Result.success("用户添加成功!");
    }

    @Override
    @Transactional
    public Result update(SysUser user) {
        //先将用户角色信息给删除
        sysUserMapper.deleteRolesByUserId(user.getId());
        //添加用户角色信息
        List<SysRole> roles = user.getRoles();
        if (roles.size()>0) {
            roles.forEach(item->{
                sysUserMapper.insertRolesByUserId(user.getId(),item.getId());
            });
        }
        user.setPassword(passwordEncoder.encode(MD5Utils.md5(user.getPassword())));
        //修改用户信息
        sysUserMapper.update(user);

        return Result.success("用户信息修改成功!");
    }

    @Override
    public Result delete(Long id) {
        SysUser user = sysUserMapper.findById(id);
        if (user == null) {
            return Result.fail("用户ID不存在!");
        }
        sysUserMapper.deleteRolesByUserId(id);
        sysUserMapper.delete(id);
        return Result.success("用户信息删除成功!");
    }
}

5.11集成七牛云-存储

开发者中心:https://developer.qiniu.com/kodo

需要的依赖

 <!--  七牛云依赖      -->
        <dependency>
            <groupId>com.qiniu</groupId>
            <artifactId>qiniu-java-sdk</artifactId>
            <version>7.7.0</version>
            <exclusions>
                <exclusion>
                    <groupId>com.google.code.gson</groupId>
                    <artifactId>gson</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.8.6</version>
        </dependency>

1.新建.config.BeanConfig

package com.jihu.javasport.config;

import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BeanConfig {

    //这样的话就会创建一个对象放在堆里面,不会每次调用都创建一个uploadManager对象
    @Bean
    public UploadManager uploadManager(){
        com.qiniu.storage.Configuration cfg = new com.qiniu.storage.Configuration(Region.region2());
        return new UploadManager(cfg);
    }
}

2.新建.util.QiniuUtils

package com.jihu.javasport.util;

import com.google.gson.Gson;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.BucketManager;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.Region;
import com.qiniu.storage.UploadManager;
import com.qiniu.storage.model.DefaultPutRet;
import com.qiniu.util.Auth;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 七牛云文件上传工具类
 * 面试一个考点: new对象都放在哪里?  在堆里
 * 堆 和 栈的区别?
 *  栈是用来存放基础变量和字符串信息
 */
@Component
@Slf4j
public class QiniuUtils {

    @Value("${qiniu.accessKey}")
    private String accessKey;

    @Value("${qiniu.secretKey}")
    private String secretKey;

    @Value("${qiniu.bucket}")
    private String bucket;

    @Autowired
    private UploadManager uploadManager;

    /**
     * 鉴权
     * @return 返回鉴权的字符串
     */
    public String uploadToken(){
        Auth auth = Auth.create(accessKey, secretKey);
       return auth.uploadToken(bucket);
    }

    /**
     * 根据文件路径上传文件
     * @param filepath
     * @param fileName
     * @return
     */
    public String upload(String filepath,String fileName){
        /**
         * 以下两行代码,为了提高效率,把其抽离出去设置成一个配置类,只会新建一次对象放在spring容器中,
         * 不会每次调用都在spring容器中新建一个对象
         */
//        Configuration cfg = new Configuration(Region.region2());
        //UploadManager上传到七牛云服务器
//        UploadManager uploadManager = new UploadManager(cfg);
        /**
         * 为了方便把其设置成一个方法
         */
//        Auth auth = Auth.create(accessKey, secretKey);
//        String uploadToken = auth.uploadToken(bucket);
        String name = this.generateName(fileName);
        try {
            Response response = uploadManager.put(filepath, name, uploadToken());
            //解析上传成功的结果
            DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
            log.info("文件上传成功===>key:{} <--> hash:{}",putRet.key,putRet.hash);
            return name;
        } catch (QiniuException e) {
            Response r = e.response;

            try {
                log.error("文件上传失败==》{}",r.bodyString());
            } catch (Exception ex2) {
                ex2.printStackTrace();
            }
            return null;
        }


    }

    /**
     * 根据文件名生成时间文件名
     * @param fileName
     * @return
     */
    public String generateName(String fileName){
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        //时间+文件名组成的文件名
        return format.format(new Date()) + fileName;
    }

    /**
     * 根据字节上传文件
     * @param bytes
     * @param fileName
     * @return
     */
    public String upload(byte[] bytes,String fileName){
        Auth auth = Auth.create(accessKey, secretKey);
        String uploadToken = auth.uploadToken(bucket);
        String name = this.generateName(fileName);
        try {
            Response response = uploadManager.put(bytes, name, uploadToken());
            //解析上传成功的结果
            DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
            log.info("文件上传成功===>key:{} <--> hash:{}",putRet.key,putRet.hash);
            return name;
        } catch (QiniuException e) {
            Response r = e.response;

            try {
                log.error("文件上传失败==》{}",r.bodyString());
            } catch (Exception ex2) {
                ex2.printStackTrace();
            }
            return null;
        }

    }


    /**
     * 根据文件输入流上传文件
     * @param stream
     * @param fileName
     * @return
     */
    public String upload(InputStream stream, String fileName){
        Auth auth = Auth.create(accessKey, secretKey);
        String uploadToken = auth.uploadToken(bucket);
        String name = this.generateName(fileName);
        try {
            Response response = uploadManager.put(stream, name, uploadToken(),null,null);
            //解析上传成功的结果
            DefaultPutRet putRet = new Gson().fromJson(response.bodyString(), DefaultPutRet.class);
            log.info("文件上传成功===>key:{} <--> hash:{}",putRet.key,putRet.hash);
            return name;
        } catch (QiniuException e) {
            Response r = e.response;

            try {
                log.error("文件上传失败==》{}",r.bodyString());
            } catch (Exception ex2) {
                ex2.printStackTrace();
            }
            return null;
        }
    }

    /**
     * 根据文件名删除文件
     * @param fileName
     */
    public void delete(String fileName){
        //构造一个带指定Region对象的配置类
        Configuration cfg = new Configuration(Region.region2());
        Auth auth = Auth.create(accessKey, secretKey);
        BucketManager bucketManager = new BucketManager(auth, cfg);

        try {
            bucketManager.delete(bucket,fileName);
            log.info("删除文件成功!");
        }catch (QiniuException ex){
            //如果遇到异常,说明删除失败
            log.error("删除失败===》code{}",ex.code());
            log.error(ex.error().toString());
        }

    }
}

3.修改application.yml

#配置七牛云秘钥
qiniu:
  accessKey: 七牛云中对应的accessKey
  secretKey: 七牛云中对应的secretKey
  bucket: javasport //七牛云中创建的文件名称(即存储桶)

4.新建ToolController

package com.jihu.javasport.controller;

import com.jihu.javasport.util.QiniuUtils;
import com.jihu.javasport.util.Result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * 工具控制器
 */
@RestController
@RequestMapping("/tool")
public class ToolController {
    @Autowired
    private QiniuUtils qiniuUtils;

    @PostMapping("/upload")
    public Result upload(@RequestBody MultipartFile file) throws IOException {
        String url = qiniuUtils.upload(file.getInputStream(), file.getOriginalFilename());
        return Result.success("文件上传成功!",url);
    }


}

5.12邮件--邮箱实现

需要的依赖

 <!-- 引入Java邮件依赖       -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>

1.修改application.yml

spring:
    #邮件配置
    mail:
      #smtp 服务主机 QQ的话是 smtp.qq.com
      host: smtp.163.com
      #服务协议
      protocol: smtp
      #设置编码集
      default-encoding: UTF-8
      #允许测试连接
      test-connection: true
      #设置发件人
      username: [email protected]
      #授权码
      password: BFJIKSLOGYANRGIH

2.新建vo.MailVo

package com.jihu.javasport.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import lombok.Data;

import java.io.Serializable;

/**
 * 邮箱对象
 */
@Data
@ApiModel(value = "邮件发送内容")
public class MailVo implements Serializable {

    @ApiModelProperty(value = "是否是HTML格式")
    private boolean html = false;

    @ApiModelProperty(value = "接收人(可以是多个)")
    private String[] receivers;

    @ApiModelProperty(value = "邮件主题")
    private String subject;

    @ApiModelProperty(value = "邮件内容")
    private String content;

}

3.新建util.MailUtils

package com.jihu.javasport.util;

import com.jihu.javasport.vo.MailVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;

import javax.mail.internet.MimeMessage;
import java.util.Arrays;

/**
 * 发送邮件工具类
 */
@Component
@Slf4j
public class MailUtils {
    @Autowired
    private JavaMailSender mailSender;

    @Value("${spring.mail.username}")
    private String from;

    /**
     * 发送邮件
     * @param mailVo
     * @return
     */
    public String sendMail(MailVo mailVo){
        try {
            if (mailVo.isHtml()){
                MimeMessage mimeMessage = mailSender.createMimeMessage();
                MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true);
                messageHelper.setFrom(from);
                messageHelper.setTo(mailVo.getReceivers());
                messageHelper.setSubject(mailVo.getSubject());
                messageHelper.setText(mailVo.getContent(),true);
                mailSender.send(mimeMessage);
                log.info("HTML邮件发送成功! 收件人-->{}--", Arrays.asList(mailVo.getReceivers()));
            }else{
                //创建邮件对象
                SimpleMailMessage mailMessage = new SimpleMailMessage();
                //发件人
                mailMessage.setFrom(from);
                //收件人
                mailMessage.setTo(mailVo.getReceivers());
                //邮件主题
                mailMessage.setSubject(mailVo.getSubject());
                //邮件内容
                mailMessage.setText(mailVo.getContent());
                mailSender.send(mailMessage);
                log.info("邮件发送成功! 收件人-->{}--", Arrays.asList(mailVo.getReceivers()));
            }
            return "发送成功!";
        }catch (Exception e){
            log.error("邮件发送失败-->{}",e.getMessage());
            return  "邮件发送失败!";
        }

    }

}

4.修改controller.ToolController

package com.jihu.javasport.controller;

import com.jihu.javasport.service.SysUserService;
import com.jihu.javasport.util.MD5Utils;
import com.jihu.javasport.util.MailUtils;
import com.jihu.javasport.util.QiniuUtils;
import com.jihu.javasport.util.Result;
import com.jihu.javasport.vo.MailVo;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Random;

/**
 * 工具控制器
 */
@RestController
@RequestMapping("/tool")
public class ToolController {
    @Autowired
    private QiniuUtils qiniuUtils;

    @Autowired
    private MailUtils mailUtils;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private SysUserService sysUserService;


    @PostMapping("/upload")
    @ApiOperation(value = "七牛云文件上传")
    public Result upload(@RequestBody MultipartFile file) throws IOException {
        String url = qiniuUtils.upload(file.getInputStream(), file.getOriginalFilename());
        return Result.success("文件上传成功!",url);
    }

    @ApiOperation(value = "忘记邮箱密码? 邮件找回")
    @PostMapping("/forget/password")
    public Result forget(@RequestBody MailVo mailVo){
        mailVo.setSubject("个人运动管理平台");
        Random random = new Random();
        //100000 + random.nextInt(1000000) :0到1000000内任何一个随机数+100000
        int password = 100000 + random.nextInt(1000000);
        sysUserService.updatePwdByMail(mailVo.getReceivers()[0],passwordEncoder.encode(MD5Utils.md5(String.valueOf(password))));
        mailVo.setContent("您的新密码:"+ password +",请妥善保管");
        return Result.success(mailUtils.sendMail(mailVo));
    }


}

5.修改SysUserService

    /**
     * 根据邮件修改密码
     * @param email
     * @param password
     */
    void updatePwdByMail(String email, String password);

6.修改SysUserServiceImpl

 @Override
    public void updatePwdByMail(String email, String password) {
        log.info("邮箱修改密码");
        sysUserMapper.updatePwdByMail(email,password);
    }

7.修改SysUserMapper

 /**
     * 根据邮件修改密码
     * @param email 邮件
     * @param password 新密码
     */
    void updatePwdByMail(@Param("email") String email,@Param("password") String password);

8.修改SysUserMapper.xml

  <update id="updatePwdByMail">
    update sts_user set `password` = #{password} where email = #{email}
    </update>
9.修改\views\login.vue
<template>
<div>
    <div class="form-class">
        <img class="logo" src="../assets/logo.png"/>
        <el-card>
            <el-form :model="form"  :rules="rules" ref="form" label-width="50px">
                <div v-if="!forgetPwd">
                    <el-form-item label="账号" prop="username">
                    <el-input type="username" v-model="form.username" ></el-input>
                    </el-form-item>
                    <el-form-item label="密码" prop="password">
                        <el-input type="password" v-model="form.password" ></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="primary" @click="submit" :loading="load">{{loginText}}</el-button>
                        <!-- <el-button @click="reset">重置</el-button> -->
                        <el-button type="text" @click="forget" >忘记密码?</el-button>
                    </el-form-item>
                </div>
                <div v-else>
                    <el-form-item label="邮箱" prop="email">
                        <el-input v-model="form.email" placeholder="请输入忘记密码账号的邮箱"></el-input>
                    </el-form-item>
                    <el-form-item>
                        <el-button type="text" @click="findPassword" >忘记密码?</el-button>
                    </el-form-item>
                </div> 
                
            </el-form>
        </el-card>
    </div>
  </div>
</template>

<script>
    export default {
        //页面加载函数
        created(){

        },
        data(){
            return{
                //表单对象
                form:{
                    username: 'admin',
                    password: '123456',
                },
                //表单效验规则
                rules:{
                    username: [
                        { required: true, message: '请输入用户名', trigger: 'blur' },
                        { min: 2, max: 20, message: '长度在 2 到 20 个字符', trigger: 'blur' }
                    ],
                    password: [
                        { required: true, message: '请输入密码', trigger: 'blur' },
                        { min: 6, max: 15, message: '长度在 6 到 15 个字符', trigger: 'blur' }
                    ],
                    email:[
                         { required: true,message: '请输入邮箱', trigger: 'blur' },
                         { type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur'] }
                    ]
                    
                },
                //登录按钮文字
                loginText:'登录',
                //登录状态
                load:false,
                forgetPwd:false,
            }
        },
        methods:{
            // 登录的方法
            submit(){
                this.$refs.form.validate((valid)=>{
                    if(!valid) return this.$message.error('数据校验失败,请检查后提交')
                    //登录加载效果
                    this.load = true;
                    this.loginText='登录中...';
                    //如果数据效验成功,则向后端发送请求登录
                    this.$ajax.post('/user/login',this.form).then(res=>{
                        console.log(res);
                        const tokenBody = res.data.data;
                        let tokenHead = tokenBody.tokenHead;
                        let token = tokenBody.token;
                        //将token存储到store中
                        this.$store.commit('setToken', tokenHead+token);
                        this.$router.push("/home")
                    }).catch(()=>{
                        this.load = false;
                        this.loginText='登录';
                    })
                })
            },
            reset(){
                //将整个表单进行重置
                this.$refs.form.resetFields();    
            },
            //忘记密码
            forget(){
                this.forgetPwd = true;    
            },
            // 找回密码, 发送邮件
            findPassword(){
                this.$refs.form.validate((valid)=>{
                    if(!valid) return this.$message.error('数据校验失败,请检查后提交')
                    //如果数据效验成功,则向后端发送请求登录
                    this.$ajax.post('/tool/forget/password',{receivers:[this.form.email]}).then(res=>{
                        console.log("findpwd",res);
                        if(res.data.flag){
                            this.$message.success(res.data.message);
                            this.forgetPwd = false;
                        }
                    })
                })
            }
        }
    }
</script>

<style scoped>
    .form-class{
        width:30%;
        margin: 200px 500px auto;
    }
    .logo{
        width: 100px;
        height: 100px;
    }
</style>

5.13 easyexcel读取示例

需要的依赖

 <!-- 引入easyexcel       -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>easyexcel</artifactId>
            <version>2.2.6</version>
        </dependency>

1.Food2

package com.jihu.javasport.entity;

import com.alibaba.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelProperty;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;


/**
 * 食物详情类
 * @E xcelIgnore : 在导入excel文件时,排除该属性,即不读取改属性的列
 * @ExcelProperty: 在导入时读取excel对应的列信息 value(default):excel列名   index: 索引   converter: 类型转换
 * @author ajie
 * @createTime 2021年06月14日 18:42:00
 */

@Data
public class Food2 {

    @ExcelIgnore
    @ApiModelProperty("主键")
    private Long id;

    @ExcelProperty(value = "食物名称")
    @ApiModelProperty("食物名称")
    private String title;

    @ExcelIgnore
    @ApiModelProperty("食物类别")
    private Long typeId;

    @ExcelIgnore
    @ApiModelProperty("图片地址")
    private String imageUrls;

    @ExcelProperty(value = "营养元素")
    @ApiModelProperty("营养元素")
    private String nutrient;

    @ExcelProperty(value = "热量")
    @ApiModelProperty("热量")
    private Float heat;

    @ExcelProperty(value = "蛋白质")
    @ApiModelProperty("蛋白质")
    private Float protein;

    @ExcelProperty(value = "脂肪")
    @ApiModelProperty("脂肪")
    private Float fat;

    @ExcelProperty(value = "碳水化合物")
    @ApiModelProperty("碳水化合物")
    private Float carbonWater;

    @ExcelProperty(value = "膳食纤维")
    @ApiModelProperty("膳食纤维")
    private Float dietaryFiber;

    @ExcelProperty(value = "维生素A")
    @ApiModelProperty("维生素A")
    private Float vitaminA;

    @ExcelProperty(value = "维生素C")
    @ApiModelProperty("维生素C")
    private Float vitaminC;

    @ExcelProperty(value = "维生素E")
    @ApiModelProperty("维生素E")
    private Float vitaminE;

    @ExcelProperty(value = "胡萝卜素")
    @ApiModelProperty("胡萝卜素")
    private Float carrot;

    @ExcelProperty(value = "维生素B1")
    @ApiModelProperty("维生素B1")
    private Float vitaminB1;

    @ExcelProperty(value = "维生素B2")
    @ApiModelProperty("维生素B2")
    private Float vitaminB2;

    @ExcelProperty(value = "烟酸")
    @ApiModelProperty("烟酸")
    private Float niacin;

    @ExcelProperty(value = "胆固醇")
    @ApiModelProperty("胆固醇")
    private Float cholesterol;

    @ExcelProperty(value = "镁")
    @ApiModelProperty("镁")
    private Float magnesium;

    @ExcelProperty(value = "铁")
    @ApiModelProperty("铁")
    private Float iron;

    @ExcelProperty(value = "钙")
    @ApiModelProperty("钙")
    private Float calcium;

    @ExcelProperty(value = "锌")
    @ApiModelProperty("锌")
    private Float zinc;

    @ExcelProperty(value = "铜")
    @ApiModelProperty("铜")
    private Float copper;

    @ExcelProperty(value = "锰")
    @ApiModelProperty("锰")
    private Float manganese;

    @ExcelProperty(value = "钾")
    @ApiModelProperty("钾")
    private Float potassium;

    @ExcelProperty(value = "磷")
    @ApiModelProperty("磷")
    private Float phosphorus;

    @ExcelProperty(value = "钠")
    @ApiModelProperty("钠")
    private Float sodium;

    @ExcelProperty(value = "硒")
    @ApiModelProperty("硒")
    private Float selenium;

    @JsonIgnore
    @ExcelProperty(value = "食物类别")
    private String typeTitle;

}

2.com.jihu.javasport.Test

package com.jihu.javasport;


import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.jihu.javasport.entity.Food2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author 阿杰
 * @create 2021-10-31 16:08
 */
public class Test {

    /**
     * 读取excel
     * @param file 导入的文件流
     * @param model 生成的类
     * @param <T>
     * @return 对象数组
     */
   public static<T> List<T> readExcel(InputStream file, Class<T> model) {
        List<T> list = new ArrayList<>();
        EasyExcel
                //读取的文件
                .read(file)
                //反射获取类型
                .head(model)
                //excel类型
                .excelType(ExcelTypeEnum.XLSX)
                //读取的excel左下角的名字
                .sheet(0)
                //注册监听器
                .registerReadListener(new AnalysisEventListener<T>() {
                    @Override
                    public void invoke(T t, AnalysisContext analysisContext) {
                        list.add(t);
                    }

                    @Override
                    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
                        System.out.println("读取完毕" + model);
                    }
                }).doRead();
        return list;
    }

    /**
     * easyexcel读取示例
     * @param args
     * @throws FileNotFoundException
     */
    public static void main(String[] args) throws FileNotFoundException {
        long start = System.currentTimeMillis();
        // new File():上传文件的路径
        List<Food2> food2s = readExcel(new FileInputStream(new File("D:\\Java-code\\sport\\excel\\food.xlsx\"")),Food2.class);
        long end = System.currentTimeMillis();
        food2s.forEach(System.out::println);
        System.err.println("读取:" + food2s.size() + "条");
        System.err.println("读取耗时:" + (end-start) / 1000 + "s");
    }

}

5.13 easypoi简单示例

easypoi官网:

所需依赖

<!--  easypoi      -->
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-base</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-web</artifactId>
            <version>3.2.0</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-annotation</artifactId>
            <version>3.2.0</version>
        </dependency>

1.Food1

package com.jihu.javasport.entity;


import cn.afterturn.easypoi.excel.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * 食物详情类
 *  Excel: name:列名   type: 导出类型 1 是文本 2 是图片,3 是函数,10 是数字 默认是文本
 *
 * @author ajie
 * @createTime 2021年06月14日 18:42:00
 */
@Data
public class Food1 {

    @ApiModelProperty("主键")
    private Long id;

    @Excel(name = "食物名称")
    @ApiModelProperty("食物名称")
    private String title;

    @ApiModelProperty("食物类别")
    private Long typeId;

    @ApiModelProperty("图片地址")
    @Excel(name = "食物图片", type = 2)
    private String imageUrls;

    @Excel(name = "营养元素")
    @ApiModelProperty("营养元素")
    private String nutrient;

    @Excel(name = "热量")
    @ApiModelProperty("热量")
    private Float heat;

    @Excel(name = "蛋白质")
    @ApiModelProperty("蛋白质")
    private Float protein;

    @Excel(name = "脂肪")
    @ApiModelProperty("脂肪")
    private Float fat;

    @Excel(name = "碳水化合物")
    @ApiModelProperty("碳水化合物")
    private Float carbonWater;

    @Excel(name = "膳食纤维")
    @ApiModelProperty("膳食纤维")
    private Float dietaryFiber;

    @Excel(name = "维生素A")
    @ApiModelProperty("维生素A")
    private Float vitaminA;

    @Excel(name = "维生素C")
    @ApiModelProperty("维生素C")
    private Float vitaminC;

    @Excel(name = "维生素E")
    @ApiModelProperty("维生素E")
    private Float vitaminE;

    @Excel(name = "胡萝卜素")
    @ApiModelProperty("胡萝卜素")
    private Float carrot;

    @Excel(name = "维生素B1")
    @ApiModelProperty("维生素B1")
    private Float vitaminB1;

    @Excel(name = "维生素B2")
    @ApiModelProperty("维生素B2")
    private Float vitaminB2;

    @Excel(name = "烟酸")
    @ApiModelProperty("烟酸")
    private Float niacin;

    @Excel(name = "胆固醇")
    @ApiModelProperty("胆固醇")
    private Float cholesterol;

    @Excel(name = "镁")
    @ApiModelProperty("镁")
    private Float magnesium;

    @Excel(name = "铁")
    @ApiModelProperty("铁")
    private Float iron;

    @Excel(name = "钙")
    @ApiModelProperty("钙")
    private Float calcium;

    @Excel(name = "锌")
    @ApiModelProperty("锌")
    private Float zinc;

    @Excel(name = "铜")
    @ApiModelProperty("铜")
    private Float copper;

    @Excel(name = "锰")
    @ApiModelProperty("锰")
    private Float manganese;

    @Excel(name = "钾")
    @ApiModelProperty("钾")
    private Float potassium;

    @Excel(name = "磷")
    @ApiModelProperty("磷")
    private Float phosphorus;

    @Excel(name = "钠")
    @ApiModelProperty("钠")
    private Float sodium;

    @Excel(name = "硒")
    @ApiModelProperty("硒")
    private Float selenium;

    @JsonIgnore
    @Excel(name = "食物类别")
    private String typeTitle;

}

2.com.jihu.javasport.Test

package com.jihu.javasport;


import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import com.alibaba.excel.support.ExcelTypeEnum;
import com.jihu.javasport.entity.Food1;
import com.jihu.javasport.entity.Food2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

/**
 * @Author 阿杰
 * @create 2021-10-31 16:08
 */
public class Test {
  /**
     * easyepoi读取示例
     * @param args
     * @throws FileNotFoundException
     */
    public static void main(String[] args) throws FileNotFoundException {
        ImportParams params = new ImportParams();
        long start2 = System.currentTimeMillis();
        List<Food1> list = ExcelImportUtil.importExcel(new File("D:\\Java-code\\sport\\excel\\food.xlsx"), Food1.class, params);
        list.forEach(System.out::println);
        System.err.println("读取:" + list.size() + "条");
        System.err.println("easypoi读取耗时:" + (System.currentTimeMillis()-start2) / 1000 + "s");
    }

}

5.14将代码放置gitee进行托管

在要提交的代码的文件中,点击Git Bash Here

rm -rf .git 

git init //初始化
git config --global user.name "尹纪虎"
git config --global user.email "[email protected]"

git add README.md //单个提交某个文件
git add .  //提交所有
git commit -m "first commit(初始化项目)" //first commit为提交上去的上面名称
git remote add origin https://gitee.com/yin-jihu/java_sport.git //java_sport为创建时的项目路径名
git push -u origin "master"

在项目名右击,点git ,再点commit directory

5.15食物管理--食物分类管理

1.食物分类管理(后端实现)

1.新增FoodController
package com.jihu.javasport.controller;

import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;

import com.alibaba.fastjson.JSONObject;
import com.jihu.javasport.entity.Food;
import com.jihu.javasport.entity.FoodType;
import com.jihu.javasport.service.FoodService;
import com.jihu.javasport.util.QiniuUtils;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.List;
import java.util.UUID;

/**
 * 食物分类管理
 * @author yin
 * @since 2023-01-14
 */
@RestController
@RequestMapping("/food")
@Api(tags = "食物管理")
public class FoodController {

    @Autowired
    private FoodService foodService;

    @Autowired
    private QiniuUtils qiniuUtils;

    @ApiOperation(value = "通过excel批量导入")
    @PostMapping("/batchImport")
    public Result batchImport(@RequestParam("file") MultipartFile file) {
        try {
            ImportParams params = new ImportParams();
            List<Food> list = ExcelImportUtil.importExcel(file.getInputStream(), Food.class, params);
            list.forEach(item -> {
                if (StringUtils.isNotEmpty(item.getImageUrls())) {
                    try {
                        FileInputStream inputStream = new FileInputStream(item.getImageUrls());
                        String uuid = UUID.randomUUID().toString().substring(0, 7);
                        int index = item.getImageUrls().lastIndexOf(".");
                        String suffix = item.getImageUrls().substring(index);
                        String fileName = uuid + suffix;
                        String upload = qiniuUtils.upload(inputStream, fileName);
                        item.setImageUrls(upload);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
            });
            return foodService.batchImport(list);
        } catch (Exception e) {
            e.printStackTrace();
            return Result.fail("食物列表导入失败!");
        }
    }

    @ApiOperation(value = "添加菜品分类")
    @PostMapping("/type/insert")
    public Result insertType(@RequestBody FoodType foodType) {
        return foodService.insertType(foodType);
    }

    @ApiOperation(value = "删除菜品分类")
    @DeleteMapping("/type/delete/{id}")
    public Result deleteType(@PathVariable Long id) {
        return foodService.deleteType(id);
    }

    @ApiOperation(value = "修改菜品分类")
    @PostMapping("/type/update")
    public Result updateType(@RequestBody FoodType foodType) {
        return foodService.updateType(foodType);
    }

    @ApiOperation(value = "分页查询菜品分类及其菜品信息")
    @PostMapping("/type/findPage")
    public Result findPage(@RequestBody QueryInfo queryInfo) {
        return foodService.findPage(queryInfo);
    }

    @GetMapping("/typeAll")
    public Result typeAll() {
        return foodService.typeAll();
    }

    @PostMapping("/findPage")
    public Result findFoodPage(@RequestBody QueryInfo queryInfo) {
        return foodService.findFoodPage(queryInfo);
    }

    @ApiOperation(value = "小程序分页查询食物列表")
    @ApiImplicitParams({
            @ApiImplicitParam(value = "页码", name = "pageNumber", required = true, dataTypeClass = Integer.class),
            @ApiImplicitParam(value = "页数大小", name = "pageSize", required = true, dataTypeClass = Integer.class),
            @ApiImplicitParam(value = "食物类别", name = "typeId", required = true, dataTypeClass = Long.class),
            @ApiImplicitParam(value = "关键字", name = "keywords", dataTypeClass = String.class)
    })
    @PostMapping("/mini/findPage")
    public Result findMiniPage(@RequestBody JSONObject object) {
        return foodService.findMiniPage(object);
    }

    @ApiOperation(value = "添加菜品")
    @PostMapping("/insert")
    public Result insert(@RequestBody Food food) {
        return foodService.insert(food);
    }

    @ApiOperation(value = "修改菜品")
    @PostMapping("/update")
    public Result update(@RequestBody Food food) {
        return foodService.update(food);
    }

    @ApiOperation(value = "删除菜品")
    @DeleteMapping("/delete/{id}")
    public Result delete(@PathVariable Long id) {
        return foodService.delete(id);
    }

    @ApiOperation(value = "根据食物类别查询食物")
    @PostMapping("/typeId")
    public Result findFoodByTypeId(@RequestBody QueryInfo queryInfo) {
        return foodService.findFoodByTypeId(queryInfo);
    }

    @GetMapping("/{id}")
    public Result foodInfo(@PathVariable Long id) {
        return foodService.findById(id);
    }

}

2.新增FoodService
package com.jihu.javasport.service;


import com.alibaba.fastjson.JSONObject;
import com.jihu.javasport.entity.Food;
import com.jihu.javasport.entity.FoodType;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;

import java.util.List;


/**
 * @author yin
 * @since 2023-01-14
 */
public interface FoodService {
    /**
     * 删除菜品
     * @param id
     * @return
     */
    Result delete(Long id);

    /**
     * 修改菜品
     * @param food
     * @return
     */
    Result update(Food food);

    /**
     * 添加菜品
     * @param food
     * @return
     */
    Result insert(Food food);

    /**
     * 分页查询菜品信息
     * @param queryInfo
     * @return
     */
    Result findPage(QueryInfo queryInfo);

    /**
     * 修改菜品分类
     * @param foodType
     * @return
     */
    Result updateType(FoodType foodType);

    /**
     * 删除食物分类
     * @param id
     * @return
     */
    Result deleteType(Long id);

    /**
     * 添加食物分类
     * @param foodType
     * @return
     */
    Result insertType(FoodType foodType);

    /**
     * 批量导入
     * @param list
     * @return
     */
    Result batchImport(List<Food> list);

    /**
     * 查询食物
     * @param queryInfo
     * @return
     */
    Result findFoodPage(QueryInfo queryInfo);

    /**
     * 查询所有分类信息
     * @return
     */
    Result typeAll();

    Result findFoodByTypeId(QueryInfo queryInfo);

    Result findMiniPage(JSONObject object);

    /**
     * 根据ID获取食物信息
     * @param id
     * @return
     */
    Result findById(Long id);
}

3.新增FoodServiceImpl
package com.jihu.javasport.service.impl;


import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.jihu.javasport.entity.Food;
import com.jihu.javasport.entity.FoodType;
import com.jihu.javasport.mapper.FoodMapper;
import com.jihu.javasport.mapper.FoodTypeMapper;
import com.jihu.javasport.service.FoodService;
import com.jihu.javasport.util.PageResult;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
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.util.ArrayList;
import java.util.List;

/**
 * @author yin
 * @since 2023-01-14
 */
@Service
@Slf4j
public class FoodServiceImpl implements FoodService {

    @Autowired
    private FoodTypeMapper foodTypeMapper;

    @Autowired
    private FoodMapper foodMapper;

    @Override
    public Result delete(Long id) {
        foodMapper.delete(id);
        return Result.success("食物删除成功");
    }

    @Override
    public Result update(Food food) {
        foodMapper.update(food);
        return Result.success("食物修改成功");
    }

    @Override
    public Result insert(Food food) {
        foodMapper.insert(food);
        return Result.success("食物添加成功");
    }

    @Override
    public Result findPage(QueryInfo queryInfo) {
        PageHelper.startPage(queryInfo.getPageNumber(), queryInfo.getPageSize());
        Page<FoodType> page = foodTypeMapper.findPage(queryInfo.getQueryString());
        return PageResult.pageResult(page.getTotal(), page.getResult());
    }

    @Override
    public Result findFoodPage(QueryInfo queryInfo) {
        PageHelper.startPage(queryInfo.getPageNumber(), queryInfo.getPageSize());
        Page<Food> page = foodMapper.findPage(queryInfo.getQueryString());
        return PageResult.pageResult(page.getTotal(), page.getResult());
    }

    @Override
    public Result updateType(FoodType foodType) {
        foodTypeMapper.update(foodType);
        return Result.success("菜品分类更新成功");
    }

    @Override
    public Result deleteType(Long id) {
        foodTypeMapper.delete(id);
        return Result.success("菜品分类删除成功");
    }

    @Override
    public Result insertType(FoodType foodType) {
        foodTypeMapper.insert(foodType);
        return Result.success("菜品分类添加成功");
    }

    @Override
    @Transactional(rollbackFor = RuntimeException.class)
    public Result batchImport(List<Food> list) {
        List<Food> foods = new ArrayList<>();
        //1. 读取分类
        for (Food food : list) {
            FoodType foodType = foodTypeMapper.findByTitle(food.getTypeTitle());
            Food title = foodMapper.findByTitle(food.getTitle());
            if (null != foodType) {
                //修改
                if (title != null) {
                    foodMapper.update(food);
                } else {
                    //添加
                    food.setTypeId(foodType.getId());
                    foods.add(food);
                }
            }
        }
        foodMapper.insertList(foods);
        return Result.success("批量导入成功!");
    }

    @Override
    public Result typeAll() {
        return Result.success("分类查询成功!", foodTypeMapper.typeAll());
    }

    @Override
    public Result findFoodByTypeId(QueryInfo queryInfo) {
        PageHelper.startPage(queryInfo.getPageNumber(), queryInfo.getPageSize());
        String queryString = queryInfo.getQueryString();
        Page<Food> foods = foodMapper.findByTypeId(queryString);
        return Result.success("食物查询成功!", new PageResult(foods.getTotal(), foods.getResult()));
    }

    @Override
    public Result findMiniPage(JSONObject object) {
        PageHelper.startPage(object.getInteger("pageNumber"), object.getInteger("pageSize"));
        Long typeId = object.getLong("typeId");
        String keywords = object.getString("keywords");
        Page<Food> foods = foodMapper.findMiniPage(typeId, keywords);
        return PageResult.pageResult(foods.getTotal(), foods.getResult());
    }

    @Override
    public Result findById(Long id) {
        if (id == null) {
            return Result.fail("请传递食物编号");
        }
        return Result.success("食物信息查询成功", foodMapper.findById(id));
    }
}

4.新增FoodMapper
package com.jihu.javasport.mapper;


import com.github.pagehelper.Page;
import com.jihu.javasport.entity.Food;
import org.apache.ibatis.annotations.Param;

import java.util.List;

/**
 * @author yin
 * @since 2023-01-14
 */
public interface FoodMapper {

    /**
     * 分页查询菜品信息
     * @param queryString
     * @return
     */
    Page<Food> findPage(String queryString);

    /**
     * 根据菜品名称查询
     * @param title
     * @return
     */
    Food findByTitle(String title);

    /**
     * 添加菜品
     * @param food
     */
    void insert(Food food);

    /**
     * 删除菜品
     * @param id
     */
    void delete(Long id);

    /**
     * 更新菜品
     * @param food
     */
    void update(Food food);

    /**
     * 根据分类ID查询食物列表
     * @param id
     * @return
     */
    Page<Food> findByTypeId(String id);

    /**
     * 添加数组类型sql
     * @param foods
     */
    void insertList(@Param("foods") List<Food> foods);

    Page<Food> findMiniPage(@Param("typeId") Long typeId, @Param("keywords") String keywords);

    Food findById(Long id);
}

5.新增FoodTypeMapper
package com.jihu.javasport.mapper;


import com.github.pagehelper.Page;
import com.jihu.javasport.entity.FoodType;

import java.util.List;

/**
 * @author yin
 * @since 2023-01-14
 */
public interface FoodTypeMapper {

    /**
     * 添加食物分类
     * @param foodType
     */
    void insert(FoodType foodType);

    /**
     * 删除食物分类
     * @param id
     */
    void delete(Long id);

    /**
     * 修改食物分类
     * @param foodType
     */
    void update(FoodType foodType);

    /**
     * 分页查询
     * @param queryString
     * @return
     */
    Page<FoodType> findPage(String queryString);

    /**
     * 根据分类名称查询分类信息
     * @param title
     * @return
     */
    FoodType findByTitle(String title);

    /**
     * 获取所有的分类
     * @return
     */
    List<FoodType> typeAll();
}

6.新增FoodMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.FoodMapper">

    <!-- 添加菜品分类 -->
    <insert id="insert" parameterType="com.jihu.javasport.entity.Food">
        insert into food values
        (null, #{title}, #{typeId}, #{imageUrls}, #{nutrient}, #{heat}, #{protein}, #{fat}, #{carbonWater}, #{dietaryFiber}, #{vitaminA},
        #{vitaminC}, #{vitaminE}, #{carrot}, #{vitaminB1}, #{vitaminB2}, #{niacin}, #{cholesterol}, #{magnesium}, #{iron}, #{calcium},
        #{zinc}, #{copper}, #{manganese}, #{potassium}, #{phosphorus}, #{sodium}, #{selenium})
    </insert>

    <!--
        添加菜品分类
        foreach: mybatis迭代标签
            collection:集合参数名称
            item: 别名
            open:迭代开始 (
            separator: 以...分割
            close: 迭代结束符号
    -->
    <insert id="insertList" parameterType="com.jihu.javasport.entity.Food">
        insert into food values
        <foreach collection="foods" item="i" separator=",">
            (null, #{i.title}, #{i.typeId}, #{i.imageUrls}, #{i.nutrient}, #{i.heat},
            #{i.protein}, #{i.fat}, #{i.carbonWater}, #{i.dietaryFiber}, #{i.vitaminA},
            #{i.vitaminC}, #{i.vitaminE}, #{i.carrot}, #{i.vitaminB1}, #{i.vitaminB2},
            #{i.niacin}, #{i.cholesterol}, #{i.magnesium}, #{i.iron}, #{i.calcium},
            #{i.zinc}, #{i.copper}, #{i.manganese}, #{i.potassium}, #{i.phosphorus},
            #{i.sodium}, #{i.selenium})
        </foreach>
    </insert>

    <!-- 删除菜品分类 -->
    <delete id="delete">
        delete from food where id = #{id}
    </delete>

    <!-- 修改菜品分类 -->
    <update id="update" parameterType="com.jihu.javasport.entity.Food">
        update food
        <set>
            <if test="title != null and title.length > 0">
                title = #{title},
            </if>
            <if test="typeId != null">
                type_id = #{typeId},
            </if>
            <if test="imageUrls != null and imageUrls.length > 0">
                image_urls = #{imageUrls},
            </if>
            <if test="nutrient != null">
                nutrient = #{nutrient},
            </if>
            <if test="heat != null">
                heat = #{heat},
            </if>
            <if test="protein != null">
                protein = #{protein},
            </if>
            <if test="fat != null">
                fat = #{fat},
            </if>
            <if test="carbonWater != null">
                carbon_water = #{carbonWater},
            </if>
            <if test="dietaryFiber != null">
                dietary_fiber = #{dietaryFiber},
            </if>
            <if test="vitaminA != null">
                vitamin_a = #{vitaminA},
            </if>
            <if test="vitaminC != null">
                vitamin_c = #{vitaminC},
            </if>
            <if test="vitaminE != null">
                vitamin_e = #{vitaminE},
            </if>
            <if test="carrot != null">
                carrot = #{carrot},
            </if>
            <if test="vitaminB1 != null">
                vitamin_b1 = #{vitaminB1},
            </if>
            <if test="vitaminB2 != null">
                vitamin_b2 = #{vitaminB2},
            </if>
            <if test="niacin != null">
                niacin = #{niacin},
            </if>
            <if test="cholesterol != null">
                cholesterol = #{cholesterol},
            </if>
            <if test="magnesium != null">
                magnesium = #{magnesium},
            </if>
            <if test="iron != null">
                iron = #{iron},
            </if>
            <if test="calcium != null">
                calcium = #{calcium},
            </if>
            <if test="zinc != null">
                zinc = #{zinc},
            </if>
            <if test="copper != null">
                copper = #{copper},
            </if>
            <if test="manganese != null">
                manganese = #{manganese},
            </if>
            <if test="potassium != null">
                potassium = #{potassium},
            </if>
            <if test="phosphorus != null">
                phosphorus = #{phosphorus},
            </if>
            <if test="sodium != null">
                sodium = #{sodium},
            </if>
            <if test="selenium != null">
                selenium = #{selenium},
            </if>
        </set>
        where id = #{id}
    </update>

    <!-- 分页查询菜品详情 -->
    <select id="findPage" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.Food">
        select * from food
        <if test="queryString != null and queryString.length > 0">
            where title like concat('%', #{queryString}, '%')
        </if>
    </select>

    <!-- 根据菜品名匹配 -->
    <select id="findByTitle" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.Food">
        select * from food where title = #{title}
    </select>


    <!-- 根据菜品分类查询食物 -->
    <select id="findByTypeId" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.Food">
        select * from food where type_id = #{value}
    </select>

    <select id="findMiniPage" resultType="com.jihu.javasport.entity.Food">
        select * from food where type_id=#{typeId}
        <if test="keywords != null and keywords.length > 0">
            and title like concat('%', #{keywords}, '%')
        </if>
    </select>

    <select id="findById" resultType="com.jihu.javasport.entity.Food">
        select * from food where id=#{id}
    </select>

</mapper>
7.新增FoodTypeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.FoodTypeMapper">

    <resultMap id="mainMap" type="com.jihu.javasport.entity.FoodType">
        <id column="id"  property="id"/>
        <result column="title"  property="title"/>
        <result column="icon"  property="icon"/>
        <collection  property="foods" select="findChildren" column="id" ofType="com.jihu.javasport.entity.FoodType"/>
    </resultMap>

    <!-- 分页查询菜品分类 -->
    <select id="findPage" parameterType="java.lang.String" resultMap="mainMap">
        SELECT
            *
        FROM
            food_type
        <if test="queryString != null and queryString.length > 0">
            where title LIKE CONCAT('%', #{queryString}, '%')
        </if>
    </select>

    <select id="findChildren" parameterType="int" resultType="com.jihu.javasport.entity.FoodType">
        select * from food where type_id = #{id}
    </select>

    <!-- 查询所有分类 -->
    <select id="typeAll" resultType="com.jihu.javasport.entity.FoodType">
        select * from food_type
    </select>

    <!-- 根据分类名称查询信息 -->
    <select id="findByTitle" parameterType="java.lang.String" resultType="com.jihu.javasport.entity.FoodType">
        select * from food_type where title = #{title}
    </select>

    <!-- 添加菜品分类 -->
    <insert id="insert" parameterType="com.jihu.javasport.entity.FoodType">
        insert into food_type(title, icon) values(#{title}, #{icon})
    </insert>

    <!-- 删除菜品分类 -->
    <delete id="delete">
        delete from food_type where id = #{id}
    </delete>

    <!-- 修改菜品分类 -->
    <update id="update" parameterType="com.jihu.javasport.entity.FoodType">
        update food_type
        <set>
            <if test="title != null and title.length > 0">
                title = #{title},
            </if>
            <if test="icon != null and icon.length > 0">
                icon = #{icon},
            </if>
        </set>
        where id = #{id}
    </update>

</mapper>
8.新增Food
package com.jihu.javasport.entity;

import cn.afterturn.easypoi.excel.annotation.Excel;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 * 食物详情类
 *  Excel: name:列名   type: 导出类型 1 是文本 2 是图片,3 是函数,10 是数字 默认是文本
 *
 * @author yin
 * @since 2023-01-14
 */
@Data
public class Food {

    @ApiModelProperty("主键")
    private Long id;

    @Excel(name = "食物名称")
    @ApiModelProperty("食物名称")
    private String title;

    @ApiModelProperty("食物类别")
    private Long typeId;

    @ApiModelProperty("图片地址")
    @Excel(name = "食物图片", type = 2)
    private String imageUrls;

    @Excel(name = "营养元素")
    @ApiModelProperty("营养元素")
    private String nutrient;

    @Excel(name = "热量")
    @ApiModelProperty("热量")
    private Float heat;

    @Excel(name = "蛋白质")
    @ApiModelProperty("蛋白质")
    private Float protein;

    @Excel(name = "脂肪")
    @ApiModelProperty("脂肪")
    private Float fat;

    @Excel(name = "碳水化合物")
    @ApiModelProperty("碳水化合物")
    private Float carbonWater;

    @Excel(name = "膳食纤维")
    @ApiModelProperty("膳食纤维")
    private Float dietaryFiber;

    @Excel(name = "维生素A")
    @ApiModelProperty("维生素A")
    private Float vitaminA;

    @Excel(name = "维生素C")
    @ApiModelProperty("维生素C")
    private Float vitaminC;

    @Excel(name = "维生素E")
    @ApiModelProperty("维生素E")
    private Float vitaminE;

    @Excel(name = "胡萝卜素")
    @ApiModelProperty("胡萝卜素")
    private Float carrot;

    @Excel(name = "维生素B1")
    @ApiModelProperty("维生素B1")
    private Float vitaminB1;

    @Excel(name = "维生素B2")
    @ApiModelProperty("维生素B2")
    private Float vitaminB2;

    @Excel(name = "烟酸")
    @ApiModelProperty("烟酸")
    private Float niacin;

    @Excel(name = "胆固醇")
    @ApiModelProperty("胆固醇")
    private Float cholesterol;

    @Excel(name = "镁")
    @ApiModelProperty("镁")
    private Float magnesium;

    @Excel(name = "铁")
    @ApiModelProperty("铁")
    private Float iron;

    @Excel(name = "钙")
    @ApiModelProperty("钙")
    private Float calcium;

    @Excel(name = "锌")
    @ApiModelProperty("锌")
    private Float zinc;

    @Excel(name = "铜")
    @ApiModelProperty("铜")
    private Float copper;

    @Excel(name = "锰")
    @ApiModelProperty("锰")
    private Float manganese;

    @Excel(name = "钾")
    @ApiModelProperty("钾")
    private Float potassium;

    @Excel(name = "磷")
    @ApiModelProperty("磷")
    private Float phosphorus;

    @Excel(name = "钠")
    @ApiModelProperty("钠")
    private Float sodium;

    @Excel(name = "硒")
    @ApiModelProperty("硒")
    private Float selenium;

    @JsonIgnore
    @Excel(name = "食物类别")
    private String typeTitle;

}

9.新增FoodType
package com.jihu.javasport.entity;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

/**
 * 食物类型
 * @author yin
 * @since 2023-01-14
 */
@Data
public class FoodType {
    @ApiModelProperty("主键")
    private Long id;

    @ApiModelProperty("分类标题")
    private String title;

    @ApiModelProperty("图标")
    private String icon;

    @ApiModelProperty("分类下的食物")
    private List<Food> foods;
}

2.食物分类管理(前端实现)

1.views\food\Food.vue
2.vue-sport\src\views\food\Type.vue
3.vue-sport\src\views\food\index.vue

5.16运动管理--运动咨询

1.markdown和marked和代码高亮引用

1.1引入markdown编辑器

参考:https://blog.csdn.net/weixin_42456784/article/details/128654978

https://www.codenong.com/cs106243641/

1.安装mavon-editor

npm install mavon-editor  --save

2.引入组件

import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'

3.使用组件

<template>
    <div>
        <mavon-editor ref="md" v-model="form.content"  @imgAdd="uploadImage"/>
    </div>
</template>
//上传本地图片
uploadImage(pos, $e) {
    let formData = new FormData();
    formData.append('file', $e);
    this.$ajax.post('/tool/upload', formData).then((res) => {
        this.$refs.md.$img2Url(pos, this.$qiniu + res.data.data);
    });
},
1.2内容回显 安装marked(markdown格式内容回显时所需)
npm install marked --save

引入marked组件

<script>
import { marked } from "marked";
export default {
    this.tableList = res.data.rows.filter(item => {
    if (item.content && item.content !== '') {
         item.content = marked(item.content);
         item.remark = item.content;
    }
    return true;
});
}
    
</script>

使用marked回显

// 内容会变成html格式
<template slot-scope="scope">
      <div v-html="scope.row.content"/>
</template>
1.3代码高亮

1.安装

npm install --save highlight.js

2.在utils下添加highlight.js

import Vue from 'vue';
import hljs from 'highlight.js'
//引入一种语法的高亮主题
import "highlight.js/styles/atom-one-dark.css"; 
//高亮指令
Vue.directive('highlight', (el)=>{
    let selectorAll = el.querySelectorAll('pre code');
    selectorAll.forEach(item=>{
        hljs.highlightBlock(item);
    })
});

3.在main.js中引入

// 引入高亮
import '@/utils/highlight';

4.使用代码高亮

因为重写了代码高亮 直接使用v-highlight,就会执行utils下的highlight.js下的 highlight
/** html使用 */
<div v-html="scope.row.content" v-highlight/>

2.运动资讯(后端实现)

1.新增SportController
package com.jihu.javasport.controller;

import com.jihu.javasport.entity.Sport;

import com.jihu.javasport.service.SportService;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 运动咨询
 *  * @author yin
 *  * @since 2023-01-14
 */
@RestController
@RequestMapping("/sport")
@Api(tags = "运动管理")
public class SportController {

    @Autowired
    private SportService sportService;

    @ApiOperation(value = "分页查询")
    @PostMapping("/findPage")
    public Result findPage(@RequestBody QueryInfo queryInfo){
        return sportService.findPage(queryInfo);
    }



    @PostMapping("/insert")
    @ApiOperation(value = "添加角色信息")
    public Result insert(@RequestBody Sport sport){
        return sportService.insert(sport);
    }

    @PostMapping("/update")
    @ApiOperation(value = "修改角色信息")
    public Result update(@RequestBody Sport sport){
        return sportService.update(sport);
    }

    @DeleteMapping("/delete/{id}")
    @ApiOperation(value = "删除角色信息")
    public Result delete(@PathVariable("id") Long id){
        return sportService.delete(id);
    }

}

2.新建SportService
package com.jihu.javasport.service;


import com.jihu.javasport.entity.Sport;
import com.jihu.javasport.util.QueryInfo;
import com.jihu.javasport.util.Result;

/**
 * @author yin
 * @since 2023-01-22
 */
public interface SportService {
    /**
     * 删除菜品
     * @param id
     * @return
     */
    Result delete(Long id);

    /**
     * 获取详情
     * @param id
     * @return
     */
    Result findInfo(Long id);

    /**
     * 修改菜品
     * @param sport
     * @return
     */
    Result update(Sport sport);

    /**
     * 添加菜品
     * @param sport
     * @return
     */
    Result insert(Sport sport);

    /**
     * 分页查询菜品信息
     * @param queryInfo
     * @return
     */
    Result findPage(QueryInfo queryInfo);

//    Result insertStep(List<WxRun> runs);

//    Result stepReport();

}

3.新建SportServiceImpl
package com.jihu.javasport.service.impl;


import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.jihu.javasport.entity.Sport;

import com.jihu.javasport.mapper.SportMapper;
import com.jihu.javasport.service.SportService;
import com.jihu.javasport.util.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;


/**
 * @author yin
 * @since 2023-01-22
 */
@Slf4j
@Service
public class SportServiceImpl implements SportService {

    @Autowired
    private SportMapper sportMapper;

    @Override
    public Result delete(Long id) {
        if (id == null) {
            return Result.fail("请传递删除ID");
        }
        sportMapper.delete(id);
        return Result.success("删除成功");
    }

    @Override
    public Result update(Sport sport) {
        sport.setCreateName(SecurityUtil.getUserName());
        sport.setCreateTime(DateUtils.getDateTime());
        sportMapper.update(sport);
        return Result.success("修改成功");
    }

    @Override
    public Result insert(Sport sport) {
        sport.setCreateName(SecurityUtil.getUserName());
        sport.setCreateTime(DateUtils.getDateTime());
        sportMapper.insert(sport);
        return Result.success("添加成功");
    }

    @Override
    public Result findPage(QueryInfo queryInfo) {
        log.info("开始数据分页-->页码{}, --->{}页数--->查询内容{}", queryInfo.getPageNumber(), queryInfo.getPageSize(), queryInfo.getQueryString());
        PageHelper.startPage(queryInfo.getPageNumber(), queryInfo.getPageSize());
        Page<Sport> sports = sportMapper.findPage(queryInfo.getQueryString());
        long total = sports.getTotal();
        List<Sport> result = sports.getResult();
        log.info("查询的总条数-->{}", total);
        log.info("分页列表-->{}", result);
        return PageResult.pageResult(total, result);
    }

    @Override
    public Result findInfo(Long id) {
        return Result.success("查询成功", sportMapper.findInfo(id));
    }






}

4.新建SportMapper
package com.jihu.javasport.mapper;


import com.github.pagehelper.Page;
import com.jihu.javasport.entity.Sport;
import org.apache.ibatis.annotations.Insert;



/**
 * @author yin
 * @since 2023-01-22
 */
public interface SportMapper {

    /**
     * 添加
     * @param sport
     */

    void insert(Sport sport);

    /**
     * 删除
     * @param id
     */
    void delete(Long id);

    /**
     * 根据id查询运动信息
     * @param id
     */
    Sport findInfo(Long id);

    /**
     * 修改
     * @param sport
     */
    void update(Sport sport);

    /**
     * 分页查询
     * @param queryString
     * @return
     */
    Page<Sport> findPage(String queryString);

    /**
     * 添加微信运动步数
     * @param runs
     */
//    void insertStep(@Param("runs") List<WxRun> runs);

    /**
     * 添加微信运动步数
     * @param openid 用户唯一标志
     * @param time 时间
     */
//    @Select("select * from wx_run where openid=#{openid} and time=#{time}")
//    WxRun findStepByTime(@Param("openid") String openid, @Param("time") String time);
//
//    @Update("update wx_run set step=#{step} where openid=#{openid} and time=#{time}")
//    void updateStep(@Param("openid") String openid, @Param("time") String time, @Param("step") Integer step);
//
//    @Select("select * from wx_run where openid=#{openid} and (time<=#{beginTime} and time>=#{endTime})")
//    List<WxRun> findByTime(@Param("beginTime") String dateTime, @Param("endTime") String date1, @Param("openid") String openId);
}

5.新建SportMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jihu.javasport.mapper.SportMapper">

    <!-- 分页查询数据 -->
    <select id="findPage" resultType="com.jihu.javasport.entity.Sport">
        select * from sport where del=0
        <if test="queryString != null and queryString.length > 0">
            and (title like concat('%', #{queryString}, '%') or content like concat('%', #{queryString}, '%'))
        </if>
    </select>

    <select id="findInfo" resultType="com.jihu.javasport.entity.Sport">
        select * from sport where id=#{id}
    </select>

    <!--  添加  -->
    <insert id="insert" parameterType="com.jihu.javasport.entity.Sport" >
    insert into sport(title, content, create_time, create_name)
    values
    (#{title}, #{content}, #{createTime}, #{createName})
    </insert>

    <!-- 删除 -->
    <delete id="delete" parameterType="java.lang.Long">
        update sport set del=1 where id=#{id}
    </delete>

    <!-- 修改 -->
    <update id="update" parameterType="com.jihu.javasport.entity.Sport">
        update sport
        <set>
            <if test="title != null">
                title = #{title},
            </if>
            <if test="content != null">
                `content` = #{content},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateName != null">
                update_name = #{updateName}
            </if>
        </set>
        where id=#{id}
    </update>

    <!-- 添加微信运动步数 -->
<!--    <insert id="insertStep" parameterType="com.ajie.entity.WxRun">-->
<!--        insert into wx_run values-->
<!--        <foreach collection="runs" item="i" separator=",">-->
<!--            (#{i.openid}, #{i.time}, #{i.step})-->
<!--        </foreach>-->
<!--    </insert>-->

</mapper>
6.新建Sport
package com.jihu.javasport.entity;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

/**
 *  * @author yin
 *  * @since 2023-01-22
 */
@Data
@ApiModel(value = "运动简介实体")
public class Sport {

    @ApiModelProperty(value = "主键")
    private Long id;

    @ApiModelProperty(value = "标题")
    private String title;

    @ApiModelProperty(value = "内容")
    private String content;

    @ApiModelProperty(value = "创建时间")
    private String createTime;

    @ApiModelProperty(value = "创建者")
    private String createName;

    @ApiModelProperty(value = "更新时间")
    private String updateTime;

    @ApiModelProperty(value = "更新者")
    private String updateName;

    @ApiModelProperty(value = "删除标记")
    private boolean del;

}

7.新建DateUtils(时间工具类)
package com.jihu.javasport.util;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 时间工具类
 */
public class DateUtils {
    private static final String YYYY_MM_DD="yyyy-MM-dd";
    private static final String YYYY_MM_DD_HH_MM_SS= "yyyy-MM-dd HH:mm:ss";

    /**
     * 返回当前时间字符串 年月日
     * @return
     */
    public static String getDate(){
        SimpleDateFormat format = new SimpleDateFormat(YYYY_MM_DD);
        return format.format(new Date());
    }

    /**
     * 返回当前时间字符串 年月日 时分秒
     * @return
     */
    public static String getDateTime(){
        SimpleDateFormat format = new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS);
        return format.format(new Date());
    }

    /**
     * 将时间字符串转化为时间对象
     * @param time
     * @return
     */
    public static Date formatDate(String time) {
        SimpleDateFormat format = new SimpleDateFormat();
        try {
            return format.parse(time);
        } catch (ParseException e) {
            return null;
        }
    }

//    public static void main(String[] args) {
//        System.out.println(getDate());
//        System.out.println(getDateTime());
//    }
}

3.运动资讯(前端实现)

vue-sport\src\views\sport\intorduction.vue

<template>
    <div>
        <el-card >
            <el-row>
                <el-col :span="8">
                    <!-- @clear="findPage":清除事件, 在输入框清楚后会调用findpage方法 -->
                    <el-input placeholder="请输入内容" clearable v-model="queryInfo.queryString" @clear="findPage" class="input-with-select">
                        <!--@click="findPage":点击搜索图标时,会进行搜索,因为与queryInfo.queryString属性绑定着呢,会将queryString值传给后端  -->
                        <el-button slot="append" icon="el-icon-search" @click="findPage"></el-button>
                    </el-input>
                </el-col>
                <el-col :span="2">
                        <el-button @click="insert" type="primary" style="margin-left: 15px;">添加信息</el-button>
                </el-col>
            </el-row>
            <!-- show-overflow-tooltip: 多出的部分隐藏 -->
            <el-table :data="tableList"
            v-loading="loading"
            element-loading-text="拼命加载中"
            element-loading-spinner="el-icon-loading" >
                <el-table-column type="expand">
                    <template slot-scope="scope" >
                        <!-- <span>{{scope.row.content}}</span> -->
                        <div v-html="scope.row.content" v-highlight />
                    </template>
                </el-table-column>
                <el-table-column type="index" label="序号"  />
                <el-table-column prop="title" label="标题" show-overflow-tooltip />
                <el-table-column prop="createTime" label="创建时间"  />
                <el-table-column prop="createName" label="创建者"  />
                <el-table-column prop="updateTime" label="更新时间"  />
                <el-table-column prop="updateName" label="更新者"  />
                <el-table-column prop="del" label="删除者"  />
                <el-table-column fixed="right" label="操作" width="180">
                    <template slot-scope="scope" >
                        <el-button type="primary" @click="update(scope.row)"   size="small" icon="el-icon-edit" circle />
                        <el-button type="danger" @click="deleteById(scope.row.id)" size="small"  icon="el-icon-delete" circle />
                    </template>
                </el-table-column>      
            </el-table>
            <el-pagination
                v-if="total > 0"
                @size-change="handlePageSize"
                @current-change="handlePageNumber"
                :current-page="queryInfo.pageNumber"
                :page-sizes="[6, 10, 20, 50,100]"
                :page-size="queryInfo.pageSize"
                layout="total, sizes, prev, pager, next, jumper"
                :total="total">
            </el-pagination>
        </el-card>
        <!-- 表单添加和修改 -->
        <el-dialog :title="title"  :visible.sync="open" width="60%" @close="dialogClose">
            <el-form :model="form" ref="form" :rules="rules" label-position="top" style="text-align: center;">
            <el-form-item label="标题" prop="title" >
                <el-input v-model="form.title" placeholder="请输入标题" />
            </el-form-item>
            <el-form-item label="内容" prop="content" >
                <mavon-editor ref="md" v-model="form.content" @imgAdd="uploadImage" @change="change" />
            </el-form-item>
            </el-form>
            <div slot="footer" class="dialog-footer">
                <el-button type="primary" @click="clickOk">确 定</el-button>
                <el-button @click="clickCancel">取 消</el-button>
            </div>
        </el-dialog>
    </div>
</template>

<script>
import { mavonEditor } from 'mavon-editor'
import 'mavon-editor/dist/css/index.css'
import { marked } from "marked";

export default {
    components:{
        mavonEditor,
    },
        data(){
            return{
                //分页查询条件
                queryInfo:{
                    pageNumber:1, //页码
                    pageSize:6, //页数
                    queryString :null, //关键字
                },
                //表格数据
                tableList:[],
                //表格加载
                loading:false,
                //总记录数
                total:0,
                //表单的标题
                title:null,
                //是否打开对话框
                open:false,
                //表单数据
                form:{},
                //表单效验
                rules:{
                    title:[
                        { required: true, message: '标题不能为空', trigger: 'blur' },
                        { max: 30, message: '标题最大不能超过30位', trigger: 'change' },
                    ],
                    code:[
                        { required: true, message: '内容不能为空', trigger: 'blur' },
                    ],
                }
            }
        },
        //页面初始化调用方法
        created(){
            this.findPage();
        },
        methods:{
            // 添加信息
            insert(){
                // console.log("添加按钮")
                this.title = "新增运动资讯";
                this.open = true;
            },  
            //修改信息  
            update(row){
                //row 就是这一行的数据
                console.log("intorductionupdate",row);
                this.form = row;
                this.title = "修改运动资讯";
                this.form.content = row.remark;
                this.open = true;
            },    
             // 分页查询
            findPage(){
                //loading 显示加载效果
                this.loading = true;
                this.$ajax.post('/sport/findPage',this.queryInfo).then((res)=>{
                    // console.log(res)
                    this.loading = false;
                    this.tableList = res.data.rows.filter(item=>{
                        if(item.content && item.content !== ''){
                            item.content = marked(item.content);
                            item.remark = item.content;
                        }
                        return true;
                    });
                    this.total = res.data.total;
                })
            },
             //页码改变事件
            handlePageNumber(newPageNumber){
                //将分页的新数据赋值给分页参数
                this.queryInfo.pageNumber = newPageNumber;
                //掉分页的方法
                this.findPage();
            },
            //页数改变事件
            handlePageSize(nwePageSize){
                this.queryInfo.pageSize = nwePageSize;
                this.findPage();
            },
             //删除权限信息
            deleteById(id){
                this.$confirm('您将永久删除编号为{'+ id +'} 的数据, 是否继续?', '提示', {
                confirmButtonText: '确定',
                cancelButtonText: '取消',
                type: 'warning'
                }).then(() => {
                    this.$ajax.delete(`/sport/delete/${id}`).then(res=>{
                        this.$message.success(res.data.message);
                        this.queryInfo.pageNumber = 1;
                        //重新查询
                        this.findPage();
                    })
                }).catch(() => {
                    this.$message({
                        type: 'info',
                        message: '已取消删除'
                    });          
                });
            },
               //关闭对话框的事件
               dialogClose(){
                //将整个表单进行重置
                this.$refs.form.resetFields();    
            },
            //点击取消
            clickCancel(){
                // this.$refs.form.resetFields();
                this.open = false;
                this.findPage();
            },
              // 点击确定,添加权限数据
              clickOk(){
                //进行表单效验
                this.$refs.form.validate((valid)=>{
                    //效验不通过
                    if(!valid) return this.$message.error('表单效验不通过,请检查后提交!');
                    //效验通过 判断是否是新增
                    if(this.form.id === undefined || this.form.id === null){
                        this.$ajax.post('/sport/insert',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }else{
                        this.$ajax.post('/sport/update',this.form).then((res)=>{
                            this.$message.success(res.data.message);
                            this.open = false;
                            this.findPage();
                        });
                    }
                })
            },
            // markdown中上传本地图片
            uploadImage(pos,$e){
                let formData = new FormData();
                formData.append('file',$e);
                this.$ajax.post('/tool/upload',formData).then(res=>{
                    this.$refs.md.$img2Url(pos,this.$qiniu + res.data.data);
                })

            },
            change(row){
                console.log('chang',row)
            }
        }
    }
</script>

<style  scoped>

</style>

5.17.运动管理--运动项目

5.18.运动管理--运动商品

6.微信小程序模块开发

1.微信小程序登录模块

所需依赖

  <!--  http请求工具类      -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.13</version>
        </dependency>

安装vant插件步骤

参考:https://blog.csdn.net/qq_39236157/article/details/126174674

1.微信小程序中输入 npm init ,会出现package.json文件

2.安装vant插件

npm i @vant/weapp -S --production

3.修改 app.json

将 app.json 中的 “style”: “v2” 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。

4.修改 project.config.json

开发者工具创建的项目,miniprogramRoot 默认为 miniprogram,package.json 在其外部,npm 构建无法正常工作。
需要手动在 project.config.json 内添加如下配置,使开发者工具可以正确索引到 npm 依赖的位置。

{
  ...
  "setting": {
    ...
    "packNpmManually": true,
    "packNpmRelationList": [
      {
        "packageJsonPath": "./package.json",
        "miniprogramNpmDistDir": "./miniprogram/"
      }
    ]
  }
}

注意: 由于目前新版开发者工具创建的小程序目录文件结构问题,npm构建的文件目录为miniprogram_npm,并且开发工具会默认在当前目录下创建miniprogram_npm的文件名,所以新版本的miniprogramNpmDistDir配置为’./'即可

5.构建 npm 包

在左上角的工具中 点击 构建npm

若出现这个错误提示,则需要在根目录下建立miniprogram文件夹

然后再点击【工具 -> 构建 npm】,构建成功提示如下

构建成功后会在【miniprogram】文件夹中生成很多文件

新的开发版本中,详情——本地设置中没有【使用 npm 模块勾选框选项】,则不用理会,如果有则需勾选

6.引入组件

以 Button 组件为例,只需要在app.jsonindex.json中配置 Button 对应的路径即可。

所有组件文档中的引入路径均以 npm 安装为例,如果你是通过下载源代码的方式使用 @vant/weapp,请将路径修改为项目中 @vant/weapp 所在的目录。

  "usingComponents": {
    "van-button": "/miniprogram/miniprogram_npm/@vant/weapp/button/index"
  }

引入组件后,可以在 wxml 中直接使用组件

<van-button type="primary">按钮</van-button>

可以看到引入vant组件成功了

2.微信小程序底部导航

自定义tabBar以及图标-使用vant-weapp 参考:https://blog.csdn.net/Utopia_Zq/article/details/111028339

1、在小程序根目录下创建custom-tab-bar文件夹,并创建以下文件。(这个是作为入口文件的)

2、修改custom-tab-bar/index.js (清除初始化的内容)

Component({
  data: { 
    selected: 0,
    list: [
      {
        pagePath: "/pages/index/index",
        text: "运动资讯",
        icon:'smile-o'
      },
      {
        pagePath: "/pages/food/food",
        text: "饮食推荐",
        icon:'like-o'
      },
      {
        pagePath: "/pages/sport/sport",
        text: "运动统计",
        icon:'fire-o'
      },
      {
        pagePath: "/pages/user/user",
        text: "我的",
        icon:'user-o'
      }
    ]
  },
  methods: {
    onChange(e) {
       console.log(e,'e')
       this.setData({ active: e.detail });
       wx.switchTab({
         url: this.data.list[e.detail].pagePath
       });
    },
    init() {
        const page = getCurrentPages().pop();
        this.setData({
          active: this.data.list.findIndex(item => item.pagePath === `/${page.route}`)
        });
       }
    }
})

3、修改custom-tab-bar/index.json

{
  "component": true,
    "usingComponents": {
      "van-tabbar": "/miniprogram/miniprogram_npm/@vant/weapp/tabbar/index",
      "van-tabbar-item": "/miniprogram/miniprogram_npm/@vant/weapp/tabbar-item/index"
      
    }
}

4、 修改custom-tab-bar/index.wxml

<van-tabbar active="{{ active }}" bind:change="onChange" active-color="#07c160" inactive-color="gainsboro">
  <van-tabbar-item wx:for="{{ list }}" wx:key="index" icon="{{item.icon}}">
    {{item.text}}
  </van-tabbar-item>
</van-tabbar>

5.在app.json添加tabBar

 "usingComponents": {
    "van-icon": "/miniprogram/miniprogram_npm/@vant/weapp/icon/index"
  },
"tabBar": {
    "custom": true,
    "color": "#000000",
    "selectedColor": "#000000",
    "backgroundColor": "#000000",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "运动资讯"
      },
      {
        "pagePath": "pages/food/food",
        "text": "饮食推荐"
      },
      {
        "pagePath": "pages/sport/sport",
        "text": "运动统计"
      },
      {
        "pagePath": "pages/user/user",
        "text": "个人中心"
      }
    ]
  },

6、每个tabBar页面的onShow()方法添加(为了消除两次点击

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    this.getTabBar().init();
  },

效果图如下

3.运动资讯模块

1.让小程序完美支持Markdown,参考:https://blog.csdn.net/qq_34375473/article/details/120673133

2.微信小程序引入阿里组件库: 参考:https://www.likecs.com/show-204203193.html

终端输入

1. npm install mini-program-iconfont-cli --save-dev

2. npx iconfont-init 

3.//iconfont.json
{
    "symbol_url": "请参考README.md,复制 http://iconfont.cn 官网提供的JS链接", //点击symbol生成的代码连接
    "save_dir": "./iconfont",
    "use_rpx": false,
    "trim_icon_prefix": "icon",
    "default_icon_size": 18
}

4. npx iconfont-wechat

5.在app.json文件里设置使用图标组件
 "usingComponents": {
    "iconfont": "/iconfont/iconfont"
  },
      
6. 使用图标
 <iconfont name="{{item.icon}}" size="25"  />

3.微信小程序引用echarts

参考:https://zhuanlan.zhihu.com/p/528731424

1.现在官网下载echarts的zip文件
https://github.com/ecomfe/echarts-for-weixin

下载后解压文件,把其中ec-canvas整个文件放在项目中,ec-canvas是echarts官方提供的组件。也可自行去下载最新的。

2.index.json配置
{
"usingComponents": {
	"ec-canvas": "../../ec-canvas/ec-canvas"
	}
} 
3.使用echarts
<!--index.wxml-->
<view class="container">
	<ec-canvas id="mychart-dom-graph" canvas-id="mychart-graph" ec="{{ ec }}"></ec-canvas>
</view>

3.1新建 pages\index\index.js

// index.js
// 获取应用实例
const app = getApp()

Page({

  /**
   * 页面的初始数据
   */
  data: {
    //请求数据
    queryInfo:{
      pageNumber:1,
      pageSize: 5
    },
    //上拉时是否继续请求数据,即是否还有更多数据  true可以继续上拉,false禁止上拉数据
    hasMoreData:true,
    //响应的数据
    tableList: [],

  },
  /**
   * 分页查询
   * @param {*} message :提示语 
   */
  findPage(message){
    //在当前页面显示导航条加载动画
    wx.showNavigationBarLoading();
    wx.showToast({
      title: message,
      icon:'loading',
    })
    wx.request({
      url: 'http://localhost:9000/sport/findPage',
      data:this.data.queryInfo,
      method:'POST',
      header:{
        'content-type': 'application/json', // 默认值
        'Authorization': wx.getStorageSync('token')
      },
      success:(res)=>{
        console.log("findpage",res);
        //将原有的结果列表定义为临时数组
        let temp = this.data.tableList;
        //将新的结果接收
        let result= res.data.rows;
        if(result.length > 0){
          //如果分页页码为1,那么说明用户再下拉
          if(this.data.queryInfo.pageNumber  == 1){
            temp = [];
          }
          //如果结果长度大于分页数,那么说明还可以上拉数据,否则禁止上拉
          if(result.length < this.data.queryInfo.pageSize){
            this.setData({
              tableList: temp.concat(result),
              hasMoreData: false
            });
          }else{
            this.setData({
              tableList: temp.concat(result),
              hasMoreData: true,
              queryInfo: {
                pageNumber: this.data.queryInfo.pageNumber + 1,
                pageSize: 5,
              }
            });
          }
        }else{
          this.setData({
            hasMoreData: false
          });
          //在当前页面显示导航条加载动画
          wx.showNavigationBarLoading();
          //显示 loading 提示框
          wx.showLoading({
            title: '没有更多数据了!',
          });
          wx.hideNavigationBarLoading();
          wx.hideLoading();
        }
      wx.hideNavigationBarLoading();
	  	wx.hideToast();
      },
      fail:(err)=>{
        console.log(err)
      }
    })
  },
/**
 * 跳转详情页
 * @param {*} id 
 */
  toDetails(event){
    // console.log(event)
    wx.navigateTo({
      url: `/pages/index/details/details?id=${event.target.dataset.id}`,
    })
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
    this.findPage('加载中...');
  },

  /**
   * 生命周期函数--监听页面初次渲染完成
   */
  onReady: function () {
    
  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow: function () {
    this.getTabBar().init();
  },

  /**
   * 生命周期函数--监听页面隐藏
   */
  onHide: function () {
    
  },

  /**
   * 生命周期函数--监听页面卸载
   */
  onUnload: function () {
    
  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh: function () {
    this.data.queryInfo.pageNumber = 1;
    this.findPage('数据刷新中...')
  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom: function () {
    if(this.data.hasMoreData){
      this.findPage('加载更多数据...')
    }
  },

  /**
   * 用户点击右上角分享
   */
  onShareAppMessage: function () {
    
  }
})

3.2新建pages\index\index.json

{
  "usingComponents": {
    "van-button": "/miniprogram/miniprogram_npm/@vant/weapp/button/index",
    "van-card": "/miniprogram/miniprogram_npm/@vant/weapp/card/index"
  },
  "enablePullDownRefresh": true,
  "navigationBarTitleText": "运动资讯"

}

3.3新建pages\index\index.wxml

<!--index.wxml-->
<view >
    <van-card
    wx:if="{{tableList.length>0}}"
    wx:for="{{tableList}}"
    wx:key="index"
    desc="发表时间:{{item.createTime}}"
    title="标题:{{item.title}}"
    >
    <view slot="footer">
      <van-button type="primary" size="mini" data-id="{{item.id}}" bindtap="toDetails">查看详情</van-button>
    </view>
    </van-card>
    <van-empty wx:if="{{tableList.length < 1}}" description="暂无数据"/>

</view>

3.4新建pages\index\details\details.js(查看详情模块)

// pages/index/details.js
const app = getApp();
Page({

  /**
   * 页面的初始数据
   */
  data: {
    //存储运动资讯详情
    data:{}

  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    // console.log("详情页面:",options)
    wx.request({
      url: `http://localhost:9000/sport/${options.id}`,
      method:'GET',
      header:{
        'Authorization': wx.getStorageSync('token')
      },
      success:(res)=>{
        // console.log(res)
        let content = res.data.data.content;
        if(content){
          res.data.data.content = app.towxml(content,'markdown',{
            //主题 dark 黑色,light白色,不填默认是light
            theme:'dark'
          })
        }
        this.setData({
          data:res.data.data
        })
      },
      fail:(err)=>{
        console.log(err)
      }
    })
  },


})

3.5新建pages\index\details\details.json(查看详情模块)

{
  "usingComponents": {
    "van-tag": "/miniprogram/miniprogram_npm/@vant/weapp/tag/index",
    "towxml":"/towxml/towxml"
  },
  "navigationBarTitleText": "运动资讯详情"
}

3.6新建pages\index\details\details.wxml(查看详情模块)

<!--pages/index/details.wxml-->
<view>
  创建者:<van-tag  plain  type="warning">{{data.createName}}</van-tag>
  创建时间:<van-tag type="primary">{{data.createTime}}</van-tag>
  <towxml nodes="{{data.content}}"/>
</view>

4.饮食推荐模块

4.1食物分类模块
1.pages\food\food.js
// pages/food/food.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    //请求数据
    queryInfo:{
      pageNumber:1,
      pageSize: 5
    },
     //上拉时是否继续请求数据,即是否还有更多数据  true可以继续上拉,false禁止上拉数据
     hasMoreData:true,
     //响应的数据
     tableList: [],
  },

  findPage(message){
    wx.request({
      url: 'http://localhost:9000/food/typeAll',
      method:'GET',
      header:{
        'content-type': 'application/json', // 默认值
        'Authorization': wx.getStorageSync('token')
      },
      success:(res)=>{
        console.log(res);
        this.setData({
          tableList: res.data.data
        })
      },
      fail:(err)=>{
        console.log(err)
      }

    })
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    this.findPage();
  },

  /**
   * 生命周期函数--监听页面显示
   */
  onShow() {
    this.getTabBar().init();
  }

})
2.pages\food\food.json
{
  "usingComponents": {
    "van-grid": "/miniprogram/miniprogram_npm/@vant/weapp/grid/index",
    "van-grid-item": "/miniprogram/miniprogram_npm/@vant/weapp/grid-item/index"
  },
  "navigationBarTitleText": "食物分类"
}
3.pages\food\food.wxml
<!--pages/food/food.wxml-->
<van-grid column-num="3">
  <van-grid-item 
   use-slot 
   wx:for="{{ tableList }}"
   wx:key="index" 
   link-type="navigateTo"
   url="/pages/food/foods/foods?id={{item.id}}">
    <iconfont name="{{item.icon}}"  />
    <view>{{item.title}}</view>
  </van-grid-item>
</van-grid>


4.2食物列表模块
1.pages\food\foods\foods.js
// pages/food/foods/foods.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    queryInfo:{
      pageNumber:1,
      pageSize: 5,
      typeId: undefined,
      keywords: '',
    },
    tableList:[],
     //上拉时是否继续请求数据,即是否还有更多数据  true可以继续上拉,false禁止上拉数据
     hasMoreData:true,
  },
  onChange(e){
    // console.log(e);
    this.data.queryInfo.keywords = e.detail;
    this.data.queryInfo.pageNumber = 1;
    this.findPage('查询中...');
  },
/**
   * 分页查询
   * @param {*} message :提示语 
   */
  findPage(message){
    //在当前页面显示导航条加载动画
    wx.showNavigationBarLoading();
    wx.showToast({
      title: message,
      icon:'loading',
    })
    wx.request({
      url: 'http://localhost:9000/food/mini/findPage',
      data:this.data.queryInfo,
      method:'POST',
      header:{
        'content-type': 'application/json', // 默认值
        'Authorization': wx.getStorageSync('token')
      },
      success:(res)=>{
        console.log("findpage",res);
        //将原有的结果列表定义为临时数组
        // let temp = this.data.rows;
        let temp = this.data.tableList;
        //将新的结果接收
        let result= res.data.rows;
        if(result.length > 0){
          //如果分页页码为1,那么说明用户再下拉
          if(this.data.queryInfo.pageNumber  == 1){
            temp = [];
          }
          //如果结果长度大于分页数,那么说明还可以上拉数据,否则禁止上拉
          if(result.length < this.data.queryInfo.pageSize){
            this.setData({
              tableList: temp.concat(result),
              hasMoreData: false
            });
          }else{
            this.setData({
              tableList: temp.concat(result),
              hasMoreData: true,
              queryInfo: {
                pageNumber: this.data.queryInfo.pageNumber + 1,
                pageSize: 5,
                typeId: this.data.queryInfo.typeId
              }
            });
          }
        }else{
          this.setData({
            hasMoreData: false
          });
          //在当前页面显示导航条加载动画
          wx.showNavigationBarLoading();
          //显示 loading 提示框
          wx.showLoading({
            title: '没有更多数据了!',
          });
          wx.hideNavigationBarLoading();
          wx.hideLoading();
        }
      wx.hideNavigationBarLoading();
	  	wx.hideToast();
      },
      fail:(err)=>{
        console.log(err)
      }
    })
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    //options 可以把前面选择的id传过来
    // console.log(options);
    this.data.queryInfo.typeId = options.id;
    this.findPage('数据加载中...');
  },

  /**
   * 页面相关事件处理函数--监听用户下拉动作
   */
  onPullDownRefresh() {
    this.data.queryInfo.pageNumber = 1;
    this.findPage('数据刷新中...')
  },

  /**
   * 页面上拉触底事件的处理函数
   */
  onReachBottom() {
    if(this.data.hasMoreData){
      this.findPage('加载更多数据...')
    }
  }

})
2.pages\food\foods\foods.json
{
  "usingComponents": {
    "van-card": "/miniprogram/miniprogram_npm/@vant/weapp/card/index",
    "van-search": "/miniprogram/miniprogram_npm/@vant/weapp/search/index"
  },
  "enablePullDownRefresh": true,
  "navigationBarTitleText": "食物列表"
}
3.pages\food\foods\foods.wxml
<!--pages/food/foods/foods.wxml-->
<van-search value="{{ queryInfo.keywords}}" placeholder="请输入搜索关键词" bind:change="onChange" />
<view>
  <van-card
  wx:for="{{tableList}}"
  wx:key="index"
  desc="{{item.heat + '千卡 / ' + item.nutrient }}"
  title="{{item.title}}"
  thumb="{{ image.split(item.imageUrls) }}"
  data-image="{{ item.imageUrls }}"
  link-type="navigateTo"
  thumb-link="/pages/food/foods/details/details?id={{item.id}}"
/>
</view>
<wxs module="image">
   // 暴露使用的方法
   module.exports = {
        split: function (str) {
            if (str) {
                return 'http://rook9tu2s.hn-bkt.clouddn.com/' + str.split(',')[0];
            } else {
                return 'https://img.yzcdn.cn/vant/cat.jpeg';
            }
        }
    }
</wxs>

4.3食物详情模块
1.pages\food\foods\details\details.js
// pages/food/foods/details/details.js
import * as echarts from '../../../../ec-canvas/echarts';
let echartsData = [];
/**
 * 初始化数据
 */
function initChart(canvas, width, height, dpr){
  const chart = echarts.init(canvas, null, {
    width: width,
    height: height,
    devicePixelRatio: dpr // 像素
  });
  canvas.setChart(chart);
  let option = {
    legend: {
        orient: 'vertical',
        //图标设置样式
        icon: "circle",
        //位置
        bottom: 'bottom',
        //格式化名字
        formatter: function (name) {
            let arr = [];
            echartsData.forEach(item => {
                if (item.name == name) {
                    arr.push(name + item.value + 'g');
                }
            });
            return arr.join('');
  }
    },
    series: [
        {
            type: 'pie',
            radius: '50%',
            data: echartsData,
            label: {
                normal: {
                    show: true,
                    formatter: '{b}{d}%',//模板变量有 {a}、{b}、{c}、{d},分别表示系列名,数据名,数据值,百分比。{d}数据会根据value值计算百分比
                }
            }
        }
    ]
};
  chart.setOption(option);
  return chart;
}

Page({

  /**
   * 页面的初始数据
   */
  data: {
    dataInfo:{},
    imageUrls:[],
    // 饼状图数据
    ec:{
      onInit: initChart
    }
  },
// 跳转 查看全部营养元素 页面
  getFoodDetail(){
    wx.navigateTo({
      url: '/pages/food/foods/foodDetails/foodDetails?data=' + JSON.stringify(this.data.dataInfo)
    });
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    // console.log(options.id)
    wx.request({
      url: 'http://localhost:9000/food/' + options.id,
      method:'GET',
      header:{
        'Authorization': wx.getStorageSync('token')
      },
      success:(res)=>{
        console.log('食物详情',res)
        let { data } = res.data;
        let temp = [];
        if(data.imageUrls){
          let arr = data.imageUrls.split(',');
          if(arr.length > 0){
            arr.forEach( i=>{
              temp.push('http://rook9tu2s.hn-bkt.clouddn.com/' + i);
            });
          }else{
            temp.push('http://rook9tu2s.hn-bkt.clouddn.com/' + arr[0]);
          }
        }else{
          temp.push('https://img.yzcdn.cn/vant/cat.jpeg');
        }
        this.setData({
          dataInfo : data,
          imageUrls : temp
        }),
        echartsData = [
          {value: data.fat, name: '脂肪'},
          {value: data.protein, name: '蛋白质'},
          {value: data.carbonWater, name: '碳水化合物'}
        ]
       
      },
      fail:(err)=>{
        console.log(err)
      }
    })
  },


})
2.pages\food\foods\details\details.json
{
  "usingComponents": {
    "van-cell": "/miniprogram/miniprogram_npm/@vant/weapp/cell/index",
    "van-cell-group": "/miniprogram/miniprogram_npm/@vant/weapp/cell-group/index",
    "ec-canvas": "../../../../ec-canvas/ec-canvas"
  },
  "navigationBarTitleText": "食物详情"
}
3.pages\food\foods\details\details.wxml
<!-- pages/food/foods/details/details.wxml -->
 <view>
<!-- 轮播图 -->
  <swiper indicator-dots  autoplay interval="2000" >
    <block wx:for="{{imageUrls}}" wx:key="index">
      <swiper-item>
      <image src="{{item}}" class="swiper-image" />
      </swiper-item>
    </block>
</swiper>
<!-- 食物描述 -->
<van-cell-group>
  <van-cell title="{{dataInfo.title}}" value="{{ dataInfo.heat + ' 千卡 / ' + dataInfo.nutrient }}" />
</van-cell-group>
<!-- echarts食物统计(饼状图) -->
<view style="width: 100%;height: 200px;">
  <ec-canvas id="mychart-dom-pie" canvas-id="mychart-pie" force-use-old-canvas="true" ec="{{ ec }}"></ec-canvas>
</view>

<!-- 温馨提示 -->
<view class="hr"/>
  <view class="hr-text" bindtap="getFoodDetail">查看全部营养元素</view>
  <view class="tip">
      <view style="font-size: 14px;">使用提示:</view>
      绿灯食物:代表在膳食指南推荐范围内可以每天足量吃的食物,
      绝大部分的蔬菜水果、粗细粮、奶制品以及低脂肪的肉类都是绿灯食物。
</view>

</view> 
4.pages\food\foods\details\details.wxss
.swiper-image {
  width: 100%;
}

.title {
  font-size: 15px;
  font-weight: bold;
}

.tip {
  margin: 3% 3% auto;
  font-size: 13px;
  color:chocolate;
}

.hr {
  background: #E4E6ED;
  width: 100%;
  height: 1rpx;
}

.hr-text {
  text-align: right;
  margin-right: 3%;
  font-size: 13px;
  color: dodgerblue;
  margin-bottom: 2px;
}

4.4食物营养元素详情模块
1.pages\food\foods\foodDetails\foodDetails.js
// pages/food/foods/foodDetails/foodDetails.js
Page({

  /**
   * 页面的初始数据
   */
  data: {
    dataInfo:{},
  },

})
2.pages\food\foods\foodDetails\foodDetails.json
{
  "usingComponents": {
    "van-cell": "/miniprogram/miniprogram_npm/@vant/weapp/cell/index",
    "van-cell-group": "/miniprogram/miniprogram_npm/@vant/weapp/cell-group/index",
    "van-field": "/miniprogram/miniprogram_npm/@vant/weapp/field/index"
  },
  "navigationBarTitleText": "食物营养元素详情"
}
3.pages\food\foods\foodDetails\foodDetails.wxml
<view>
    <van-cell-group>
        <van-field
            value="{{ dataInfo.nutrient }}"
            label="营养元素"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.heat + '千卡' }}"
            label="热量"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.protein + '克' }}"
            label="蛋白质"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.fat + '克' }}"
            label="脂肪"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.carbonWater + '克' }}"
            label="碳水化合物"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.dietaryFiber + '克' }}"
            label="膳食纤维"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.vitaminA + 'ugRE' }}"
            label="维生素A"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.vitaminC + '毫克' }}"
            label="维生素C"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.vitaminE + '克' }}"
            label="维生素E"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.carrot + '微克' }}"
            label="胡萝卜素"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.vitaminB1 + '毫克' }}"
            label="维生素B1"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.vitaminB2 + '毫克' }}"
            label="维生素B2"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.niacin + '毫克' }}"
            label="烟酸"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.cholesterol + '毫克' }}"
            label="胆固醇"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.magnesium + '毫克' }}"
            label="镁"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.iron + '毫克' }}"
            label="铁"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.calcium + '毫克' }}"
            label="钙"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.zinc + '毫克' }}"
            label="锌"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.copper + '毫克' }}"
            label="铜"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.manganese + '毫克' }}"
            label="锰"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.potassium + '毫克' }}"
            label="钾"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.phosphorus + '毫克' }}"
            label="磷"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.sodium + '毫克' }}"
            label="钠"
            input-align="right"/>
        <van-field
            value="{{ dataInfo.selenium + '毫克' }}"
            label="硒"
            input-align="right"/>
</van-cell-group>
</view>

5.运动统计模块

微信小程序获取手机号 (java解密) 参考:https://blog.csdn.net/qq_36725282/article/details/89175592

所需依赖

 <!--   用于微信小程序解密     -->
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.57</version>
        </dependency>

1.pages\sport\sport.js

// pages/sport/sport.js
import * as echarts from '../../ec-canvas/echarts';
Page({
  /**
   * 页面的初始数据
   */
  data: {
    //运动数据
    ec:{
      //延迟加载
      lazyLoad: true
    },
    data1: [],
		data2: [],
		data3: [],
    data4: [],
    //今日步数
    step: wx.getStorageSync('step') || 0
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad(options) {
    wx.request({
      url: 'http://localhost:9000/mini/step/report',
      method:'GET',
      header:{
        'Authorization': wx.getStorageSync('token')
      },
      success:(res)=>{
        console.log(res);
        const {week1,week2,week3,week4} = res.data.data;
          //迭代运动信息
        for (let i=0; i<7; i++) {
          //装本周数据
          if (week1[i] === null || week1[i] === undefined) {
            this.data.data1.push(null);
          } else {
            this.data.data1.push(week1[i].step);
          }
          this.data.data2.push(week2[i].step);
          this.data.data3.push(week3[i].step);
          this.data.data4.push(week4[i].step);
        }
        this.lineComponent = this.selectComponent('#mychart-dom-line');
			  this.initChart();
      
      }
    })
  },

	/**
	 * 初始化echarts数据
	 */
	initChart: function() {
		this.lineComponent.init((canvas, width, height, dpr) => {
			const chart = echarts.init(canvas, null, {
				width: width,
				height: height,
				devicePixelRatio: dpr // new
			});
			canvas.setChart(chart);
			chart.setOption(this.getOption());
			return chart;
		});
  },

  /**
	 * 折线图数据
	 */
	getOption() {
		return {
			//点击时显示信息
			tooltip: {
				trigger: 'axis'
			},
			legend: {
				data: ['本周', '第二周', '第三周', '第四周']
			},
			//格子数据 刻度
			grid: {
				left: '3%',
				right: '4%',
				bottom: '3%',
				containLabel: true
			},
			//X轴显示日期
			xAxis: {
				type: 'category',
				boundaryGap: false,
				data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
			},
			//Y轴数据
			yAxis: {
				type: 'value'
			},
			series: [
				{
					name: '本周',
					type: 'line',
					data: this.data.data1
				},
				{
					name: '第二周',
					type: 'line',
					data: this.data.data2
				},
				{
					name: '第三周',
					type: 'line',
					data: this.data.data3
				},
				{
					name: '第四周',
					type: 'line',
					data: this.data.data4
				}
			]
		};
	},

})

2.pages\sport\sport.json

{
  "usingComponents": {
    "ec-canvas": "../../ec-canvas/ec-canvas"
  },
  "navigationBarTitleText": "运动统计"
}

3.pages\sport\sport.wxml

<view class="title">
    近四周运动数据统计(步/天)
</view>
<!-- 运功统计 必须设置宽高,不然不显示 -->
<view style="width: 100%;height: 350px;">
  <ec-canvas id="mychart-dom-line" canvas-id="mychart-line"  ec="{{ ec }}"></ec-canvas>
  <!-- <echarts id="mychart-dom-line" canvas-id="mychart-line" ec="{{ ec }}"></echarts> -->
</view>
<!-- 今日步数 -->
<view class="step">
    今日走路{{ step }}步,消耗了{{ step * 28 / 1000 }}千卡
</view>

4.pages\sport\sport.wxss

.title {
  text-align: center;
  color: chocolate;
  margin-bottom: 15px;
}

.step {
  font-family: 华文行楷;
  font-size: 22px;
  color: chartreuse;
  margin-top: 10px;
  text-align: center;
}

6.个人中心模块

1.pages\user\user.js

// index.js
// 获取应用实例
const app = getApp()

Page({

  /**
   * 页面的初始数据
   */
  data: {
    userInfo: {},

  },
  // 获取用户信息
  getInfo(){
    wx.getUserProfile({
      desc: '用于完善会员资料', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
      success: (res) => {
        // console.log(res)
        const { avatarUrl,nickName,gender,country,province,city} = res.userInfo;
        wx.request({
          url: 'http://localhost:9000/mini/update/info',
          method:'POST',
          header:{
            'content-type': 'application/json', // 默认值
            'Authorization': wx.getStorageSync('token')
          },
          data:{
            nickName:nickName,
            sex:gender,
            avatar:avatarUrl,
            address:country + province + city,
            openId: wx.getStorageSync('openid')   
          },
          success:(res)=>{
            console.log(res)
          },
          fail:(err)=>{
            console.log(err)
          }
        })
      }
    })
  },
//退出登录
  logout(){
    wx.clearStorageSync();
    wx.exitMiniProgram();
  }

})

2.pages\user\user.json

{
  "usingComponents": {
    "van-button": "/miniprogram/miniprogram_npm/@vant/weapp/button/index",
    "van-image": "/miniprogram/miniprogram_npm/@vant/weapp/image/index"
  },
  "navigationBarTitleText": "个人中心"
}

3.pages\user\user.wxml

<!--pages/user/user.wxml-->
<!--index.wxml-->
<view class="container">
  <view class="userinfo">
    <van-image round width="10rem" height="10rem" src="{{ userInfo.avatar }}"  bindtap="getInfo" />
    <!-- <cover-image src="{{userInfo.avatar}}"   bindtap="getInfo" class="userinfo-avatar" /> -->
    <van-button type="warning" bindtap="logout">退出登录</van-button>
    <!-- <van-button type="primary" bindtap="getInfo">获取信息</van-button> -->
  </view>

</view>

4.pages\user\user.wxss

/**index.wxss**/
.userinfo {
  display: flex;
  flex-direction: column;
  align-items: center;
  color: #aaa;
}

.userinfo-avatar {
  overflow: hidden;
  width: 180rpx;
  height: 180rpx;
  margin: 20rpx;
  border-radius: 50%;
}

.usermotto {
  margin-top: 200px;
}

5.app.js

// app.js
App({
   // 引入`towxml3.0`解析方法
   towxml:require('/towxml/index'),
  onLaunch() {
    // 展示本地存储能力
    const logs = wx.getStorageSync('logs') || []
    logs.unshift(Date.now())
    wx.setStorageSync('logs', logs)

    // 登录
    wx.login({
      success (res) {
        if (res.code) {
          //发起网络请求
          wx.request({
            url: 'http://localhost:9000/mini/login',
            data: {
              code: res.code
            },
            success:(res)=>{
              console.log("接口请求成功--->",res);
              const {flag , data,message} = res.data;
              if(!flag){
                return wx.showToast({
                  title: message,
                  icon: 'error',
                  duration: 2000
                });
              }
              wx.setStorageSync('token', `${data.tokenHead} ${data.token}`);
              wx.setStorageSync('userInfo', data.userInfo);
              wx.setStorageSync('openid', data.openid);

              //获取微信运动步数
              wx.getWeRunData({
                success: (res)=>{
                  console.log('getWeRunData:',res)
                  // 拿 encryptedData 到开发者后台解密开放数据
                  const {encryptedData,iv }= res;
                  wx.request({
                    url: 'http://localhost:9000/mini/wxrun',
                    method:'POST',
                    data:{
                      encryptedData: encryptedData,
                      iv: iv,
                      sessionKey: data.sessionKey,//微信小程序登录时获取
                      openid: data.openid
                    } ,
                    header:{
                      'Authorization': wx.getStorageSync('token')
                    },
                    success:(res)=>{
                      // console.log("wxrun",res)
                      wx.setStorageSync('step', res.data.data);
                    }
                  })
                  
                },
                fail:(err)=>{
                 return wx.showToast({
                    title: '请关注“微信运动”公众号后重试',
                    icon:'none',
                    duration: 3000
                  })
                }
              });

            },
            fail:(err)=>{
              console.log("接口请求失败--->",err);
            }
          })
        } else {
          console.log('登录失败!' + res.errMsg)
        }
      }
    })
  },
  // 封建的ajax请求,可以用这种方法封装
  // ajax:(url,method,data)=>{
  //   return new Promise((resolve,reject)=>{
  //     wx.request({
  //       url: `http://localhost:9000/${url}`,
  //       method:method,
  //       data:data,
  //       header:{
  //         'Authorization': wx.getStorageSync('token')
  //       },
  //       success:(res)=>{
  //         if(res.data.flag){
  //           resolve(res);
  //         }else{
  //           wx.showToast({
  //             title: res.data.message,
  //             icon: 'error'
  //           })
  //           reject(err);
  //         }
  //       },
  //       fail:(err)=>{
  //         wx.showToast({
  //           title: '网络请求异常',
  //           icon: 'error'
  //         })
  //         reject(err);
  //       }

  //     })
  //   })
  // },
  globalData: {
    userInfo: null
  }
})

6.app.json

{
  "pages": [
    "pages/index/index",
    "pages/user/user",
    "pages/sport/sport",
    "pages/food/food",
    "pages/shopping/shopping",
    "pages/index/details/details",
    "pages/food/foods/foods",
    "pages/food/foods/details/details",
    "pages/food/foods/foodDetails/foodDetails"
  ],
  "usingComponents": {
    "van-icon": "/miniprogram/miniprogram_npm/@vant/weapp/icon/index",
    "van-card": "/miniprogram/miniprogram_npm/@vant/weapp/card/index",
    "van-empty": "/miniprogram/miniprogram_npm/@vant/weapp/empty/index",
    "iconfont": "/iconfont/iconfont"
  },
  "window": {
    "backgroundTextStyle": "light",
    "navigationBarBackgroundColor": "#fff",
    "navigationBarTitleText": "Weixin",
    "navigationBarTextStyle": "black"
  },
  "tabBar": {
    "custom": true,
    "color": "#000000",
    "selectedColor": "#000000",
    "backgroundColor": "#000000",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "运动资讯"
      },
      {
        "pagePath": "pages/food/food",
        "text": "饮食推荐"
      },
      {
        "pagePath": "pages/shopping/shopping",
        "text": "购物"
      },
      {
        "pagePath": "pages/sport/sport",
        "text": "运动统计"
      },
      {
        "pagePath": "pages/user/user",
        "text": "个人中心"
      }
    ]
  },
  "sitemapLocation": "sitemap.json"
}
-->
收藏
关注
评论